Nicholas Telford created KAFKA-21357:
----------------------------------------
Summary: Topology optimization fails with "Found a null
keyChangingChild node" on nested merges
Key: KAFKA-21357
URL: https://issues.apache.org/jira/browse/KAFKA-21357
Project: Kafka
Issue Type: Bug
Components: streams
Affects Versions: 4.3.0, 4.4.0
Reporter: Nicholas Telford
h3. Problem
When {{topology.optimization=all}} is set, {{StreamsBuilder.build()}} throws
this exception {*}for a topology with nested merges{*}:
{noformat}
org.apache.kafka.streams.errors.StreamsException: Found a null keyChangingChild
node for OptimizableRepartitionNode{ BaseRepartitionNode{...
repartitionTopic='grp-repartition', ...}
StreamsGraphNode{nodeName='grp-repartition-source', ..., parentNodes=[]} }
{noformat}
The failure occurs when all of these conditions are true:
# A key-changing operation feeds a merge (the inner merge).
# The inner merge feeds a second merge (the outer merge).
# An optimizable repartition (for example {{{}groupByKey().count(){}}})
follows the outer merge.
# No other key-changing operation sits between them.
The same topology builds without an error on 4.2.2 and earlier.
h3. Reproduction
{code:java}
StreamsBuilder b = new StreamsBuilder();
KStream<String, String> f1 = b.<String, String>stream("a").selectKey((k, v) ->
v);
KStream<String, String> f2 = b.<String, String>stream("b").selectKey((k, v) ->
v);
KStream<String, String> j = b.<String, String>stream("c").selectKey((k, v) ->
v);
f1.merge(f2).merge(j)
.groupByKey(Grouped.with("grp", Serdes.String(), Serdes.String()))
.count();
Properties p = new Properties();
p.put(StreamsConfig.APPLICATION_ID_CONFIG, "repro");
p.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
p.put(StreamsConfig.TOPOLOGY_OPTIMIZATION_CONFIG, StreamsConfig.OPTIMIZE);
b.build(p); // throws on 4.3.x and 4.4.0
{code}
||Version||Result||
|4.1.2|OK|
|4.2.2|OK|
|4.3.1|StreamsException|
|4.4.0|StreamsException|
A single merge with three inputs (a merge node with three parents) builds
without an error on 4.4.0.
h3. Cause
KAFKA-20264 changed {{{}InternalStreamsBuilder.findParentNodeMatching{}}}. The
method now returns the first match in a depth-first search. Before the change,
it returned the result of the last parent. The change is correct, but it
exposes a defect in {{{}maybeUpdateKeyChangingRepartitionNodeMap{}}}.
In the reproduction, M1 = {{{}f1.merge(f2){}}}, M2 = {{{}M1.merge(j){}}}, and R
is the repartition node below M2.
# {{getKeyChangingParentNode(R)}} searches up through M2, then M1, and returns
f1. Before KAFKA-20264, it returned j, the last parent of M2. R is now
registered under f1.
# {{maybeUpdateKeyChangingRepartitionNodeMap}} processes each merge node
separately. R is downstream of both M1 and M2, and f1 is upstream of both. So
both M1 and M2 take a copy of f1's set, and R goes into two sets.
# {{mergeRepartitionTopics}} processes the M1 set first. It replaces R and
removes R from its parents, so {{R.parentNodes()}} becomes empty.
# It then processes the M2 set, which also contains R.
{{findParentNodeMatching(R, ...)}} finds no parents and returns {{{}null{}}},
and the exception occurs.
Before KAFKA-20264, R was registered under j. M1 is not downstream of j, so
only M2 took the set, and R was processed one time.
h3. Suggested fix
Make sure that each {{OptimizableRepartitionNode}} goes into only one merge
node's set. Two possible ways:
* In {{{}maybeUpdateKeyChangingRepartitionNodeMap{}}}, give each key changer's
set only to the merge node nearest to the repartition nodes.
* In {{{}mergeRepartitionTopics{}}}, skip a repartition node that has already
been processed.
The tests that KAFKA-20264 added cover only a single merge. A test for nested
merges would catch this regression.
h3. Workaround
Do not nest merges above a repartition. Merge all inputs at one merge node. The
public API has only a two-input {{{}merge{}}}, so this requires a custom N-way
merge.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)