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]

Reply via email to