diff options
Diffstat (limited to 'subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/communication/CommunicationTracker.java')
-rw-r--r-- | subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/communication/CommunicationTracker.java | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/communication/CommunicationTracker.java b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/communication/CommunicationTracker.java new file mode 100644 index 00000000..d244e644 --- /dev/null +++ b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/communication/CommunicationTracker.java | |||
@@ -0,0 +1,467 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2010-2017, Tamas Szabo, Istvan Rath and Daniel Varro | ||
3 | * This program and the accompanying materials are made available under the | ||
4 | * terms of the Eclipse Public License v. 2.0 which is available at | ||
5 | * http://www.eclipse.org/legal/epl-v20.html. | ||
6 | * | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.viatra.runtime.rete.network.communication; | ||
10 | |||
11 | import java.util.HashMap; | ||
12 | import java.util.HashSet; | ||
13 | import java.util.List; | ||
14 | import java.util.Map; | ||
15 | import java.util.PriorityQueue; | ||
16 | import java.util.Queue; | ||
17 | import java.util.Set; | ||
18 | |||
19 | import tools.refinery.viatra.runtime.rete.itc.alg.incscc.IncSCCAlg; | ||
20 | import tools.refinery.viatra.runtime.rete.itc.alg.misc.topsort.TopologicalSorting; | ||
21 | import tools.refinery.viatra.runtime.rete.itc.graphimpl.Graph; | ||
22 | import tools.refinery.viatra.runtime.matchers.tuple.TupleMask; | ||
23 | import tools.refinery.viatra.runtime.rete.aggregation.IAggregatorNode; | ||
24 | import tools.refinery.viatra.runtime.rete.boundary.ExternalInputEnumeratorNode; | ||
25 | import tools.refinery.viatra.runtime.rete.eval.RelationEvaluatorNode; | ||
26 | import tools.refinery.viatra.runtime.rete.index.DualInputNode; | ||
27 | import tools.refinery.viatra.runtime.rete.index.ExistenceNode; | ||
28 | import tools.refinery.viatra.runtime.rete.index.Indexer; | ||
29 | import tools.refinery.viatra.runtime.rete.index.IndexerListener; | ||
30 | import tools.refinery.viatra.runtime.rete.index.IterableIndexer; | ||
31 | import tools.refinery.viatra.runtime.rete.index.SpecializedProjectionIndexer; | ||
32 | import tools.refinery.viatra.runtime.rete.network.IGroupable; | ||
33 | import tools.refinery.viatra.runtime.rete.network.NetworkStructureChangeSensitiveNode; | ||
34 | import tools.refinery.viatra.runtime.rete.network.Node; | ||
35 | import tools.refinery.viatra.runtime.rete.network.ProductionNode; | ||
36 | import tools.refinery.viatra.runtime.rete.network.Receiver; | ||
37 | import tools.refinery.viatra.runtime.rete.network.ReteContainer; | ||
38 | import tools.refinery.viatra.runtime.rete.network.communication.timely.TimelyIndexerListenerProxy; | ||
39 | import tools.refinery.viatra.runtime.rete.network.communication.timely.TimelyMailboxProxy; | ||
40 | import tools.refinery.viatra.runtime.rete.network.mailbox.FallThroughCapableMailbox; | ||
41 | import tools.refinery.viatra.runtime.rete.network.mailbox.Mailbox; | ||
42 | import tools.refinery.viatra.runtime.rete.network.mailbox.timeless.BehaviorChangingMailbox; | ||
43 | import tools.refinery.viatra.runtime.rete.single.TransitiveClosureNode; | ||
44 | import tools.refinery.viatra.runtime.rete.single.TrimmerNode; | ||
45 | |||
46 | /** | ||
47 | * An instance of this class is associated with every {@link ReteContainer}. The tracker serves two purposes: <br> | ||
48 | * (1) It allows RETE nodes to register their communication dependencies on-the-fly. These dependencies can be | ||
49 | * registered or unregistered when nodes are disposed of. <br> | ||
50 | * (2) It allows RETE nodes to register their mailboxes as dirty, that is, they can tell the tracker that they have | ||
51 | * something to send to other nodes in the network. The tracker is then responsible for ordering these messages (more | ||
52 | * precisely, the mailboxes that contain the messages) for the associated {@link ReteContainer}. The ordering is | ||
53 | * governed by the strongly connected components in the dependency network and follows a topological sorting scheme; | ||
54 | * those mailboxes will be emptied first whose owner nodes do not depend on other undelivered messages. | ||
55 | * | ||
56 | * @author Tamas Szabo | ||
57 | * @since 1.6 | ||
58 | * | ||
59 | */ | ||
60 | public abstract class CommunicationTracker { | ||
61 | |||
62 | /** | ||
63 | * The minimum group id assigned so far | ||
64 | */ | ||
65 | protected int minGroupId; | ||
66 | |||
67 | /** | ||
68 | * The maximum group id assigned so far | ||
69 | */ | ||
70 | protected int maxGroupId; | ||
71 | |||
72 | /** | ||
73 | * The dependency graph of the communications in the RETE network | ||
74 | */ | ||
75 | protected final Graph<Node> dependencyGraph; | ||
76 | |||
77 | /** | ||
78 | * Incremental SCC information about the dependency graph | ||
79 | */ | ||
80 | protected final IncSCCAlg<Node> sccInformationProvider; | ||
81 | |||
82 | /** | ||
83 | * Precomputed node -> communication group map | ||
84 | */ | ||
85 | protected final Map<Node, CommunicationGroup> groupMap; | ||
86 | |||
87 | /** | ||
88 | * Priority queue of active communication groups | ||
89 | */ | ||
90 | protected final Queue<CommunicationGroup> groupQueue; | ||
91 | |||
92 | // groups should have a simple integer flag which represents its position in a priority queue | ||
93 | // priority queue only contains the ACTIVE groups | ||
94 | |||
95 | public CommunicationTracker() { | ||
96 | this.dependencyGraph = new Graph<Node>(); | ||
97 | this.sccInformationProvider = new IncSCCAlg<Node>(this.dependencyGraph); | ||
98 | this.groupQueue = new PriorityQueue<CommunicationGroup>(); | ||
99 | this.groupMap = new HashMap<Node, CommunicationGroup>(); | ||
100 | } | ||
101 | |||
102 | public Graph<Node> getDependencyGraph() { | ||
103 | return dependencyGraph; | ||
104 | } | ||
105 | |||
106 | public CommunicationGroup getGroup(final Node node) { | ||
107 | return this.groupMap.get(node); | ||
108 | } | ||
109 | |||
110 | private void precomputeGroups() { | ||
111 | groupMap.clear(); | ||
112 | |||
113 | // reconstruct group map from dependency graph | ||
114 | final Graph<Node> reducedGraph = sccInformationProvider.getReducedGraph(); | ||
115 | final List<Node> representatives = TopologicalSorting.compute(reducedGraph); | ||
116 | |||
117 | for (int i = 0; i < representatives.size(); i++) { // groups for SCC representatives | ||
118 | final Node representative = representatives.get(i); | ||
119 | createAndStoreGroup(representative, i); | ||
120 | } | ||
121 | |||
122 | minGroupId = 0; | ||
123 | maxGroupId = representatives.size() - 1; | ||
124 | |||
125 | for (final Node node : dependencyGraph.getAllNodes()) { // extend group map to the rest of nodes | ||
126 | final Node representative = sccInformationProvider.getRepresentative(node); | ||
127 | final CommunicationGroup group = groupMap.get(representative); | ||
128 | if (representative != node) { | ||
129 | addToGroup(node, group); | ||
130 | } | ||
131 | } | ||
132 | |||
133 | for (final Node node : dependencyGraph.getAllNodes()) { | ||
134 | // set fall-through flags of default mailboxes | ||
135 | precomputeFallThroughFlag(node); | ||
136 | // perform further tracker-specific post-processing | ||
137 | postProcessNode(node); | ||
138 | } | ||
139 | |||
140 | // reconstruct new queue contents based on new group map | ||
141 | if (!groupQueue.isEmpty()) { | ||
142 | final Set<CommunicationGroup> oldActiveGroups = new HashSet<CommunicationGroup>(groupQueue); | ||
143 | groupQueue.clear(); | ||
144 | reconstructQueueContents(oldActiveGroups); | ||
145 | } | ||
146 | |||
147 | // post process the groups | ||
148 | for (final CommunicationGroup group : groupMap.values()) { | ||
149 | postProcessGroup(group); | ||
150 | } | ||
151 | } | ||
152 | |||
153 | /** | ||
154 | * This method is responsible for reconstructing the active queue contents after the network structure has changed. | ||
155 | * It it defined as abstract because the reconstruction logic is specific to each {@link CommunicationTracker}. | ||
156 | * @since 2.4 | ||
157 | */ | ||
158 | protected abstract void reconstructQueueContents(final Set<CommunicationGroup> oldActiveGroups); | ||
159 | |||
160 | private void addToGroup(final Node node, final CommunicationGroup group) { | ||
161 | groupMap.put(node, group); | ||
162 | if (node instanceof Receiver) { | ||
163 | ((Receiver) node).getMailbox().setCurrentGroup(group); | ||
164 | if (node instanceof IGroupable) { | ||
165 | ((IGroupable) node).setCurrentGroup(group); | ||
166 | } | ||
167 | } | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Depends on the groups, as well as the parent nodes of the argument, so recomputation is needed if these change | ||
172 | */ | ||
173 | private void precomputeFallThroughFlag(final Node node) { | ||
174 | CommunicationGroup group = groupMap.get(node); | ||
175 | if (node instanceof Receiver) { | ||
176 | IGroupable mailbox = ((Receiver) node).getMailbox(); | ||
177 | if (mailbox instanceof FallThroughCapableMailbox) { | ||
178 | Set<Node> directParents = dependencyGraph.getSourceNodes(node).distinctValues(); | ||
179 | // decide between using quick&cheap fall-through, or allowing for update cancellation | ||
180 | boolean fallThrough = | ||
181 | // disallow fallthrough: updates at production nodes should cancel, if they can be trimmed or | ||
182 | // disjunctive | ||
183 | (!(node instanceof ProductionNode && ( // it is a production node... | ||
184 | // with more than one parent | ||
185 | directParents.size() > 0 || | ||
186 | // or true trimming in its sole parent | ||
187 | directParents.size() == 1 && trueTrimming(directParents.iterator().next())))) && | ||
188 | // disallow fallthrough: external updates should be stored (if updates are delayed) | ||
189 | (!(node instanceof ExternalInputEnumeratorNode)) && | ||
190 | // disallow fallthrough: RelationEvaluatorNode needs to be notified in batch-style, and the batching is done by the mailbox | ||
191 | // however, it is not the RelationEvaluatorNode itself that is interesting here, as that indirectly uses the BatchingReceiver | ||
192 | // so we need to disable fall-through for the BatchingReceiver | ||
193 | (!(node instanceof RelationEvaluatorNode.BatchingReceiver)); | ||
194 | // do additional checks | ||
195 | if (fallThrough) { | ||
196 | // recursive parent groups generate excess updates that should be cancelled after delete&rederive | ||
197 | // phases | ||
198 | // aggregator and transitive closure parent nodes also generate excess updates that should be | ||
199 | // cancelled | ||
200 | directParentLoop: for (Node directParent : directParents) { | ||
201 | Set<Node> parentsToCheck = new HashSet<>(); | ||
202 | // check the case where a direct parent is the reason for mailbox usage | ||
203 | parentsToCheck.add(directParent); | ||
204 | // check the case where an indirect parent (join slot) is the reason for mailbox usage | ||
205 | if (directParent instanceof DualInputNode) { | ||
206 | // in case of existence join (typically antijoin), a mailbox should allow | ||
207 | // an insertion and deletion (at the secondary slot) to cancel each other out | ||
208 | if (directParent instanceof ExistenceNode) { | ||
209 | fallThrough = false; | ||
210 | break directParentLoop; | ||
211 | } | ||
212 | // in beta nodes, indexer slots (or their active nodes) are considered indirect parents | ||
213 | DualInputNode dualInput = (DualInputNode) directParent; | ||
214 | IterableIndexer primarySlot = dualInput.getPrimarySlot(); | ||
215 | if (primarySlot != null) | ||
216 | parentsToCheck.add(primarySlot.getActiveNode()); | ||
217 | Indexer secondarySlot = dualInput.getSecondarySlot(); | ||
218 | if (secondarySlot != null) | ||
219 | parentsToCheck.add(secondarySlot.getActiveNode()); | ||
220 | } | ||
221 | for (Node parent : parentsToCheck) { | ||
222 | CommunicationGroup parentGroup = groupMap.get(parent); | ||
223 | if ( // parent is in a different, recursive group | ||
224 | (group != parentGroup && parentGroup.isRecursive()) || | ||
225 | // node and parent within the same recursive group, and... | ||
226 | (group == parentGroup && group.isRecursive() && ( | ||
227 | // parent is a transitive closure or aggregator node, or a trimmer | ||
228 | // allow trimmed or disjunctive tuple updates to cancel each other | ||
229 | (parent instanceof TransitiveClosureNode) || (parent instanceof IAggregatorNode) | ||
230 | || trueTrimming(parent)))) { | ||
231 | fallThrough = false; | ||
232 | break directParentLoop; | ||
233 | } | ||
234 | } | ||
235 | } | ||
236 | } | ||
237 | // overwrite fallthrough flag with newly computed value | ||
238 | ((FallThroughCapableMailbox) mailbox).setFallThrough(fallThrough); | ||
239 | } | ||
240 | } | ||
241 | } | ||
242 | |||
243 | /** | ||
244 | * A trimmer node that actually eliminates some columns (not just reorders) | ||
245 | */ | ||
246 | private boolean trueTrimming(Node node) { | ||
247 | if (node instanceof TrimmerNode) { | ||
248 | TupleMask mask = ((TrimmerNode) node).getMask(); | ||
249 | return (mask.indices.length != mask.sourceWidth); | ||
250 | } | ||
251 | return false; | ||
252 | } | ||
253 | |||
254 | public void activateUnenqueued(final CommunicationGroup group) { | ||
255 | groupQueue.add(group); | ||
256 | group.isEnqueued = true; | ||
257 | } | ||
258 | |||
259 | public void deactivate(final CommunicationGroup group) { | ||
260 | groupQueue.remove(group); | ||
261 | group.isEnqueued = false; | ||
262 | } | ||
263 | |||
264 | public CommunicationGroup getAndRemoveFirstGroup() { | ||
265 | final CommunicationGroup group = groupQueue.poll(); | ||
266 | group.isEnqueued = false; | ||
267 | return group; | ||
268 | } | ||
269 | |||
270 | public boolean isEmpty() { | ||
271 | return groupQueue.isEmpty(); | ||
272 | } | ||
273 | |||
274 | protected abstract CommunicationGroup createGroup(final Node representative, final int index); | ||
275 | |||
276 | protected CommunicationGroup createAndStoreGroup(final Node representative, final int index) { | ||
277 | final CommunicationGroup group = createGroup(representative, index); | ||
278 | addToGroup(representative, group); | ||
279 | return group; | ||
280 | } | ||
281 | |||
282 | /** | ||
283 | * Registers the dependency that the target {@link Node} depends on the source {@link Node}. In other words, source | ||
284 | * may send messages to target in the RETE network. If the dependency edge is already present, this method call is a | ||
285 | * noop. | ||
286 | * | ||
287 | * @param source | ||
288 | * the source node | ||
289 | * @param target | ||
290 | * the target node | ||
291 | */ | ||
292 | public void registerDependency(final Node source, final Node target) { | ||
293 | // nodes can be immediately inserted, if they already exist in the graph, this is a noop | ||
294 | dependencyGraph.insertNode(source); | ||
295 | dependencyGraph.insertNode(target); | ||
296 | |||
297 | if (!this.dependencyGraph.getTargetNodes(source).containsNonZero(target)) { | ||
298 | |||
299 | // query all these information before the actual edge insertion | ||
300 | // because SCCs may be unified during the process | ||
301 | final Node sourceRepresentative = sccInformationProvider.getRepresentative(source); | ||
302 | final Node targetRepresentative = sccInformationProvider.getRepresentative(target); | ||
303 | final boolean targetHadOutgoingEdges = sccInformationProvider.hasOutgoingEdges(targetRepresentative); | ||
304 | |||
305 | // insert the edge | ||
306 | dependencyGraph.insertEdge(source, target); | ||
307 | |||
308 | // create groups if they do not yet exist | ||
309 | CommunicationGroup sourceGroup = groupMap.get(sourceRepresentative); | ||
310 | if (sourceGroup == null) { | ||
311 | // create on-demand with the next smaller group id | ||
312 | sourceGroup = createAndStoreGroup(sourceRepresentative, --minGroupId); | ||
313 | } | ||
314 | final int sourceIndex = sourceGroup.identifier; | ||
315 | |||
316 | CommunicationGroup targetGroup = groupMap.get(targetRepresentative); | ||
317 | if (targetGroup == null) { | ||
318 | // create on-demand with the next larger group id | ||
319 | targetGroup = createAndStoreGroup(targetRepresentative, ++maxGroupId); | ||
320 | } | ||
321 | final int targetIndex = targetGroup.identifier; | ||
322 | |||
323 | if (sourceIndex <= targetIndex) { | ||
324 | // indices obey current topological ordering | ||
325 | refreshFallThroughFlag(target); | ||
326 | postProcessNode(source); | ||
327 | postProcessNode(target); | ||
328 | postProcessGroup(sourceGroup); | ||
329 | if (sourceGroup != targetGroup) { | ||
330 | postProcessGroup(targetGroup); | ||
331 | } | ||
332 | } else if (sourceIndex > targetIndex && !targetHadOutgoingEdges) { | ||
333 | // indices violate current topological ordering, but we can simply bump the target index | ||
334 | final boolean wasEnqueued = targetGroup.isEnqueued; | ||
335 | if (wasEnqueued) { | ||
336 | groupQueue.remove(targetGroup); | ||
337 | } | ||
338 | targetGroup.identifier = ++maxGroupId; | ||
339 | if (wasEnqueued) { | ||
340 | groupQueue.add(targetGroup); | ||
341 | } | ||
342 | |||
343 | refreshFallThroughFlag(target); | ||
344 | postProcessNode(source); | ||
345 | postProcessNode(target); | ||
346 | postProcessGroup(sourceGroup); | ||
347 | postProcessGroup(targetGroup); | ||
348 | } else { | ||
349 | // needs a full re-computation because of more complex change | ||
350 | precomputeGroups(); | ||
351 | } | ||
352 | } | ||
353 | } | ||
354 | |||
355 | /** | ||
356 | * Returns true if the given {@link Node} is in a recursive {@link CommunicationGroup}, false otherwise. | ||
357 | */ | ||
358 | public boolean isInRecursiveGroup(final Node node) { | ||
359 | final CommunicationGroup group = this.getGroup(node); | ||
360 | if (group == null) { | ||
361 | return false; | ||
362 | } else { | ||
363 | return group.isRecursive(); | ||
364 | } | ||
365 | } | ||
366 | |||
367 | /** | ||
368 | * Returns true if the given two {@link Node}s are in the same {@link CommunicationGroup}. | ||
369 | */ | ||
370 | public boolean areInSameGroup(final Node left, final Node right) { | ||
371 | final CommunicationGroup leftGroup = this.getGroup(left); | ||
372 | final CommunicationGroup rightGroup = this.getGroup(right); | ||
373 | return leftGroup != null && leftGroup == rightGroup; | ||
374 | } | ||
375 | |||
376 | /** | ||
377 | * Unregisters a dependency between source and target. | ||
378 | * | ||
379 | * @param source | ||
380 | * the source node | ||
381 | * @param target | ||
382 | * the target node | ||
383 | */ | ||
384 | public void unregisterDependency(final Node source, final Node target) { | ||
385 | // delete the edge first, and then query the SCC info provider | ||
386 | this.dependencyGraph.deleteEdgeIfExists(source, target); | ||
387 | |||
388 | final Node sourceRepresentative = sccInformationProvider.getRepresentative(source); | ||
389 | final Node targetRepresentative = sccInformationProvider.getRepresentative(target); | ||
390 | |||
391 | // if they are still in the same SCC, | ||
392 | // then this deletion did not affect the SCCs, | ||
393 | // and it is sufficient to recompute affected fall-through flags; | ||
394 | // otherwise, we need a new pre-computation for the groupMap and groupQueue | ||
395 | if (sourceRepresentative.equals(targetRepresentative)) { | ||
396 | // this deletion could not have affected the split flags | ||
397 | refreshFallThroughFlag(target); | ||
398 | postProcessNode(source); | ||
399 | postProcessNode(target); | ||
400 | } else { | ||
401 | // preComputeGroups takes care of the split flag maintenance | ||
402 | precomputeGroups(); | ||
403 | } | ||
404 | } | ||
405 | |||
406 | /** | ||
407 | * Refresh fall-through flags if dependencies change for given target, but no SCC change | ||
408 | */ | ||
409 | private void refreshFallThroughFlag(final Node target) { | ||
410 | precomputeFallThroughFlag(target); | ||
411 | if (target instanceof DualInputNode) { | ||
412 | for (final Node indirectTarget : dependencyGraph.getTargetNodes(target).distinctValues()) { | ||
413 | precomputeFallThroughFlag(indirectTarget); | ||
414 | } | ||
415 | } | ||
416 | } | ||
417 | |||
418 | /** | ||
419 | * Returns true if the given source-target edge in the communication network acts as a recursion cut point. | ||
420 | * The current implementation considers edges leading into {@link ProductionNode}s as cut point iff | ||
421 | * both source and target belong to the same group. | ||
422 | * | ||
423 | * @param source the source node | ||
424 | * @param target the target node | ||
425 | * @return true if the edge is a cut point, false otherwise | ||
426 | * @since 2.4 | ||
427 | */ | ||
428 | protected boolean isRecursionCutPoint(final Node source, final Node target) { | ||
429 | final Node effectiveSource = source instanceof SpecializedProjectionIndexer | ||
430 | ? ((SpecializedProjectionIndexer) source).getActiveNode() | ||
431 | : source; | ||
432 | final CommunicationGroup sourceGroup = this.getGroup(effectiveSource); | ||
433 | final CommunicationGroup targetGroup = this.getGroup(target); | ||
434 | return sourceGroup != null && sourceGroup == targetGroup && target instanceof ProductionNode; | ||
435 | } | ||
436 | |||
437 | /** | ||
438 | * This hook allows concrete tracker implementations to perform tracker-specific post processing on nodes (cf. | ||
439 | * {@link NetworkStructureChangeSensitiveNode} and {@link BehaviorChangingMailbox}). At the time of the invocation, | ||
440 | * the network topology has already been updated. | ||
441 | */ | ||
442 | protected abstract void postProcessNode(final Node node); | ||
443 | |||
444 | /** | ||
445 | * This hook allows concrete tracker implementations to perform tracker-specific post processing on groups. At the | ||
446 | * time of the invocation, the network topology has already been updated. | ||
447 | * @since 2.4 | ||
448 | */ | ||
449 | protected abstract void postProcessGroup(final CommunicationGroup group); | ||
450 | |||
451 | /** | ||
452 | * Creates a proxy for the given {@link Mailbox} for the given requester {@link Node}. The proxy creation is | ||
453 | * {@link CommunicationTracker}-specific and depends on the identity of the requester. This method is primarily used | ||
454 | * to create {@link TimelyMailboxProxy}s depending on the network topology. There is no guarantee that the same | ||
455 | * proxy instance is returned when this method is called multiple times with the same arguments. | ||
456 | */ | ||
457 | public abstract Mailbox proxifyMailbox(final Node requester, final Mailbox original); | ||
458 | |||
459 | /** | ||
460 | * Creates a proxy for the given {@link IndexerListener} for the given requester {@link Node}. The proxy creation is | ||
461 | * {@link CommunicationTracker}-specific and depends on the identity of the requester. This method is primarily used | ||
462 | * to create {@link TimelyIndexerListenerProxy}s depending on the network topology. There is no guarantee that the | ||
463 | * same proxy instance is returned when this method is called multiple times with the same arguments. | ||
464 | */ | ||
465 | public abstract IndexerListener proxifyIndexerListener(final Node requester, final IndexerListener original); | ||
466 | |||
467 | } | ||