The Beam key becomes the Kafka record key so Kafka Streams shuffles by it. The topology is a
* {@link ShuffleByKeyProcessor} that sets that key and passes watermark reports through, a sink to
- * an internal repartition topic using a {@link GroupByKeyBroadcastPartitioner} that hashes data by
- * key and fans watermarks out to every partition, a source reading that topic back, and the {@link
+ * an internal repartition topic using a {@link KStreamsPayloadPartitioner} that hashes data by key
+ * and fans watermarks out to every partition, a source reading that topic back, and the {@link
* WindowedGroupByKeyProcessor} with its state and timer stores.
*/
class GroupByKeyTranslator implements PTransformTranslator {
@@ -114,16 +114,20 @@ public void translate(
shuffleName,
() ->
new ShuffleByKeyProcessor(
- keyCoder, upstreamPartitionCount, shuffleName, context.getTerminationTracker()),
+ keyCoder,
+ upstreamPartitionCount,
+ partitionCount,
+ shuffleName,
+ context.getTerminationTracker()),
parentProcessor);
- // Shuffle through the repartition topic: data partitioned by key, watermark broadcast.
+ // Shuffle through the repartition topic: data by key, watermarks broadcast, flushes targeted.
topology.addSink(
sinkName,
repartitionTopic,
Serdes.ByteArray().serializer(),
payloadSerde.serializer(),
- new GroupByKeyBroadcastPartitioner<>(),
+ new KStreamsPayloadPartitioner<>(partitionCount),
shuffleName);
topology.addSource(
sourceName,
diff --git a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/GroupByKeyBroadcastPartitioner.java b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KStreamsPayloadPartitioner.java
similarity index 74%
rename from runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/GroupByKeyBroadcastPartitioner.java
rename to runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KStreamsPayloadPartitioner.java
index 3c775c86f14c..8a892300225b 100644
--- a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/GroupByKeyBroadcastPartitioner.java
+++ b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/KStreamsPayloadPartitioner.java
@@ -25,19 +25,27 @@
import org.apache.kafka.streams.processor.StreamPartitioner;
/**
- * Partitions records on the GroupByKey repartition topic.
+ * Partitions records on a repartition topic.
*
*
*
data records go to the single partition selected by hashing the (already encoded
* Beam key) Kafka record key — the same scheme Kafka's default partitioner uses — so every
* value of a key lands together;
*
watermark reports are broadcast to every partition, so each downstream
- * GroupByKey task observes the terminal watermark and fires its keys.
+ * GroupByKey task observes the terminal watermark and fires its keys;
+ *
flush markers go to the partitions they name, one copy to each.
*
*
* @param the data element type carried by data payloads
*/
-class GroupByKeyBroadcastPartitioner implements StreamPartitioner> {
+class KStreamsPayloadPartitioner implements StreamPartitioner> {
+
+ /** The partition count the flush targets were computed for. */
+ private final int expectedPartitions;
+
+ KStreamsPayloadPartitioner(int expectedPartitions) {
+ this.expectedPartitions = expectedPartitions;
+ }
@Override
public Integer partition(String topic, byte[] key, KStreamsPayload value, int numPartitions) {
@@ -56,6 +64,17 @@ public Optional> partitions(
}
return Optional.of(all);
}
+ if (value.isFlush()) {
+ // Targets assume internalParallelism partitions; a topic from an earlier run may differ.
+ if (numPartitions != expectedPartitions) {
+ throw new IllegalStateException(
+ String.format(
+ "Repartition topic %s has %d partitions but the pipeline expects %d. It was"
+ + " probably created by an earlier run with a different --internalParallelism.",
+ topic, numPartitions, expectedPartitions));
+ }
+ return Optional.of(value.asFlush().getTargetPartitions());
+ }
if (key == null) {
// A keyless record has no partition it must go to, so leave the choice to Kafka rather than
// hashing a null or pinning one partition: an empty Optional tells Kafka Streams no explicit
diff --git a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ShuffleByKeyProcessor.java b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ShuffleByKeyProcessor.java
index 638239406ed9..ce4bc9bb48f4 100644
--- a/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ShuffleByKeyProcessor.java
+++ b/runners/kafka-streams/src/main/java/org/apache/beam/runners/kafka/streams/translation/ShuffleByKeyProcessor.java
@@ -17,10 +17,12 @@
*/
package org.apache.beam.runners.kafka.streams.translation;
+import java.util.Set;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.util.CoderUtils;
import org.apache.beam.sdk.values.KV;
+import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet;
import org.apache.kafka.streams.processor.api.Processor;
import org.apache.kafka.streams.processor.api.ProcessorContext;
import org.apache.kafka.streams.processor.api.Record;
@@ -39,11 +41,13 @@
*
Watermark reports are relabelled here with the reporting instance's real partition identity.
* This is the point at which a report stops being delivered in-process and starts crossing a topic:
* upstream of it a transform forwards to its fused children, which see exactly one instance of it,
- * so the report names a single source. The {@link GroupByKeyBroadcastPartitioner} on the sink below
+ * so the report names a single source. The {@link KStreamsPayloadPartitioner} on the sink below
* fans each report out to every partition, so a downstream task instead sees a report from
* every instance of the upstream transform, and has to be able to tell them apart to know when it
* has heard from all of them. The transform id is left alone, so the report still names the
* transform that produced it.
+ *
+ *
Flush markers are readdressed here too; see {@link #flushTargets}.
*/
class ShuffleByKeyProcessor
implements Processor, byte[], KStreamsPayload>> {
@@ -57,6 +61,9 @@ class ShuffleByKeyProcessor
private int upstreamPartition;
+ /** Partitions of the repartition topic. */
+ private final int downstreamPartitionCount;
+
// Reports this shuffle as finished once it has written the terminal watermark to the repartition
// topic. The downstream side reading that topic reports separately, which is why the pipeline
// waits for every processor rather than the first.
@@ -67,10 +74,12 @@ class ShuffleByKeyProcessor
ShuffleByKeyProcessor(
Coder