java-topology/defects/kafka/patch/kafka-0009-streams-graphgraceutil-diamond-recursion-memoize.patch
russell@unturf.com 1dee074618 kafka-0009: GraphGraceSearchUtil diamond recursion O(2^D) → O(N); count 621→622
findAndVerifyWindowGrace() recurses over parent GraphNodes without a visited
accumulator. Kafka Streams GraphNode is a genuine DAG (addChild wires
parent→child with multiple parents allowed), so a diamond topology causes
2^D recursive calls. Fix: thread an IdentityHashMap<GraphNode,Long> memo
through recursion; memoize on first visit, return cached result on revisit.
8/8 unit tests PASS; D=10 defect count=3071 vs patched O(N).

Diamond-recursion CLEAN markers added for: flink, neo4j, janusgraph,
tinkerpop, dgraph, zookeeper, storm, ant, gradle, graal, eclipse-jdt,
exposed, intellij, kotlin, scala3, hibernate-0007 (prior session work
now committed).
2026-03-29 16:59:50 -04:00

64 lines
2.9 KiB
Diff

# UNDF: UNDF-2026-000000451
--- a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/graph/GraphGraceSearchUtil.java
+++ b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/graph/GraphGraceSearchUtil.java
@@ -17,6 +17,8 @@
package org.apache.kafka.streams.kstream.internals.graph;
import org.apache.kafka.streams.errors.TopologyException;
+import java.util.IdentityHashMap;
+import java.util.Map;
public final class GraphGraceSearchUtil {
private GraphGraceSearchUtil() {}
public static long findAndVerifyWindowGrace(final GraphNode graphNode) {
- return findAndVerifyWindowGrace(graphNode, "");
+ return findAndVerifyWindowGrace(graphNode, "", new IdentityHashMap<>());
}
@SuppressWarnings("rawtypes")
- private static long findAndVerifyWindowGrace(final GraphNode graphNode, final String chain) {
+ private static long findAndVerifyWindowGrace(final GraphNode graphNode,
+ final String chain,
+ final Map<GraphNode, Long> memo) {
// error base case: we traversed off the end of the graph without finding a window definition
if (graphNode == null) {
throw new TopologyException(
"Window close time is only defined for windowed computations. Got [" + chain + "]."
);
}
// base case: return if this node defines a grace period.
if (graphNode instanceof GracePeriodGraphNode) {
return ((GracePeriodGraphNode) graphNode).gracePeriod();
}
+ // memoization: if we have already computed the grace for this node, return cached result
+ if (memo.containsKey(graphNode)) {
+ return memo.get(graphNode);
+ }
final String newChain = chain.equals("") ? graphNode.nodeName() : graphNode.nodeName() + "->" + chain;
if (graphNode.parentNodes().isEmpty()) {
// error base case: we traversed to the end of the graph without finding a window definition
throw new TopologyException(
"Window close time is only defined for windowed computations. Got [" + newChain + "]."
);
}
// recursive case: all parents must define a grace period, and we use the max of our parents' graces.
long inheritedGrace = -1;
for (final GraphNode parentNode : graphNode.parentNodes()) {
- final long parentGrace = findAndVerifyWindowGrace(parentNode, newChain);
+ final long parentGrace = findAndVerifyWindowGrace(parentNode, newChain, memo);
inheritedGrace = Math.max(inheritedGrace, parentGrace);
}
if (inheritedGrace == -1) {
throw new IllegalStateException(); // shouldn't happen, and it's not a legal grace period
}
+ memo.put(graphNode, inheritedGrace);
return inheritedGrace;
}
}