[GSoC 2026] Kafka Streams runner: run on a real broker, correctly across partitions - #39546
Open
junaiddshaukat wants to merge 2 commits into
Open
Conversation
Adds what the runner needs to execute on an actual Kafka cluster, and an integration test that runs a pipeline through the production KafkaStreamsPipelineRunner against a Kafka container. Everything until now ran through TopologyTestDriver, which fakes the topics and never builds a KafkaStreams application, so the production path had not been executed. KafkaStreamsTopicManager creates the topics the runner names for itself - a bootstrap topic per Impulse and primitive Read, and a repartition topic per GroupByKey - with an AdminClient before the application starts. Kafka Streams treats them as user topics because they are declared with explicit names, so it does not create them and refuses to start when a source topic is missing; the broker's auto-creation is not a substitute, since it is often disabled and gives the topic the broker's default partition count. Only topics carrying one of the runner's prefixes are created, so a topic the user named is never created implicitly. Adds topicPartitions and topicReplicationFactor options. Fixes two bugs the test driver could not catch. Validation demanded jobEndpoint, which KafkaStreamsPipelineOptions inherits as required from PortablePipelineOptions but which is a client-side option: this code runs on the job server for an already-submitted pipeline, so it rejected every valid job. Only the options the runner reads are checked now, as Flink's equivalent PortablePipelineRunner does. Separately, the result object registers a KafkaStreams state listener in its constructor but was built after start(), and Kafka Streams only accepts a listener in the CREATED state, so every real startup threw and the listener behind waitUntilFinish and cancel was never installed; the result is now built before the application starts. The integration test needs Docker, so the default test task excludes it and a brokerIntegrationTest task runs it. Also adds PortableWindowingStrategyTest, checking that the windowing the runner reconstructs comes from the standard beam:window_fn URNs every SDK emits rather than anything Java-specific.
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Contributor
|
Assigning reviewers: R: @chamikaramj added as fallback since no labels match configuration Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
Every transform stamped its watermark reports as partition 0 of 1. That is right for a report handed to a fused child, which runs in the same task and sees exactly one instance of its parent, but wrong for a report crossing a repartition topic: the sink's partitioner broadcasts each report to every partition, so a downstream task sees one from every instance of the upstream transform. With all of them claiming to be the only partition it would treat the first report as if every instance had reported, advance its watermark, and fire before the other partitions had delivered their data. Identity is now attached where the report stops being delivered in process and starts crossing a topic: ShuffleByKeyProcessor restamps it. That processor runs in the upstream transform's own task, so taskId().partition() is exactly the instance the report is for, and the transform id is left alone so a downstream aggregator still matches the report to the upstream it expects. Transforms go on reporting a single source when forwarding in process. The partition count a transform runs at is threaded through translation: one everywhere, raised only by a GroupByKey's shuffle, and inherited by everything fused downstream of it. Also pins the bootstrap topics to a single partition. An Impulse or a Read emits once per task, gated by a state store that is itself per task, so a multi-partition bootstrap topic would fire the same Impulse once per partition; only the repartition topic of a GroupByKey carries the pipeline's parallelism. Adds a broker test running two chained GroupByKeys across four partitions, which is the shape this affects — the second GroupByKey aggregates the reports of every task of the first, and a single-GroupByKey pipeline passes either way — plus a one-partition control and unit tests for the restamping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part of #18479.
Makes the runner able to run on a real Kafka cluster, and makes it correct when a pipeline is spread across more than one partition. Until now everything ran through
TopologyTestDriver, which fakes the topics and never builds aKafkaStreamsapplication, so the production path had never actually been executed and the distributed path had never been exercised at all. Running both found four bugs.Creating the runner's topics
The runner shuffles through topics it names itself — a bootstrap topic per Impulse and per primitive Read, and a repartition topic per GroupByKey. Kafka Streams creates the internal topics it manages, but these are declared with explicit names through
addSourceandaddSink, so to Kafka Streams they are ordinary user topics: it does not create them, and refuses to start withMissingSourceTopicExceptionwhen a source topic is missing. The broker'sauto.create.topics.enableis not a substitute — it is off on many clusters, and a topic auto-created on first fetch gets the broker's default partition count instead of the pipeline's.KafkaStreamsTopicManagercreates them with anAdminClientbefore the application starts, and only creates topics carrying one of the runner's own prefixes, so a topic the user named is never created implicitly. Bootstrap topics are always created with a single partition: an Impulse or a Read emits once per task, gated by a state store that is itself per task, so a bootstrap topic with several partitions would fire the same Impulse once per partition. The repartition topic of a GroupByKey is what carries the pipeline's parallelism, and takes the newtopicPartitionsoption (topicReplicationFactorcomes with it).Watermark identity across a shuffle
Every transform stamped its watermark reports as partition 0 of 1. That is right for a report handed to a fused child — it runs in the same task and sees exactly one instance of its parent — but wrong for a report that crosses a repartition topic, because the sink's partitioner broadcasts each report to every partition. A downstream task therefore sees a report from every instance of the upstream transform, and with all of them claiming to be the only partition it would treat the first report as if every instance had already reported, advance its watermark, and fire before the other partitions had delivered their data.
So identity is attached where the report stops being delivered in process and starts crossing a topic:
ShuffleByKeyProcessorrestamps it. That processor runs in the upstream transform's own task, sotaskId().partition()is exactly the instance the report is for, and the transform id is left alone so a downstream aggregator still matches the report to the upstream it expects. Transforms go on reporting a single source when they forward in process.The partition count a transform runs at is threaded through translation: it is one everywhere, only a GroupByKey's shuffle raises it, and everything fused downstream of that inherits it.
Two bugs the test driver could not catch
Options validation rejected every valid job.
runvalidated the wholeKafkaStreamsPipelineOptionsinterface, which inherits@Required jobEndpointfromPortablePipelineOptions:jobEndpointis a client-side option — the address a client uses to reach the job server. This code runs on the job server, executing a pipeline that has already been submitted, so it does not apply, and Flink's equivalentPortablePipelineRunnerdoes not validate here either. Only the options the runner reads are checked now.The state listener was registered after the application had started.
The result object registers a
KafkaStreamsstate listener in its constructor but was built afterstart(), and Kafka Streams only accepts a listener while the application is inCREATED. This threw on every real startup, and that listener is what backswaitUntilFinish()andcancel(). The result is now built before the application starts, and the requirement is documented on its constructor.Testing
KafkaStreamsRunnerBrokerITruns pipelines through the productionKafkaStreamsPipelineRunneragainst a Kafka container, polling the pipeline's metrics for the expected output:ShuffleByKeyProcessorTestcovers the restamping directly: the instance identity attached, two instances staying distinct, an unpartitioned upstream still reporting a single source, and data records passing through untouched.It uses testcontainers rather than an embedded broker because
library.java.testcontainers_kafkais already a declared Beam dependency and is what KafkaIO's own tests use. It needs Docker, so the defaulttesttask excludes it and a separate task runs it:Also adds
PortableWindowingStrategyTest, which checks that the windowing the runner reconstructs comes from the language-neutralbeam:window_fn:*URNs every SDK emits rather than the Java-serialized form — a question raised on the windowing PR about pipelines submitted from another SDK.