-
Notifications
You must be signed in to change notification settings - Fork 4.6k
[GSoC 2026] Kafka Streams runner: Flatten support #39273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
je-ik
merged 7 commits into
apache:feat/18479-kafka-streams-runner-skeleton
from
junaiddshaukat:feat/ks-flatten
Jul 15, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a5f946f
[GSoC 2026] Kafka Streams runner: Flatten support
junaiddshaukat 1784261
Address review: fail fast when a PCollection feeds a Flatten more tha…
junaiddshaukat 445b236
Address review: number producers globally so a PCollection can feed t…
junaiddshaukat aa78709
Address review: carry the producing transform id in the watermark pay…
junaiddshaukat 769579b
Retrigger CI (Actions infra failure: Bad Gateway during action download)
junaiddshaukat e6be254
Address review: WatermarkAggregator test suite + drop-null-payload re…
junaiddshaukat e4d2a77
Address review: verify watermark propagation through Flatten
junaiddshaukat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
119 changes: 119 additions & 0 deletions
119
...ams/src/main/java/org/apache/beam/runners/kafka/streams/translation/FlattenProcessor.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.runners.kafka.streams.translation; | ||
|
|
||
| import java.util.Set; | ||
| import org.apache.beam.sdk.transforms.windowing.BoundedWindow; | ||
| import org.apache.kafka.streams.processor.api.Processor; | ||
| import org.apache.kafka.streams.processor.api.ProcessorContext; | ||
| import org.apache.kafka.streams.processor.api.Record; | ||
| import org.checkerframework.checker.nullness.qual.Nullable; | ||
| import org.joda.time.Instant; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Kafka Streams {@link Processor} implementing Beam's {@code Flatten} primitive ({@code | ||
| * beam:transform:flatten:v1}): the union of N input PCollections into one output PCollection. | ||
| * | ||
| * <p><b>Data</b> records are forwarded straight through unchanged — the merge of the N parents' | ||
| * data streams <em>is</em> the flatten. | ||
| * | ||
| * <p><b>Watermark</b> reports are where Flatten does real work, and it owns its output watermark | ||
| * the same way GroupByKey does: it runs a {@link WatermarkAggregator} over its inputs, forwards its | ||
| * own watermark only when the {@code min()} across them advances, and stamps that as a single | ||
| * source ({@code 0 of 1}) to its downstream. This holds the output watermark back until | ||
| * <em>every</em> input branch has reported, so a downstream GroupByKey does not fire before all | ||
| * flattened branches are drained. | ||
| * | ||
| * <p>The {@link WatermarkAggregator} tells the input branches apart by the transform id each | ||
| * branch's producer stamps on its watermark (Kafka Streams does not tell a processor which parent | ||
| * forwarded a record). Each producer stamps its own identity regardless of who consumes it, so a | ||
| * PCollection feeding several Flattens reports one identity and every Flatten still waits only for | ||
| * the upstream transforms it expects — the set handed to it at construction from the pipeline | ||
| * graph. | ||
| */ | ||
| class FlattenProcessor | ||
| implements Processor<byte[], KStreamsPayload<?>, byte[], KStreamsPayload<?>> { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(FlattenProcessor.class); | ||
|
|
||
| // This transform's own id, stamped on every watermark it forwards downstream. | ||
| private final String transformId; | ||
| // Computes the output watermark as min() over the upstream transforms' reports, holding until | ||
| // every partition of every expected upstream transform has reported (see WatermarkAggregator). | ||
| private final WatermarkAggregator watermarkAggregator; | ||
| // The last watermark actually forwarded downstream, so we only forward when it advances. | ||
| private Instant lastForwardedWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; | ||
|
|
||
| private @Nullable ProcessorContext<byte[], KStreamsPayload<?>> context; | ||
|
|
||
| /** | ||
| * @param transformId this Flatten's own transform id, stamped on the watermarks it emits | ||
| * @param upstreamTransformIds the producers of this Flatten's input PCollections (known from the | ||
| * pipeline graph), whose reports the {@link WatermarkAggregator} waits for | ||
| */ | ||
| FlattenProcessor(String transformId, Set<String> upstreamTransformIds) { | ||
| this.transformId = transformId; | ||
| this.watermarkAggregator = new WatermarkAggregator(upstreamTransformIds); | ||
| } | ||
|
|
||
| @Override | ||
| public void init(ProcessorContext<byte[], KStreamsPayload<?>> context) { | ||
| this.context = context; | ||
| } | ||
|
|
||
| @Override | ||
| public void process(Record<byte[], KStreamsPayload<?>> record) { | ||
| KStreamsPayload<?> payload = record.value(); | ||
| if (payload == null) { | ||
| // A topic feeding the runner can always be written to from outside (or carry a tombstone), | ||
| // so recover from the obvious error instead of crashing the task: warn and drop. | ||
| LOG.warn( | ||
| "Flatten {} dropping record with null payload (external write or tombstone)", | ||
| transformId); | ||
| return; | ||
| } | ||
| ProcessorContext<byte[], KStreamsPayload<?>> ctx = checkInitialized(context); | ||
| if (!payload.isWatermark()) { | ||
| // Data: the union of the parents' data streams is the flatten — forward unchanged. | ||
| ctx.forward(record); | ||
| return; | ||
| } | ||
| watermarkAggregator.observe(payload.asWatermark()); | ||
| Instant advanced = watermarkAggregator.advance(); | ||
| if (advanced.isAfter(lastForwardedWatermark)) { | ||
| lastForwardedWatermark = advanced; | ||
| // Stamped with this Flatten's own transform id; Flatten is a single instance for now, so the | ||
| // report is for its only partition (0 of 1). | ||
| ctx.forward( | ||
| new Record<byte[], KStreamsPayload<?>>( | ||
| record.key(), | ||
| KStreamsPayload.watermark(advanced.getMillis(), transformId, 0, 1), | ||
| record.timestamp())); | ||
| } | ||
|
je-ik marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private static ProcessorContext<byte[], KStreamsPayload<?>> checkInitialized( | ||
| @Nullable ProcessorContext<byte[], KStreamsPayload<?>> context) { | ||
| if (context == null) { | ||
| throw new IllegalStateException("FlattenProcessor used before init()"); | ||
| } | ||
| return context; | ||
| } | ||
| } | ||
82 changes: 82 additions & 0 deletions
82
...ms/src/main/java/org/apache/beam/runners/kafka/streams/translation/FlattenTranslator.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.runners.kafka.streams.translation; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import org.apache.beam.model.pipeline.v1.RunnerApi; | ||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; | ||
| import org.apache.kafka.streams.Topology; | ||
|
|
||
| /** | ||
| * Translates Beam's {@code Flatten} primitive ({@code beam:transform:flatten:v1}): the union of N | ||
| * input PCollections into one output PCollection. | ||
| * | ||
| * <p>Wires a single {@link FlattenProcessor} node to the producer of every input PCollection (Kafka | ||
| * Streams lets a processor have many parents), so the parents' data streams merge into it, and | ||
| * registers it as the producer of the flattened output so downstream translators wire to it. The | ||
| * processor forwards data through and owns its output watermark via a {@link WatermarkAggregator}, | ||
| * which is handed the producers of the input PCollections — the upstream transform ids whose | ||
| * watermark reports the Flatten must hear from. Producers stamp their own transform id on the | ||
| * reports they emit, without regard to who consumes them, so an input shared with another Flatten | ||
| * needs no special handling. | ||
| * | ||
| * <p>A user-written self-flatten never reaches this translator: the fuser folds the Flatten into | ||
| * the consuming SDK-harness stage, which performs the duplication itself. The duplicate-input check | ||
| * below is defensive — if a runner-executed Flatten ever did receive the same PCollection twice, | ||
| * Kafka Streams could not wire the same parent to a child twice and the duplicate copy would be | ||
| * silently dropped, so failing fast is safer. | ||
| */ | ||
| class FlattenTranslator implements PTransformTranslator { | ||
|
|
||
| @Override | ||
| public void translate( | ||
| String transformId, RunnerApi.Pipeline pipeline, KafkaStreamsTranslationContext context) { | ||
| RunnerApi.PTransform transform = pipeline.getComponents().getTransformsOrThrow(transformId); | ||
| // Flatten produces exactly one output PCollection, fed by all of its input PCollections. | ||
| String outputPCollectionId = Iterables.getOnlyElement(transform.getOutputsMap().values()); | ||
|
|
||
| Set<String> seenInputs = new HashSet<>(); | ||
| List<String> parentProcessors = new ArrayList<>(); | ||
| Set<String> upstreamTransformIds = new HashSet<>(); | ||
| for (String inputPCollectionId : transform.getInputsMap().values()) { | ||
| if (!seenInputs.add(inputPCollectionId)) { | ||
| throw new UnsupportedOperationException( | ||
| "Flatten " | ||
| + transform.getUniqueName() | ||
| + " has PCollection " | ||
| + inputPCollectionId | ||
| + " as an input more than once; a self-flatten is not yet supported by the Kafka" | ||
| + " Streams runner."); | ||
| } | ||
| String parentProcessor = context.getProcessorNameForPCollection(inputPCollectionId); | ||
| parentProcessors.add(parentProcessor); | ||
| upstreamTransformIds.add(parentProcessor); | ||
| } | ||
|
junaiddshaukat marked this conversation as resolved.
|
||
|
|
||
| Topology topology = context.getTopology(); | ||
| topology.addProcessor( | ||
| transformId, | ||
| () -> new FlattenProcessor(transformId, upstreamTransformIds), | ||
| parentProcessors.toArray(new String[0])); | ||
|
|
||
| context.registerPCollectionProducer(outputPCollectionId, transformId); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.