je-ik commented on code in PR #39410: URL: https://github.com/apache/beam/pull/39410#discussion_r3628757179
########## runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/StageOutputProcessor.java: ########## @@ -0,0 +1,88 @@ +/* + * 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 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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * One output port of a multi-output {@link ExecutableStageProcessor}: a relay node that stands in + * as the producer of one of the stage's output PCollections. + * + * <p>A Kafka Streams processor forwards a record either to all of its children or to one named + * child, but downstream transforms are wired to a <em>producer node</em> by PCollection id, and + * that node's identity has to be known when the stage is translated — before the downstream + * transforms are. So each output PCollection of a multi-output stage gets its own relay: the stage + * routes each harness output to the matching relay by name, and downstream transforms wire to the + * relay. (A single-output stage needs none of this and forwards directly.) + * + * <p>The relay forwards data records unchanged, and re-stamps the watermark with its own transform + * id so a downstream watermark aggregator, which expects to hear from this relay, sees a consistent + * source. The stage is a single instance and its watermark is already the monotonic aggregate over + * its input, so the relay only relabels it — there is exactly one upstream partition to relay, no + * aggregation to do. + */ +class StageOutputProcessor + implements Processor<byte[], KStreamsPayload<?>, byte[], KStreamsPayload<?>> { + + private static final Logger LOG = LoggerFactory.getLogger(StageOutputProcessor.class); + + private final String transformId; + private @Nullable ProcessorContext<byte[], KStreamsPayload<?>> context; + + StageOutputProcessor(String transformId) { + this.transformId = transformId; + } + + @Override + public void init(ProcessorContext<byte[], KStreamsPayload<?>> context) { + this.context = context; + } + + @Override + public void process(Record<byte[], KStreamsPayload<?>> record) { + KStreamsPayload<?> payload = record.value(); + ProcessorContext<byte[], KStreamsPayload<?>> ctx = context; + if (ctx == null) { + throw new IllegalStateException("StageOutputProcessor used before init()"); + } + if (payload == null) { + LOG.warn( + "Stage output {} dropping record with null payload (external write or tombstone)", + transformId); + return; + } + if (!payload.isWatermark()) { + // Data for this output: forward unchanged. + ctx.forward(record); + return; + } + // Re-stamp the stage's watermark with this output port's own id. Single upstream instance, so + // its value is already monotonic; just relabel it as source 0 of 1. + WatermarkPayload report = payload.asWatermark(); + ctx.forward( + new Record<byte[], KStreamsPayload<?>>( + record.key(), + KStreamsPayload.watermark(report.getWatermarkMillis(), transformId, 0, 1), Review Comment: Is this actually correct? Is we don't do any aggregation, we should probably preserve the original range of input partitions (and numbers), as this is what actually matters to downstream processing to infer how many parallel instances of the (originally multioutput) transform there is? ########## runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/MultiOutputStageTest.java: ########## @@ -0,0 +1,85 @@ +/* + * 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; + +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Count; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; +import org.junit.Rule; +import org.junit.Test; + +/** + * Verifies that a fused executable stage with more than one output routes each output to its own + * downstream — including the case the mentor asked about, where each output is followed by a + * separate GroupByKey (so each output ends up in its own repartition topic). + * + * <p>A DoFn splits its input to a main and an additional (side) output; each output is grouped with + * {@code Count.perElement} (a GroupByKey under the hood), and a {@link PAssert} on each confirms + * the two outputs carried the right elements to the right GroupByKey. + */ +public class MultiOutputStageTest { + + private static final TupleTag<Integer> EVENS = new TupleTag<Integer>() {}; + private static final TupleTag<Integer> ODDS = new TupleTag<Integer>() {}; + + /** Routes even elements to the main output and odd elements to the side output. */ + private static class SplitByParityFn extends DoFn<Integer, Integer> { + @ProcessElement + public void processElement(@Element Integer input, MultiOutputReceiver out) { + if (input % 2 == 0) { + out.get(EVENS).output(input); + } else { + out.get(ODDS).output(input); + } + } + } + + private static PipelineOptions options() { + PipelineOptions options = PipelineOptionsFactory.create(); + options.setRunner(TestKafkaStreamsRunner.class); + return options; + } + + @Rule public final transient TestPipeline pipeline = TestPipeline.fromOptions(options()); + + @Test + public void eachStageOutputFeedsItsOwnGroupByKey() { + PCollectionTuple split = + pipeline + .apply("create", Create.of(1, 2, 3, 4, 5, 6)) + .apply( + "split", + ParDo.of(new SplitByParityFn()).withOutputTags(EVENS, TupleTagList.of(ODDS))); + + PAssert.that(split.get(EVENS).apply("countEvens", Count.perElement())) Review Comment: The Count seems a little redundant here, do we need the GBK after the data are emitted? If so, I would do one branch with Count and one without, e.g. `PAssert.that(split.get(ODDS).containsInAnyOrder(1, 2, 5))`; -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
