Skip to content

[GSoC 2026] Kafka Streams runner: multi-output executable stages#39410

Merged
je-ik merged 2 commits into
apache:feat/18479-kafka-streams-runner-skeletonfrom
junaiddshaukat:feat/ks-multi-output-stages
Jul 22, 2026
Merged

[GSoC 2026] Kafka Streams runner: multi-output executable stages#39410
je-ik merged 2 commits into
apache:feat/18479-kafka-streams-runner-skeletonfrom
junaiddshaukat:feat/ks-multi-output-stages

Conversation

@junaiddshaukat

@junaiddshaukat junaiddshaukat commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

A fused executable stage can have more than one output — a DoFn with side outputs, or a Read whose SDF wrapper produces several. The runner rejected multi-output stages; this routes each output to its own downstream.

How it works:

  • Each output PCollection of a multi-output stage gets a relay node
    (StageOutputProcessor) that stands in as that output's producer. Downstream
    transforms are wired to a producer node by PCollection id, and that node must
    exist when the stage is translated (before its consumers are), so the relay
    gives each output a stable producer to wire to.
  • The stage tags each harness output with its PCollection id and forwards it to
    the matching relay with ctx.forward(record, childName) — in-process routing
    to a named processor node, no extra topic. The relay forwards data unchanged
    and re-stamps the stage's watermark with its own id so a downstream watermark
    aggregator sees a consistent source.
  • A single-output stage is unchanged: it forwards to its one downstream directly
    and registers itself as that output's producer.

An output that feeds a GroupByKey is stored in that GroupByKey's own repartition topic (the shuffle creates it for the key change); an output feeding a stateless ParDo flows in-process. Discussed the storage with @je-ik on Slack.

Result on Beam's suite: GroupByKeyTest basic tests now pass (their PAssert path produces multi-output stages), and the multi-consumer FlattenTest is un-sickbayed (14/14). CreateTest stays 5/5. Adds MultiOutputStageTest: a DoFn with a side output whose two outputs each feed a separate GroupByKey.

The remaining GroupByKeyTest failures are non-global windowing (a GlobalWindow cast) — sickbayed until windowing lands.

A fused executable stage can have more than one output (a DoFn with side
outputs, or a Read whose SDF wrapper produces several). The runner previously
rejected these; it now routes each output to its own downstream.

Each output PCollection of a multi-output stage gets a relay node
(StageOutputProcessor) that stands in as that output's producer, so downstream
transforms have a producer node to wire to at translation time. The stage tags
each harness output with its PCollection id and forwards it to the matching
relay by name (in-process routing, no topic); the relay forwards data through
and re-stamps the stage's watermark with its own id so downstream watermark
aggregation stays consistent. A single-output stage is unchanged -- it forwards
to its one downstream directly.

Enables GroupByKeyTest (its PAssert path produces multi-output stages) and
un-sickbays the multi-consumer Flatten test. Adds MultiOutputStageTest, a DoFn
with a side output whose two outputs each feed a separate GroupByKey. The
remaining GroupByKey failures are non-global windowing (sickbayed).
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enables the Kafka Streams runner to handle fused executable stages that produce multiple outputs. Previously, the runner rejected such stages, limiting its ability to support complex pipeline patterns like side outputs. The implementation introduces a relay-based routing mechanism where each output PCollection is assigned a dedicated relay node, ensuring correct downstream wiring and consistent watermark management.

Highlights

  • Multi-output Stage Support: Enabled support for fused executable stages with multiple outputs (e.g., DoFns with side outputs) by introducing a relay mechanism that routes outputs to their respective downstream consumers.
  • StageOutputProcessor Implementation: Added a new StageOutputProcessor relay node that ensures each output PCollection has a stable producer, allowing for consistent watermark propagation and routing.
  • Test Coverage: Added MultiOutputStageTest to verify multi-output routing and updated the test suite to include GroupByKeyTest, while sickbaying specific non-global windowing tests.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for multi-output executable stages in the Kafka Streams runner by introducing a StageOutputProcessor relay node and updating ExecutableStageProcessor and ExecutableStageTranslator to route outputs to their respective downstream nodes. Feedback on the changes suggests using record.withValue(...) instead of manually reconstructing Record objects in both ExecutableStageProcessor and StageOutputProcessor to ensure metadata headers are preserved and to make the code more concise.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +276 to +278
Record<byte[], KStreamsPayload<?>> outputRecord =
new Record<byte[], KStreamsPayload<?>>(
record.key(), KStreamsPayload.data(output), record.timestamp()));
record.key(), KStreamsPayload.data(output.value), record.timestamp());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using record.withValue(...) is more concise and idiomatic than manually reconstructing the Record object. More importantly, it automatically preserves any metadata headers present on the original input record, which is crucial for features like distributed tracing (e.g., OpenTelemetry/Jaeger) or custom interceptors.

      Record<byte[], KStreamsPayload<?>> outputRecord =
          record.withValue(KStreamsPayload.data(output.value));

Comment on lines +82 to +86
ctx.forward(
new Record<byte[], KStreamsPayload<?>>(
record.key(),
KStreamsPayload.watermark(report.getWatermarkMillis(), transformId, 0, 1),
record.timestamp()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using record.withValue(...) is more concise and idiomatic than manually reconstructing the Record object. It also ensures that any metadata headers present on the original record are preserved when forwarding the watermark payload.

    ctx.forward(
        record.withValue(
            KStreamsPayload.watermark(report.getWatermarkMillis(), transformId, 0, 1)));

@github-actions

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @jrmccluskey added as fallback since no labels match configuration

Note: If you would like to opt out of this review, comment assign to next reviewer.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

The PR bot will only process comments in the main thread (not review comments).

@je-ik je-ik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we correctly propagate the watermark. Maybe we could add a test that will verify it? We would need multiple instances of the multi-output transform running in parallel.

… relay

StageOutputProcessor re-stamped the watermark as partition 0 of 1, which drops
the reporting stage instance's partition and count. That is fine while the stage
runs as a single instance, but once it runs in parallel a downstream aggregator
could no longer infer how many instances produced the output. The relay now
relabels only the transform id and keeps the incoming source_partition and
total_partitions.

Adds StageOutputProcessorTest, which feeds watermark reports from several stage
partitions through a MockProcessorContext (a single-instance TopologyTestDriver
cannot) and asserts they stay distinct downstream. Also simplifies
MultiOutputStageTest to assert one branch directly and route only the other
through a GroupByKey.
@je-ik
je-ik merged commit fca1435 into apache:feat/18479-kafka-streams-runner-skeleton Jul 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants