This is an automated email from the ASF dual-hosted git repository.

je-ik pushed a commit to branch feat/18479-kafka-streams-runner-skeleton
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to 
refs/heads/feat/18479-kafka-streams-runner-skeleton by this push:
     new 27c4522ae5e [GSoC 2026] Kafka Streams runner #39249: Support Create
27c4522ae5e is described below

commit 27c4522ae5e4de8aad086aa917be4ceae3b35b7b
Author: M Junaid Shaukat <[email protected]>
AuthorDate: Thu Jul 9 13:20:20 2026 +0500

    [GSoC 2026] Kafka Streams runner #39249: Support Create
---
 .../KafkaStreamsPipelineTranslator.java            |   1 +
 .../KafkaStreamsTranslationContext.java            |  20 ++
 .../kafka/streams/translation/ReadProcessor.java   | 207 +++++++++++++++++++++
 .../kafka/streams/translation/ReadTranslator.java  | 173 +++++++++++++++++
 .../kafka/streams/KafkaStreamsTestRunner.java      |   6 +
 .../kafka/streams/translation/CreateTest.java      |  73 ++++++++
 .../kafka/streams/translation/ReadTest.java        |  75 ++++++++
 7 files changed, 555 insertions(+)

diff --git 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsPipelineTranslator.java
 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsPipelineTranslator.java
index 189482ed5f9..943673dfe98 100644
--- 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsPipelineTranslator.java
+++ 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsPipelineTranslator.java
@@ -50,6 +50,7 @@ public class KafkaStreamsPipelineTranslator {
     this(
         ImmutableMap.<String, PTransformTranslator>builder()
             .put(PTransformTranslation.IMPULSE_TRANSFORM_URN, new 
ImpulseTranslator())
+            .put(PTransformTranslation.READ_TRANSFORM_URN, new 
ReadTranslator())
             .put(PTransformTranslation.REDISTRIBUTE_ARBITRARILY_URN, new 
RedistributeTranslator())
             .put(PTransformTranslation.GROUP_BY_KEY_TRANSFORM_URN, new 
GroupByKeyTranslator())
             .put(ExecutableStage.URN, new ExecutableStageTranslator())
diff --git 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsTranslationContext.java
 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsTranslationContext.java
index 3d95eabafed..0a045ff7395 100644
--- 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsTranslationContext.java
+++ 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KafkaStreamsTranslationContext.java
@@ -19,6 +19,7 @@ package org.apache.beam.runners.kafka.streams.translation;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.regex.Pattern;
 import org.apache.beam.runners.fnexecution.provisioning.JobInfo;
 import org.apache.beam.runners.kafka.streams.KafkaStreamsPipelineOptions;
 import org.apache.kafka.streams.Topology;
@@ -34,6 +35,12 @@ public class KafkaStreamsTranslationContext {
   /** Prefix for the per-job bootstrap topic Impulse reads from. */
   private static final String IMPULSE_BOOTSTRAP_TOPIC_PREFIX = 
"__beam_impulse_";
 
+  /** Prefix for the per-transform bootstrap topic a primitive Read reads 
from. */
+  private static final String READ_BOOTSTRAP_TOPIC_PREFIX = "__beam_read_";
+
+  /** Characters not legal in a Kafka topic name; a topic's legal set is 
{@code [a-zA-Z0-9._-]}. */
+  private static final Pattern ILLEGAL_TOPIC_CHARS = 
Pattern.compile("[^a-zA-Z0-9._-]");
+
   private final JobInfo jobInfo;
   private final KafkaStreamsPipelineOptions pipelineOptions;
   private final Topology topology;
@@ -100,4 +107,17 @@ public class KafkaStreamsTranslationContext {
   public String getImpulseBootstrapTopic() {
     return IMPULSE_BOOTSTRAP_TOPIC_PREFIX + pipelineOptions.getApplicationId();
   }
+
+  /**
+   * Returns the dedicated bootstrap topic name a primitive Read reads from. 
Keyed by transform id
+   * (sanitized to Kafka's legal topic-name character set) so multiple Reads — 
and Impulse — never
+   * register the same topic on two source nodes, which Kafka Streams rejects.
+   */
+  public String getReadBootstrapTopic(String transformId) {
+    String sanitizedTransformId = 
ILLEGAL_TOPIC_CHARS.matcher(transformId).replaceAll("_");
+    return READ_BOOTSTRAP_TOPIC_PREFIX
+        + pipelineOptions.getApplicationId()
+        + "_"
+        + sanitizedTransformId;
+  }
 }
diff --git 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadProcessor.java
 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadProcessor.java
new file mode 100644
index 00000000000..604eca54014
--- /dev/null
+++ 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadProcessor.java
@@ -0,0 +1,207 @@
+/*
+ * 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.io.IOException;
+import java.time.Duration;
+import org.apache.beam.runners.core.construction.SerializablePipelineOptions;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.CoderException;
+import org.apache.beam.sdk.io.BoundedSource;
+import org.apache.beam.sdk.io.BoundedSource.BoundedReader;
+import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
+import org.apache.beam.sdk.util.CoderUtils;
+import org.apache.beam.sdk.values.WindowedValue;
+import org.apache.beam.sdk.values.WindowedValues;
+import org.apache.kafka.streams.processor.Cancellable;
+import org.apache.kafka.streams.processor.PunctuationType;
+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.apache.kafka.streams.state.KeyValueStore;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Kafka Streams {@link Processor} implementing Beam's deprecated primitive 
{@code Read}
+ * (beam:transform:read:v1) over a {@link BoundedSource}.
+ *
+ * <p>For each task instance, reads the whole {@link BoundedSource} once and 
emits, in order:
+ *
+ * <ol>
+ *   <li>One {@link KStreamsPayload#data data} payload per source element, 
each wrapping a {@link
+ *       WindowedValue} in the {@link 
org.apache.beam.sdk.transforms.windowing.GlobalWindow} at the
+ *       element's own event time (from {@link 
BoundedReader#getCurrentTimestamp()}).
+ *   <li>A {@link KStreamsPayload#watermark watermark} payload at {@link
+ *       BoundedWindow#TIMESTAMP_MAX_VALUE} telling downstream transforms the 
source is done.
+ * </ol>
+ *
+ * <p><b>Wire form.</b> Unlike Impulse (whose element is already an opaque 
{@code byte[]}), a Read
+ * produces <em>decoded</em> Java objects. Downstream {@link 
ExecutableStageProcessor} feeds
+ * whatever it receives straight into the SDK harness, whose main-input 
receiver expects each
+ * element in the runner-side wire form — a raw object for a model coder, but 
a length-prefixed
+ * {@code byte[]} for a coder the runner does not know (e.g. {@code 
VarIntCoder}). Stage-to-stage
+ * edges already carry that wire form because harness outputs are decoded with 
the runner-side wire
+ * coder; this processor reproduces it for the source edge by transcoding each 
element through the
+ * SDK-side wire coder (encode) and back through the runner-side wire coder 
(decode). The two are
+ * byte-compatible by construction, so the transcode yields exactly the object 
the receiver expects,
+ * nesting and all.
+ *
+ * <p>This mirrors {@link ImpulseProcessor}: a persistent state store records 
whether the elements
+ * have already been emitted so task restarts do not duplicate them, while the 
terminal watermark is
+ * re-emitted on every restart so downstream watermark holds still release 
after recovery. The
+ * trigger is a wall-clock punctuator scheduled on {@link #init} so the 
processor fires even though
+ * its bootstrap source topic is empty.
+ *
+ * <p>The source is read in a single instance with no splitting — parallelism 
across the source's
+ * splits arrives with the topic-based shuffle work (#18479). Kafka Streams 
disallows negative
+ * record timestamps, so each forwarded {@link Record} carries the Unix epoch 
({@code 0L}); the Beam
+ * event time lives inside the {@link WindowedValue}.
+ */
+class ReadProcessor<T> implements Processor<byte[], byte[], byte[], 
KStreamsPayload<?>> {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ReadProcessor.class);
+
+  /** Sole entry in the state store; the value tracks whether this processor 
has already emitted. */
+  static final String FIRED_KEY = "fired";
+
+  /** How soon after {@link #init} the punctuator first fires. */
+  private static final Duration PUNCTUATION_DELAY = Duration.ofMillis(50);
+
+  private final BoundedSource<T> source;
+  // Held as SerializablePipelineOptions (Beam's idiom for a reader holding 
options) so the
+  // processor's captured state is uniformly serializable alongside the 
BoundedSource and coders.
+  private final SerializablePipelineOptions options;
+  // Encodes a raw WindowedValue<T> as the SDK harness would on the wire 
(length-prefixing the
+  // element coder if the runner does not know it); the runner-side coder then 
decodes it into the
+  // wire form the downstream stage's input receiver expects. See the class 
javadoc.
+  private final Coder<WindowedValue<T>> sdkWireCoder;
+  private final Coder<WindowedValue<?>> runnerWireCoder;
+  private final String stateStoreName;
+  private final String transformId;
+
+  private @Nullable ProcessorContext<byte[], KStreamsPayload<?>> context;
+  private @Nullable KeyValueStore<String, Boolean> firedStore;
+  private @Nullable Cancellable scheduledPunctuator;
+
+  ReadProcessor(
+      BoundedSource<T> source,
+      SerializablePipelineOptions options,
+      Coder<WindowedValue<T>> sdkWireCoder,
+      Coder<WindowedValue<?>> runnerWireCoder,
+      String stateStoreName,
+      String transformId) {
+    this.source = source;
+    this.options = options;
+    this.sdkWireCoder = sdkWireCoder;
+    this.runnerWireCoder = runnerWireCoder;
+    this.stateStoreName = stateStoreName;
+    this.transformId = transformId;
+  }
+
+  @Override
+  public void init(ProcessorContext<byte[], KStreamsPayload<?>> context) {
+    this.context = context;
+    this.firedStore = context.getStateStore(stateStoreName);
+    this.scheduledPunctuator =
+        context.schedule(PUNCTUATION_DELAY, PunctuationType.WALL_CLOCK_TIME, 
ts -> maybeFire());
+  }
+
+  @Override
+  public void process(Record<byte[], byte[]> record) {
+    // Records that happen to land on the bootstrap topic are not actual data; 
they just provide an
+    // extra opportunity to fire the read on restart. The state store still 
gates the emit.
+    maybeFire();
+  }
+
+  private void maybeFire() {
+    ProcessorContext<byte[], KStreamsPayload<?>> ctx = context;
+    KeyValueStore<String, Boolean> store = firedStore;
+    if (ctx == null || store == null) {
+      return;
+    }
+    if (Boolean.TRUE.equals(store.get(FIRED_KEY))) {
+      // Elements were already emitted in a previous task lifetime, but 
downstream watermark holds
+      // may still need to release after the restart — re-emit the terminal 
watermark and stop.
+      forwardWatermarkMax(ctx);
+      cancelPunctuator();
+      return;
+    }
+    int count = readAndForward(ctx);
+    forwardWatermarkMax(ctx);
+    store.put(FIRED_KEY, Boolean.TRUE);
+    cancelPunctuator();
+    LOG.debug("Read {} emitted {} elements and terminal watermark", 
transformId, count);
+  }
+
+  /** Reads the whole bounded source and forwards each element, in wire form, 
as a data payload. */
+  private int readAndForward(ProcessorContext<byte[], KStreamsPayload<?>> ctx) 
{
+    int count = 0;
+    try (BoundedReader<T> reader = source.createReader(options.get())) {
+      for (boolean hasElement = reader.start(); hasElement; hasElement = 
reader.advance()) {
+        WindowedValue<T> element =
+            WindowedValues.timestampedValueInGlobalWindow(
+                reader.getCurrent(), reader.getCurrentTimestamp());
+        // The Read output PCollection is not keyed; use an empty byte[] as a 
placeholder key so
+        // downstream processors that adopt the byte[]-key convention see a 
consistent shape.
+        ctx.forward(
+            new Record<byte[], KStreamsPayload<?>>(
+                new byte[0], KStreamsPayload.data(toRunnerWire(element)), 0L));
+        count++;
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to read bounded source for transform 
" + transformId, e);
+    }
+    return count;
+  }
+
+  /** Transcodes a raw element into the runner-side wire form the SDK harness 
input expects. */
+  private WindowedValue<?> toRunnerWire(WindowedValue<T> element) {
+    try {
+      byte[] wireBytes = CoderUtils.encodeToByteArray(sdkWireCoder, element);
+      return CoderUtils.decodeFromByteArray(runnerWireCoder, wireBytes);
+    } catch (CoderException e) {
+      throw new RuntimeException(
+          "Failed to transcode a read element to wire form for transform " + 
transformId, e);
+    }
+  }
+
+  /**
+   * Forwards a terminal {@code TIMESTAMP_MAX_VALUE} watermark payload to 
downstream processors.
+   *
+   * <p>Read is a single-instance source, so the report is stamped as the only 
source partition:
+   * {@code sourcePartition=0} of {@code totalSourcePartitions=1}. Real 
per-partition identities
+   * arrive once the topology gains topic-based shuffle.
+   */
+  private static void forwardWatermarkMax(ProcessorContext<byte[], 
KStreamsPayload<?>> ctx) {
+    long maxMillis = BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis();
+    ctx.forward(
+        new Record<byte[], KStreamsPayload<?>>(
+            new byte[0], KStreamsPayload.<Object>watermark(maxMillis, 0, 1), 
0L));
+  }
+
+  /** Cancels the wall-clock punctuator after the read has fired to stop 
periodic wakeups. */
+  private void cancelPunctuator() {
+    Cancellable handle = scheduledPunctuator;
+    if (handle != null) {
+      handle.cancel();
+      scheduledPunctuator = null;
+    }
+  }
+}
diff --git 
a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadTranslator.java
 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadTranslator.java
new file mode 100644
index 00000000000..403499ca616
--- /dev/null
+++ 
b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ReadTranslator.java
@@ -0,0 +1,173 @@
+/*
+ * 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.io.IOException;
+import org.apache.beam.model.pipeline.v1.RunnerApi;
+import 
org.apache.beam.model.pipeline.v1.RunnerApi.ExecutableStagePayload.WireCoderSetting;
+import org.apache.beam.runners.core.construction.SerializablePipelineOptions;
+import org.apache.beam.runners.fnexecution.wire.WireCoders;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.io.BoundedSource;
+import org.apache.beam.sdk.util.construction.ReadTranslation;
+import org.apache.beam.sdk.util.construction.RehydratedComponents;
+import org.apache.beam.sdk.util.construction.graph.PipelineNode;
+import 
org.apache.beam.sdk.util.construction.graph.PipelineNode.PCollectionNode;
+import org.apache.beam.sdk.values.WindowedValue;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables;
+import org.apache.kafka.common.serialization.Serdes;
+import org.apache.kafka.streams.Topology;
+import org.apache.kafka.streams.state.KeyValueBytesStoreSupplier;
+import org.apache.kafka.streams.state.Stores;
+
+/**
+ * Translates the deprecated primitive {@code Read} URN ({@code 
beam:transform:read:v1}) over a
+ * {@link BoundedSource}.
+ *
+ * <p>The runner forces every {@code Read.Bounded} (including the one {@code 
Create} of two or more
+ * elements expands to) into this primitive read before translation — see 
{@code
+ * KafkaStreamsTestRunner.translate}, which applies {@code
+ * SplittableParDo.convertReadBasedSplittableDoFnsToPrimitiveReads}. This 
deliberately avoids the
+ * default {@code BoundedSourceAsSDFWrapperFn} splittable-DoFn expansion, 
which the runner cannot
+ * execute yet (no SDF restriction protocol), as agreed with the mentor.
+ *
+ * <p>Adds the same three-node shape as {@link ImpulseTranslator}:
+ *
+ * <ul>
+ *   <li>A {@code byte[]} source bound to a dedicated per-transform bootstrap 
topic (see {@link
+ *       KafkaStreamsTranslationContext#getReadBootstrapTopic(String)}). Kafka 
Streams refuses to
+ *       start a topology with no real source topic; records published to it 
are ignored by {@link
+ *       ReadProcessor}.
+ *   <li>The {@link ReadProcessor}, which reads the {@link BoundedSource} on a 
one-shot wall-clock
+ *       punctuator and emits one data payload per element followed by a 
terminal watermark.
+ *   <li>A per-processor persistent state store recording whether the read has 
already fired so task
+ *       restarts do not duplicate elements.
+ * </ul>
+ *
+ * <p>The processor emits elements in the runner-side wire form the downstream 
stage's SDK harness
+ * expects, so it is handed the SDK-side and runner-side wire coders for the 
read's output
+ * PCollection (see {@link ReadProcessor} for why). Only {@link
+ * org.apache.beam.model.pipeline.v1.RunnerApi.IsBounded.Enum#BOUNDED bounded} 
sources are
+ * supported; {@link ReadTranslation#boundedSourceFromProto} rejects an 
unbounded payload.
+ */
+class ReadTranslator implements PTransformTranslator {
+
+  static final String SOURCE_SUFFIX = "-source";
+  static final String STATE_STORE_SUFFIX = "-state";
+
+  @Override
+  public void translate(
+      String transformId, RunnerApi.Pipeline pipeline, 
KafkaStreamsTranslationContext context) {
+    RunnerApi.PTransform transform = 
pipeline.getComponents().getTransformsOrThrow(transformId);
+    // Read produces exactly one output PCollection; downstream consumers are 
separate PTransforms
+    // whose inputs reference this PCollection id and are wired by their own 
translators.
+    String outputPCollectionId = 
Iterables.getOnlyElement(transform.getOutputsMap().values());
+    addReadNodes(
+        transformId,
+        boundedSource(transform),
+        pipeline.getComponents(),
+        outputPCollectionId,
+        context);
+  }
+
+  private static BoundedSource<?> boundedSource(RunnerApi.PTransform 
transform) {
+    try {
+      RunnerApi.ReadPayload payload =
+          RunnerApi.ReadPayload.parseFrom(transform.getSpec().getPayload());
+      return ReadTranslation.boundedSourceFromProto(payload);
+    } catch (IOException e) {
+      throw new RuntimeException(
+          "Failed to read the BoundedSource from transform " + 
transform.getUniqueName(), e);
+    }
+  }
+
+  /**
+   * Adds the source, {@link ReadProcessor}, and state store for the read. The 
type variable {@code
+   * T} captures the {@link BoundedSource}'s element type so the processor and 
its wire coders are
+   * built consistently.
+   */
+  private <T> void addReadNodes(
+      String transformId,
+      BoundedSource<T> source,
+      RunnerApi.Components components,
+      String outputPCollectionId,
+      KafkaStreamsTranslationContext context) {
+    PCollectionNode outputNode =
+        PipelineNode.pCollection(
+            outputPCollectionId, 
components.getPcollectionsOrThrow(outputPCollectionId));
+    Coder<WindowedValue<T>> sdkWireCoder = sdkWireCoder(outputNode, 
components);
+    Coder<WindowedValue<?>> runnerWireCoder = runnerWireCoder(outputNode, 
components);
+
+    Topology topology = context.getTopology();
+    String sourceNodeName = transformId + SOURCE_SUFFIX;
+    String stateStoreName = transformId + STATE_STORE_SUFFIX;
+    String bootstrapTopic = context.getReadBootstrapTopic(transformId);
+    SerializablePipelineOptions options =
+        new SerializablePipelineOptions(context.getPipelineOptions());
+
+    topology.addSource(
+        sourceNodeName,
+        Serdes.ByteArray().deserializer(),
+        Serdes.ByteArray().deserializer(),
+        bootstrapTopic);
+    topology.addProcessor(
+        transformId,
+        () ->
+            new ReadProcessor<>(
+                source, options, sdkWireCoder, runnerWireCoder, 
stateStoreName, transformId),
+        sourceNodeName);
+    KeyValueBytesStoreSupplier storeSupplier = 
Stores.persistentKeyValueStore(stateStoreName);
+    topology.addStateStore(
+        Stores.keyValueStoreBuilder(storeSupplier, Serdes.String(), 
Serdes.Boolean()), transformId);
+
+    context.registerPCollectionProducer(outputPCollectionId, transformId);
+  }
+
+  /** The coder the SDK harness would use on the wire, keeping unknown element 
coders intact. */
+  private static <T> Coder<WindowedValue<T>> sdkWireCoder(
+      PCollectionNode outputNode, RunnerApi.Components components) {
+    try {
+      RunnerApi.Components.Builder builder = components.toBuilder();
+      String coderId =
+          WireCoders.addSdkWireCoder(outputNode, builder, 
WireCoderSetting.getDefaultInstance());
+      @SuppressWarnings("unchecked")
+      Coder<WindowedValue<T>> coder =
+          (Coder<WindowedValue<T>>)
+              
RehydratedComponents.forComponents(builder.build()).getCoder(coderId);
+      return coder;
+    } catch (IOException e) {
+      throw new RuntimeException(
+          "Failed to build the SDK wire coder for PCollection " + 
outputNode.getId(), e);
+    }
+  }
+
+  /** The coder the runner uses on the wire, replacing unknown element coders 
with byte arrays. */
+  private static Coder<WindowedValue<?>> runnerWireCoder(
+      PCollectionNode outputNode, RunnerApi.Components components) {
+    try {
+      @SuppressWarnings("unchecked")
+      Coder<WindowedValue<?>> coder =
+          (Coder<WindowedValue<?>>)
+              (Coder<?>) WireCoders.instantiateRunnerWireCoder(outputNode, 
components);
+      return coder;
+    } catch (IOException e) {
+      throw new RuntimeException(
+          "Failed to build the runner wire coder for PCollection " + 
outputNode.getId(), e);
+    }
+  }
+}
diff --git 
a/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/KafkaStreamsTestRunner.java
 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/KafkaStreamsTestRunner.java
index 53b7714548a..4d73eee9581 100644
--- 
a/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/KafkaStreamsTestRunner.java
+++ 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/KafkaStreamsTestRunner.java
@@ -36,6 +36,7 @@ import org.apache.beam.sdk.testing.CrashingRunner;
 import org.apache.beam.sdk.util.construction.Environments;
 import org.apache.beam.sdk.util.construction.PipelineOptionsTranslation;
 import org.apache.beam.sdk.util.construction.PipelineTranslation;
+import org.apache.beam.sdk.util.construction.SplittableParDo;
 import org.apache.kafka.common.serialization.ByteArrayDeserializer;
 import org.apache.kafka.common.serialization.ByteArraySerializer;
 import org.apache.kafka.common.serialization.Serdes;
@@ -88,6 +89,11 @@ public final class KafkaStreamsTestRunner {
   public static KafkaStreamsTranslationContext translate(Pipeline pipeline) {
     KafkaStreamsPipelineOptions options =
         pipeline.getOptions().as(KafkaStreamsPipelineOptions.class);
+    // Force every Read.Bounded (including the one Create of 2+ elements 
expands to) into the
+    // deprecated primitive Read the runner translates, instead of the default
+    // BoundedSourceAsSDFWrapperFn splittable-DoFn expansion the runner cannot 
execute yet. This is
+    // unconditional — it does not depend on the use_deprecated_read 
experiment (mentor's steer).
+    SplittableParDo.convertReadBasedSplittableDoFnsToPrimitiveReads(pipeline);
     RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline);
     KafkaStreamsPipelineTranslator translator = new 
KafkaStreamsPipelineTranslator();
     JobInfo jobInfo =
diff --git 
a/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/CreateTest.java
 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/CreateTest.java
new file mode 100644
index 00000000000..e2fab243d4b
--- /dev/null
+++ 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/CreateTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.List;
+import org.apache.beam.runners.kafka.streams.KafkaStreamsTestRunner;
+import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.transforms.DoFn;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.junit.Test;
+
+/**
+ * End-to-end test that {@code Create} of two or more elements runs on the 
Kafka Streams runner.
+ *
+ * <p>{@code Create.of(1, 2, 3)} expands to {@code Read.from(CreateSource)}, a 
bounded {@code
+ * Read.Bounded}. {@code KafkaStreamsTestRunner.translate} forces it into the 
deprecated primitive
+ * read the runner translates (see {@link ReadTranslator}), so the elements 
flow through {@link
+ * ReadProcessor} into a recording ParDo in the EMBEDDED harness. This is the 
multi-element Create
+ * case that previously failed because the default expansion is an unsupported 
splittable DoFn.
+ */
+public class CreateTest {
+
+  /** Records every element the harness feeds it so the test can assert Create 
produced them. */
+  private static class RecordingFn extends DoFn<Integer, Integer> {
+    private final SharedTestCollector<Integer> collector;
+
+    RecordingFn(SharedTestCollector<Integer> collector) {
+      this.collector = collector;
+    }
+
+    @ProcessElement
+    public void processElement(@Element Integer input, OutputReceiver<Integer> 
out) {
+      collector.record(input);
+      out.output(input);
+    }
+  }
+
+  @Test
+  public void createOfMultipleElementsRunsThroughPrimitiveRead() {
+    try (SharedTestCollector<Integer> collector = 
SharedTestCollector.create()) {
+      Pipeline pipeline = 
Pipeline.create(KafkaStreamsTestRunner.testOptions());
+      pipeline
+          .apply("create", Create.of(1, 2, 3))
+          .apply("record", ParDo.of(new RecordingFn(collector)));
+
+      KafkaStreamsTestRunner.run(pipeline);
+
+      List<Integer> recorded = collector.recorded();
+      assertThat(recorded.size(), is(3));
+      assertThat(recorded, hasItems(1, 2, 3));
+    }
+  }
+}
diff --git 
a/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/ReadTest.java
 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/ReadTest.java
new file mode 100644
index 00000000000..5b50aa27214
--- /dev/null
+++ 
b/runners/kafka-streams/src/test/java/org/apache/beam/runners/kafka/streams/translation/ReadTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.List;
+import org.apache.beam.runners.kafka.streams.KafkaStreamsTestRunner;
+import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.io.CountingSource;
+import org.apache.beam.sdk.io.Read;
+import org.apache.beam.sdk.transforms.DoFn;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.junit.Test;
+
+/**
+ * End-to-end test for {@link ReadTranslator}: a primitive bounded {@code 
Read} over a {@link
+ * CountingSource} feeds a recording ParDo executed in the in-process 
(EMBEDDED) Java SDK harness.
+ * Verifies every source element reaches the DoFn.
+ *
+ * <p>{@code KafkaStreamsTestRunner.translate} forces the {@code Read.Bounded} 
into the deprecated
+ * primitive read the runner translates, rather than the default 
splittable-DoFn expansion — so this
+ * exercises the {@link ReadProcessor} + {@link ReadTranslator} path end to 
end.
+ */
+public class ReadTest {
+
+  /** Records every element the harness feeds it so the test can assert the 
read produced them. */
+  private static class RecordingFn extends DoFn<Long, Long> {
+    private final SharedTestCollector<Long> collector;
+
+    RecordingFn(SharedTestCollector<Long> collector) {
+      this.collector = collector;
+    }
+
+    @ProcessElement
+    public void processElement(@Element Long input, OutputReceiver<Long> out) {
+      collector.record(input);
+      out.output(input);
+    }
+  }
+
+  @Test
+  public void boundedReadEmitsEverySourceElement() {
+    try (SharedTestCollector<Long> collector = SharedTestCollector.create()) {
+      Pipeline pipeline = 
Pipeline.create(KafkaStreamsTestRunner.testOptions());
+      pipeline
+          .apply("read", Read.from(CountingSource.upTo(5)))
+          .apply("record", ParDo.of(new RecordingFn(collector)));
+
+      KafkaStreamsTestRunner.run(pipeline);
+
+      List<Long> recorded = collector.recorded();
+      // CountingSource.upTo(5) yields 0..4 exactly once each.
+      assertThat(recorded.size(), is(5));
+      assertThat(recorded, hasItems(0L, 1L, 2L, 3L, 4L));
+    }
+  }
+}

Reply via email to