[GSoC 2026] Kafka Streams runner: CombineTest coverage and two review follow-ups - #39610
Conversation
… follow-ups Enables CombineTest in the ValidatesRunner suite, taking it from 49 to 59 tests. Combine was expected to work without a translator of its own, since the fuser expands Combine.perKey into a GroupByKey with the combining logic running as ordinary ParDos in the SDK harness, but nothing exercised that. BasicTests passes in full, including hot-key fanout and the accumulation-mode variant, and WindowingTests contributes the fixed-window and empty-window cases. The remainder falls out on category excludes the task already declares. testSessionsCombine is sickbayed alongside the existing merging windows entry, and it is the only Combine failure. Corrects the Flatten partition-count comment, which asserted that the inputs are co-partitioned and so implied the Math.max over them was redundant. Neither half held. The max is not a no-op in principle: Kafka Streams merges the subtopologies of every parent a processor is wired to and gives the result as many tasks as its largest source topic has partitions. But the mismatched shape does not reach this translator, because the fuser folds such a Flatten into the harness stage, and the runner Flattens that do arrive come from the fuser deduplicating partial outputs of one PCollection. FlattenParallelismTest records that, so a change letting the mismatched shape through starts failing there rather than producing a pipeline that stalls waiting for a watermark report that never comes. Guards the null record key in GroupByKeyBroadcastPartitioner.partitions(). partition() already guarded it, but partitions() is the method Kafka Streams calls and it hashed the key unguarded.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
je-ik
left a comment
There was a problem hiding this comment.
Good to see the combine tests pass, I have left a few questions.
| KafkaStreamsTranslationContext context = | ||
| KafkaStreamsTestRunner.translate(mixedParallelismFlatten(4)); | ||
|
|
||
| assertThat(context.getTopology().describe().subtopologies().isEmpty(), is(false)); |
There was a problem hiding this comment.
Should we test how the topology looks-like, not only that it is non-empty?
There was a problem hiding this comment.
Agreed, isEmpty() == false would have passed no matter what the fuser did, so it wasn't testing the thing the file exists for. Both now assert the shape: that no processor node stands for the Flatten, and that the branches stay in three separate subtopologies instead of being merged into one. That is the actual claim, since a Flatten node over branches of differing parallelism is exactly the case where the smaller branch can't reach all instances and the rest stall.
The second test asserts the same shape deliberately, because whether the Flatten is fused is a property of the fused graph rather than of the parallelism — which is what makes raising the parallelism safe. I printed the translated topology to write these, and there's no Flatten node in either case; both branches just end.
| KafkaStreamsTranslationContext context = | ||
| KafkaStreamsTestRunner.translate(mixedParallelismFlatten(1)); | ||
|
|
||
| assertThat(context.getTopology().describe().subtopologies().isEmpty(), is(false)); |
A null record key was being sent to partition 0, which is a fixed partition rather than no choice at all: keyless records would have piled onto one partition of the topic. Returning an empty Optional instead tells Kafka Streams that no explicit partition was chosen, and the producer's default partitioner spreads them, which is what a record with no key should get. The Flatten tests asserted only that the topology was non-empty, which would have passed whatever the fuser did with the Flatten. They now assert the property they exist for: that no processor node stands for the Flatten, and that the branches remain in three separate subtopologies rather than being merged into one. Were a Flatten node to appear over branches of differing parallelism, the branch with fewer partitions could not reach all of its instances and the rest would wait forever for a watermark report. Both the mismatched and the single-parallelism case assert the same shape, since whether the Flatten is fused is a property of the fused graph and not of the parallelism.
fc36301
into
apache:feat/18479-kafka-streams-runner-skeleton
Summary
Part of #18479. Enables
CombineTestin the ValidatesRunner suite, and picks up two review follow-ups from #39546 and #39578.Combine coverage
Combine has always been expected to work here without a translator of its own: the fuser expands
Combine.perKeyinto a GroupByKey with the combining logic running as ordinary ParDos in the SDK harness, all of which the runner already executes. That was an assumption though — nothing exercised it. EnablingCombineTesttakes the suite from 49 to 59 tests and makes it a tested claim.CombineTest$BasicTestsCombineTest$WindowingTestsBasicTestspasses in full, including hot-key fanout and the accumulation-mode variant;WindowingTestscontributes the fixed-window and empty-window cases. The rest falls out on category excludes the task already declares —CombineWithContextTestsandAccumulationTestsneed side inputs, and most ofWindowingTestsneeds side inputs, triggers orTestStream.CombineTest$WindowingTests.testSessionsCombineis sickbayed. Session windows are merging windows, which the first windowing pass did not implement, so it joins the existingtestGroupByKeyMergingWindowsentry under a comment now worded to cover Combine too. That is the only Combine failure, and it is the known windowing gap rather than anything specific to Combine.Flatten: what the
Math.maxis actually doingThe review question on #39546 was whether the
Math.maxover the inputs' partition counts is redundant, since the comment above it asserted the inputs are co-partitioned.Neither half of that was quite right, so both are now fixed. The max is not a no-op in principle: Kafka Streams merges the subtopologies of every parent a processor is wired to and gives the merged subtopology as many tasks as its largest source topic has partitions, so the max is what that task count comes to. But the mismatched case does not reach this translator at all. A Flatten whose branches would disagree — one through a GroupByKey, one straight from a source — is folded into the SDK harness stage by the fuser rather than becoming a node here, and the runner Flattens that do arrive come from the fuser deduplicating partial outputs of a single PCollection.
I tried to build the mismatched shape to see what the runner does with it, and could not: it never becomes a runner Flatten.
FlattenParallelismTestrecords that, so if a change ever lets that shape through, it starts failing and the partition-count handling gets revisited. The comment now describes the situation instead of asserting an invariant nothing enforces.Worth stating why this is not just tidying. If such a Flatten ever did reach the translator, the branch with fewer partitions would only produce on that many of the merged subtopology's tasks, and the remaining Flatten instances would wait forever for a watermark report from it. A stalled pipeline is a bad failure mode to leave undetected, which is what the test is guarding.
Partitioner: guard the null key
GroupByKeyBroadcastPartitioner.partition()guarded against a null record key, butpartitions()— the method Kafka Streams actually calls when it is present — hashed it unguarded. Nothing reaches that today, because data arriving at a repartition sink has been re-keyed byShuffleByKeyProcessorfirst, but the guard belongs on the method that runs.That is the groundwork for the other half of the review point: that a stateless stage should carry a null key rather than the empty-array placeholder Impulse and Read emit. That change is more invasive than it looks — every processor is declared
Processor<byte[], …, byte[], …>, so emitting a null key means moving all of them, plus the payload serde and the partitioner generics, tobyte @Nullable [], or adding nullness suppressions in several places. It is worth doing, but as its own change rather than folded in here.Testing