This is an automated email from the ASF dual-hosted git repository.
Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new c979b5cfbe2 [Flink] Select bounded-source split assignment by
estimated size (#39874)
c979b5cfbe2 is described below
commit c979b5cfbe26b545c4d742081292d187d58dbd11
Author: Paulius Kuzmickas <[email protected]>
AuthorDate: Wed Sep 2 18:59:13 2026 +0100
[Flink] Select bounded-source split assignment by estimated size (#39874)
* [Flink] Select bounded-source split assignment by size
---
CHANGES.md | 1 +
.../beam/runners/flink/FlinkPipelineOptions.java | 11 +
.../beam/runners/flink/FlinkPipelineOptions.java | 11 +
.../wrappers/streaming/io/source/FlinkSource.java | 64 +--
.../io/source/FlinkSourceEnumeratorState.java | 52 ++
.../FlinkSourceEnumeratorStateSerializer.java | 103 ++++
.../io/source/FlinkSourceSplitEnumerator.java | 141 +++---
.../streaming/io/source/FlinkSourceSplitUtils.java | 84 ++++
.../io/source/LazyFlinkSourceSplitEnumerator.java | 184 ++++----
.../SizeBasedFlinkSourceSplitEnumerator.java | 205 ++++++++
.../io/source/FlinkSourceSplitEnumeratorTest.java | 522 +++++++++++++++++----
.../shortcodes/flink_java_pipeline_options.html | 5 +
.../shortcodes/flink_python_pipeline_options.html | 5 +
13 files changed, 1101 insertions(+), 287 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index b857e09ac29..fc34c155256 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -123,6 +123,7 @@
## New Features / Improvements
+* [Flink Runner] Added opt-in static round-robin split assignment for small
bounded sources via the new `sourceStaticSplitThresholdMb` pipeline option. The
default of 0 keeps the existing lazy pull-based assignment
([#39873](https://github.com/apache/beam/issues/39873)).
* Added automatic caching of bounded, single-pane side-input views for classic
Java Flink DataStream execution
([#39866](https://github.com/apache/beam/issues/39866)).
* Added `GroupIntoBatches` transform and the standard
`beam:coder:sharded_key:v1` coder to the Go SDK, along with
diff --git
a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
index 3fee130d58e..d3c5fefd9cf 100644
---
a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
+++
b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
@@ -365,6 +365,17 @@ public interface FlinkPipelineOptions
void setFileInputSplitMaxSizeMB(Long fileInputSplitMaxSizeMB);
+ @Description(
+ "Static split assignment threshold in MiB per source reader for bounded
sources in Flink "
+ + "DataStream mode. The default of 0 always uses lazy assignment. A
positive value "
+ + "selects static round-robin assignment for sources with a known,
positive estimate "
+ + "below the threshold and lazy assignment otherwise. Any negative
value always uses "
+ + "static assignment.")
+ @Default.Long(0)
+ Long getSourceStaticSplitThresholdMb();
+
+ void setSourceStaticSplitThresholdMb(Long thresholdMb);
+
@Description(
"Allow drain operation for flink pipelines that contain
RequiresStableInput operator. Note that at time of draining,"
+ "the RequiresStableInput contract might be violated if there any
processing related failures in the DoFn operator.")
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
index 6e4d998ed09..a6729356bdc 100644
---
a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkPipelineOptions.java
@@ -382,6 +382,17 @@ public interface FlinkPipelineOptions
void setFileInputSplitMaxSizeMB(Long fileInputSplitMaxSizeMB);
+ @Description(
+ "Static split assignment threshold in MiB per source reader for bounded
sources in Flink "
+ + "DataStream mode. The default of 0 always uses lazy assignment. A
positive value "
+ + "selects static round-robin assignment for sources with a known,
positive estimate "
+ + "below the threshold and lazy assignment otherwise. Any negative
value always uses "
+ + "static assignment.")
+ @Default.Long(0)
+ Long getSourceStaticSplitThresholdMb();
+
+ void setSourceStaticSplitThresholdMb(Long thresholdMb);
+
@Description(
"Allow drain operation for flink pipelines that contain
RequiresStableInput operator. Note that at time of draining,"
+ "the RequiresStableInput contract might be violated if there any
processing related failures in the DoFn operator.")
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSource.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSource.java
index 0c1ba73b2a2..f3bbf14f68f 100644
---
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSource.java
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSource.java
@@ -18,12 +18,10 @@
package org.apache.beam.runners.flink.translation.wrappers.streaming.io.source;
import java.io.Serializable;
-import java.util.List;
-import java.util.Map;
import java.util.function.Function;
import org.apache.beam.runners.core.construction.SerializablePipelineOptions;
import org.apache.beam.runners.flink.FlinkPipelineOptions;
-import org.apache.beam.runners.flink.translation.utils.SerdeUtils;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.bounded.FlinkBoundedSource;
import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.impulse.BeamImpulseSource;
import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.unbounded.FlinkUnboundedSource;
@@ -43,7 +41,7 @@ import org.apache.flink.core.io.SimpleVersionedSerializer;
* @param <OutputT> The data type of the records emitted by the Flink Source.
*/
public abstract class FlinkSource<T, OutputT>
- implements Source<OutputT, FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>> {
+ implements Source<OutputT, FlinkSourceSplit<T>,
FlinkSourceEnumeratorState<T>> {
protected final String stepName;
protected final org.apache.beam.sdk.io.Source<T> beamSource;
@@ -102,36 +100,38 @@ public abstract class FlinkSource<T, OutputT>
}
@Override
- public SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>>
- createEnumerator(SplitEnumeratorContext<FlinkSourceSplit<T>>
enumContext) throws Exception {
- return createEnumerator(enumContext, false);
- }
-
- public SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>>
- createEnumerator(
- SplitEnumeratorContext<FlinkSourceSplit<T>> enumContext, boolean
splitInitialized)
- throws Exception {
-
+ public SplitEnumerator<FlinkSourceSplit<T>, FlinkSourceEnumeratorState<T>>
createEnumerator(
+ SplitEnumeratorContext<FlinkSourceSplit<T>> enumContext) throws
Exception {
+ FlinkPipelineOptions options =
serializablePipelineOptions.get().as(FlinkPipelineOptions.class);
if (boundedness == Boundedness.BOUNDED) {
- return new LazyFlinkSourceSplitEnumerator<>(
- enumContext, beamSource, serializablePipelineOptions.get(),
numSplits, splitInitialized);
- } else {
- return new FlinkSourceSplitEnumerator<>(
- enumContext, beamSource, serializablePipelineOptions.get(),
numSplits, splitInitialized);
+ long thresholdMb = options.getSourceStaticSplitThresholdMb();
+ if (thresholdMb < 0) {
+ return new FlinkSourceSplitEnumerator<>(enumContext, beamSource,
options, numSplits);
+ }
+ if (thresholdMb > 0) {
+ return new SizeBasedFlinkSourceSplitEnumerator<>(
+ enumContext, (BoundedSource<T>) beamSource, options, numSplits);
+ }
+ return new LazyFlinkSourceSplitEnumerator<>(enumContext, beamSource,
options, numSplits);
}
+ return new FlinkSourceSplitEnumerator<>(enumContext, beamSource, options,
numSplits);
}
@Override
- public SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>>
- restoreEnumerator(
- SplitEnumeratorContext<FlinkSourceSplit<T>> enumContext,
- Map<Integer, List<FlinkSourceSplit<T>>> checkpoint)
- throws Exception {
- SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>> enumerator =
- createEnumerator(enumContext, true);
- checkpoint.forEach(
- (subtaskId, splitsForSubtask) ->
enumerator.addSplitsBack(splitsForSubtask, subtaskId));
- return enumerator;
+ public SplitEnumerator<FlinkSourceSplit<T>, FlinkSourceEnumeratorState<T>>
restoreEnumerator(
+ SplitEnumeratorContext<FlinkSourceSplit<T>> enumContext,
+ FlinkSourceEnumeratorState<T> checkpoint)
+ throws Exception {
+ FlinkPipelineOptions options =
serializablePipelineOptions.get().as(FlinkPipelineOptions.class);
+ if (checkpoint.getAssignmentMode() == AssignmentMode.LAZY) {
+ return new LazyFlinkSourceSplitEnumerator<>(
+ enumContext, beamSource, options, numSplits, checkpoint);
+ }
+ if (checkpoint.getAssignmentMode() == AssignmentMode.STATIC) {
+ return new FlinkSourceSplitEnumerator<>(
+ enumContext, beamSource, options, numSplits, checkpoint);
+ }
+ return createEnumerator(enumContext);
}
@Override
@@ -140,9 +140,11 @@ public abstract class FlinkSource<T, OutputT>
}
@Override
- public SimpleVersionedSerializer<Map<Integer, List<FlinkSourceSplit<T>>>>
+ public SimpleVersionedSerializer<FlinkSourceEnumeratorState<T>>
getEnumeratorCheckpointSerializer() {
- return SerdeUtils.getNaiveObjectSerializer();
+ AssignmentMode legacyAssignmentMode =
+ boundedness == Boundedness.BOUNDED ? AssignmentMode.LAZY :
AssignmentMode.STATIC;
+ return new FlinkSourceEnumeratorStateSerializer<>(legacyAssignmentMode);
}
public int getNumSplits() {
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorState.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorState.java
new file mode 100644
index 00000000000..8a825eeb991
--- /dev/null
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorState.java
@@ -0,0 +1,52 @@
+/*
+ * 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.flink.translation.wrappers.streaming.io.source;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Checkpoint state shared by the lazy and static source split assignment
strategies. */
+public final class FlinkSourceEnumeratorState<T> implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final AssignmentMode assignmentMode;
+ private final ArrayList<FlinkSourceSplit<T>> pendingSplits;
+
+ /** Takes ownership of {@code pendingSplits}; the caller must not mutate it
afterwards. */
+ FlinkSourceEnumeratorState(
+ AssignmentMode assignmentMode, ArrayList<FlinkSourceSplit<T>>
pendingSplits) {
+ this.assignmentMode = assignmentMode;
+ this.pendingSplits = pendingSplits;
+ }
+
+ AssignmentMode getAssignmentMode() {
+ return assignmentMode;
+ }
+
+ List<FlinkSourceSplit<T>> getPendingSplits() {
+ return Collections.unmodifiableList(pendingSplits);
+ }
+
+ enum AssignmentMode {
+ UNDECIDED,
+ LAZY,
+ STATIC
+ }
+}
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorStateSerializer.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorStateSerializer.java
new file mode 100644
index 00000000000..3470e60d0bd
--- /dev/null
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceEnumeratorStateSerializer.java
@@ -0,0 +1,103 @@
+/*
+ * 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.flink.translation.wrappers.streaming.io.source;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.beam.runners.flink.translation.utils.SerdeUtils;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+
+/** Serializes source enumerator state and upgrades the map used by earlier
runner versions. */
+final class FlinkSourceEnumeratorStateSerializer<T>
+ implements SimpleVersionedSerializer<FlinkSourceEnumeratorState<T>> {
+ // Version written by SerdeUtils.getNaiveObjectSerializer, which serialized
the
+ // Map<Integer, List<FlinkSourceSplit<T>>> state used by earlier runner
versions.
+ static final int LEGACY_MAP_VERSION = 0;
+ static final int VERSION = 1;
+
+ private final AssignmentMode legacyAssignmentMode;
+
+ FlinkSourceEnumeratorStateSerializer(AssignmentMode legacyAssignmentMode) {
+ this.legacyAssignmentMode = legacyAssignmentMode;
+ }
+
+ @Override
+ public int getVersion() {
+ return VERSION;
+ }
+
+ @Override
+ public byte[] serialize(FlinkSourceEnumeratorState<T> state) throws
IOException {
+ return SerdeUtils.serializeObject(state);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public FlinkSourceEnumeratorState<T> deserialize(int version, byte[]
serialized)
+ throws IOException {
+ if (version == VERSION) {
+ Object deserialized = SerdeUtils.deserializeObject(serialized);
+ if (deserialized instanceof FlinkSourceEnumeratorState) {
+ return (FlinkSourceEnumeratorState<T>) deserialized;
+ }
+ throw new IOException(
+ "Expected source enumerator state of type FlinkSourceEnumeratorState
for version "
+ + version
+ + ", but got: "
+ + describe(deserialized));
+ }
+ if (version == LEGACY_MAP_VERSION) {
+ Object deserialized = SerdeUtils.deserializeObject(serialized);
+ if (deserialized instanceof Map) {
+ return upgradeLegacyState((Map<?, ?>) deserialized);
+ }
+ throw new IOException(
+ "Expected legacy source enumerator state of type Map for version "
+ + version
+ + ", but got: "
+ + describe(deserialized));
+ }
+ throw new IOException(
+ String.format(
+ "Received source enumerator state version %d, but the highest
supported version "
+ + "is %d.",
+ version, VERSION));
+ }
+
+ @SuppressWarnings("unchecked")
+ private FlinkSourceEnumeratorState<T> upgradeLegacyState(Map<?, ?>
legacyState)
+ throws IOException {
+ ArrayList<FlinkSourceSplit<T>> pendingSplits = new ArrayList<>();
+ for (Object value : legacyState.values()) {
+ List<FlinkSourceSplit<T>> legacySplits = (List<FlinkSourceSplit<T>>)
value;
+ if (legacySplits == null) {
+ throw new IOException("Legacy source enumerator state contains a null
split list.");
+ }
+ pendingSplits.addAll(legacySplits);
+ }
+ return new FlinkSourceEnumeratorState<>(legacyAssignmentMode,
pendingSplits);
+ }
+
+ private static String describe(@Nullable Object obj) {
+ return obj == null ? "null" : obj.getClass().getName();
+ }
+}
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumerator.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumerator.java
index 38d2f363939..bb8956f5dae 100644
---
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumerator.java
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumerator.java
@@ -21,10 +21,13 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nullable;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
import org.apache.beam.sdk.io.BoundedSource;
import org.apache.beam.sdk.io.Source;
import org.apache.beam.sdk.io.UnboundedSource;
@@ -35,27 +38,17 @@ import
org.apache.flink.api.connector.source.SplitsAssignment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * A Flink {@link org.apache.flink.api.connector.source.SplitEnumerator
SplitEnumerator}
- * implementation that holds a Beam {@link Source} and does the following:
- *
- * <ul>
- * <li>Split the Beam {@link Source} to desired number of splits.
- * <li>Assign the splits to the Flink Source Reader.
- * </ul>
- *
- * <p>Note that at this point, this class has a static round-robin split
assignment strategy.
- *
- * @param <T> The output type of the encapsulated Beam {@link Source}.
- */
+/** Splits a Beam source and assigns its splits to Flink source readers
round-robin. */
public class FlinkSourceSplitEnumerator<T>
- implements SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>> {
+ implements SplitEnumerator<FlinkSourceSplit<T>,
FlinkSourceEnumeratorState<T>> {
private static final Logger LOG =
LoggerFactory.getLogger(FlinkSourceSplitEnumerator.class);
+
private final SplitEnumeratorContext<FlinkSourceSplit<T>> context;
private final Source<T> beamSource;
private final PipelineOptions pipelineOptions;
private final int numSplits;
private final Map<Integer, List<FlinkSourceSplit<T>>> pendingSplits;
+
private boolean splitsInitialized;
public FlinkSourceSplitEnumerator(
@@ -63,8 +56,7 @@ public class FlinkSourceSplitEnumerator<T>
Source<T> beamSource,
PipelineOptions pipelineOptions,
int numSplits) {
-
- this(context, beamSource, pipelineOptions, numSplits, false);
+ this(context, beamSource, pipelineOptions, numSplits, null);
}
public FlinkSourceSplitEnumerator(
@@ -72,17 +64,31 @@ public class FlinkSourceSplitEnumerator<T>
Source<T> beamSource,
PipelineOptions pipelineOptions,
int numSplits,
- boolean splitsInitialized) {
-
+ @Nullable FlinkSourceEnumeratorState<T> restoredState) {
this.context = context;
this.beamSource = beamSource;
this.pipelineOptions = pipelineOptions;
this.numSplits = numSplits;
this.pendingSplits = new HashMap<>(numSplits);
- this.splitsInitialized = splitsInitialized;
+ this.splitsInitialized = restoredState != null;
+
+ if (restoredState != null) {
+ if (restoredState.getAssignmentMode() != AssignmentMode.STATIC) {
+ throw new IllegalArgumentException(
+ "Cannot restore the static source enumerator from "
+ + restoredState.getAssignmentMode()
+ + " state.");
+ }
+ int parallelism = context.currentParallelism();
+ for (FlinkSourceSplit<T> split : restoredState.getPendingSplits()) {
+ int targetSubtask = split.splitIndex() % parallelism;
+ pendingSplits.computeIfAbsent(targetSubtask, ignored -> new
ArrayList<>()).add(split);
+ }
+ }
LOG.info(
- "Created new enumerator with parallelism {}, source {}, numSplits {},
initialized {}",
+ "Created static source enumerator with parallelism {}, source {},
numSplits {}, "
+ + "initialized {}",
context.currentParallelism(),
beamSource,
numSplits,
@@ -93,52 +99,33 @@ public class FlinkSourceSplitEnumerator<T>
public void start() {
if (!splitsInitialized) {
initializeSplits();
+ } else {
+ sendPendingSplitsToSourceReaders();
}
}
private void initializeSplits() {
context.callAsync(
- () -> {
- try {
- LOG.info("Starting source {}", beamSource);
- List<? extends Source<T>> beamSplitSourceList = splitBeamSource();
- Map<Integer, List<FlinkSourceSplit<T>>> flinkSourceSplitsList =
new HashMap<>();
- int i = 0;
- for (Source<T> beamSplitSource : beamSplitSourceList) {
- int targetSubtask = i % context.currentParallelism();
- List<FlinkSourceSplit<T>> splitsForTask =
- flinkSourceSplitsList.computeIfAbsent(
- targetSubtask, ignored -> new ArrayList<>());
- splitsForTask.add(new FlinkSourceSplit<>(i, beamSplitSource));
- i++;
- }
- return flinkSourceSplitsList;
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- },
+ this::splitBeamSource,
(sourceSplits, error) -> {
if (error != null) {
throw new RuntimeException("Failed to start source enumerator.",
error);
- } else {
- pendingSplits.putAll(sourceSplits);
- splitsInitialized = true;
- sendPendingSplitsToSourceReaders();
}
+ prepareAssignments(sourceSplits);
+ splitsInitialized = true;
+ sendPendingSplitsToSourceReaders();
});
}
@Override
public void handleSplitRequest(int subtaskId, @Nullable String
requesterHostname) {
- // Not used.
+ // Static assignment happens when readers register.
}
@Override
public void addSplitsBack(List<FlinkSourceSplit<T>> splits, int subtaskId) {
LOG.info("Adding splits {} back from subtask {}", splits, subtaskId);
- List<FlinkSourceSplit<T>> splitsForSubtask =
- pendingSplits.computeIfAbsent(subtaskId, ignored -> new ArrayList<>());
- splitsForSubtask.addAll(splits);
+ pendingSplits.computeIfAbsent(subtaskId, ignored -> new
ArrayList<>()).addAll(splits);
}
@Override
@@ -146,18 +133,19 @@ public class FlinkSourceSplitEnumerator<T>
List<FlinkSourceSplit<T>> splitsForSubtask =
pendingSplits.remove(subtaskId);
if (splitsForSubtask != null) {
assignSplitsAndLog(splitsForSubtask, subtaskId);
- } else {
- if (splitsInitialized) {
- LOG.info("There is no split for subtask {}. Signaling no more
splits.", subtaskId);
- context.signalNoMoreSplits(subtaskId);
- }
+ } else if (splitsInitialized) {
+ LOG.info("There is no split for subtask {}. Signaling no more splits.",
subtaskId);
+ context.signalNoMoreSplits(subtaskId);
}
}
@Override
- public Map<Integer, List<FlinkSourceSplit<T>>> snapshotState(long
checkpointId) throws Exception {
+ public FlinkSourceEnumeratorState<T> snapshotState(long checkpointId) {
LOG.info("Taking snapshot for checkpoint {}", checkpointId);
- return pendingSplits;
+ ArrayList<FlinkSourceSplit<T>> checkpointSplits = new ArrayList<>();
+ pendingSplits.values().forEach(checkpointSplits::addAll);
+ AssignmentMode mode = splitsInitialized ? AssignmentMode.STATIC :
AssignmentMode.UNDECIDED;
+ return new FlinkSourceEnumeratorState<>(mode, checkpointSplits);
}
@Override
@@ -165,34 +153,49 @@ public class FlinkSourceSplitEnumerator<T>
// NoOp
}
- // -------------- Private helper methods ----------------------
- private List<? extends Source<T>> splitBeamSource() throws Exception {
+ private ArrayList<FlinkSourceSplit<T>> splitBeamSource() throws Exception {
+ LOG.info("Starting source {}", beamSource);
if (beamSource instanceof BoundedSource) {
BoundedSource<T> boundedSource = (BoundedSource<T>) beamSource;
- long desiredSizeBytes =
boundedSource.getEstimatedSizeBytes(pipelineOptions) / numSplits;
- return boundedSource.split(desiredSizeBytes, pipelineOptions);
- } else if (beamSource instanceof UnboundedSource) {
- List<? extends UnboundedSource<T, ?>> splits =
- ((UnboundedSource<T, ?>) beamSource).split(numSplits,
pipelineOptions);
- LOG.info("Split source {} to {} splits", beamSource, splits);
- return splits;
- } else {
- throw new IllegalStateException("Unknown source type " +
beamSource.getClass());
+ long estimatedSizeBytes =
+ FlinkSourceSplitUtils.estimateBoundedSourceSize(boundedSource,
pipelineOptions);
+ return FlinkSourceSplitUtils.splitBoundedSource(
+ boundedSource, pipelineOptions, numSplits, estimatedSizeBytes);
+ }
+ if (beamSource instanceof UnboundedSource) {
+ return FlinkSourceSplitUtils.splitUnboundedSource(
+ (UnboundedSource<T, ?>) beamSource, pipelineOptions, numSplits);
+ }
+ throw new IllegalStateException("Unknown source type " +
beamSource.getClass());
+ }
+
+ private void prepareAssignments(List<FlinkSourceSplit<T>> sourceSplits) {
+ int parallelism = context.currentParallelism();
+ for (FlinkSourceSplit<T> split : sourceSplits) {
+ int targetSubtask = split.splitIndex() % parallelism;
+ pendingSplits.computeIfAbsent(targetSubtask, ignored -> new
ArrayList<>()).add(split);
}
}
private void sendPendingSplitsToSourceReaders() {
+ Set<Integer> assignedReaders = new HashSet<>();
Iterator<Map.Entry<Integer, List<FlinkSourceSplit<T>>>> splitIter =
pendingSplits.entrySet().iterator();
while (splitIter.hasNext()) {
Map.Entry<Integer, List<FlinkSourceSplit<T>>> entry = splitIter.next();
- int readerIndex = entry.getKey();
- int targetSubtask = readerIndex % context.currentParallelism();
- if (context.registeredReaders().containsKey(targetSubtask)) {
- assignSplitsAndLog(entry.getValue(), targetSubtask);
+ int subtaskId = entry.getKey();
+ if (context.registeredReaders().containsKey(subtaskId)) {
+ assignSplitsAndLog(entry.getValue(), subtaskId);
+ assignedReaders.add(subtaskId);
splitIter.remove();
}
}
+
+ for (int subtaskId : context.registeredReaders().keySet()) {
+ if (!assignedReaders.contains(subtaskId) &&
!pendingSplits.containsKey(subtaskId)) {
+ context.signalNoMoreSplits(subtaskId);
+ }
+ }
}
private void assignSplitsAndLog(List<FlinkSourceSplit<T>> splits, int
subtaskId) {
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitUtils.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitUtils.java
new file mode 100644
index 00000000000..dda99bf53be
--- /dev/null
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitUtils.java
@@ -0,0 +1,84 @@
+/*
+ * 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.flink.translation.wrappers.streaming.io.source;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.beam.runners.flink.FlinkPipelineOptions;
+import org.apache.beam.sdk.io.BoundedSource;
+import org.apache.beam.sdk.io.FileBasedSource;
+import org.apache.beam.sdk.io.Source;
+import org.apache.beam.sdk.io.UnboundedSource;
+import org.apache.beam.sdk.options.PipelineOptions;
+
+/** Shared Beam source sizing and splitting helpers. */
+final class FlinkSourceSplitUtils {
+ static final long MEBIBYTE = 1024L * 1024L;
+
+ private FlinkSourceSplitUtils() {}
+
+ static <T> long estimateBoundedSourceSize(
+ BoundedSource<T> boundedSource, PipelineOptions pipelineOptions) throws
Exception {
+ return boundedSource.getEstimatedSizeBytes(pipelineOptions);
+ }
+
+ static <T> ArrayList<FlinkSourceSplit<T>> splitBoundedSource(
+ BoundedSource<T> boundedSource,
+ PipelineOptions pipelineOptions,
+ int numSplits,
+ long estimatedSizeBytes)
+ throws Exception {
+ long desiredSizeBytes =
+ getDesiredSizeBytes(boundedSource, pipelineOptions, numSplits,
estimatedSizeBytes);
+ return toFlinkSplits(boundedSource.split(desiredSizeBytes,
pipelineOptions));
+ }
+
+ static <T> ArrayList<FlinkSourceSplit<T>> splitUnboundedSource(
+ UnboundedSource<T, ?> unboundedSource, PipelineOptions pipelineOptions,
int numSplits)
+ throws Exception {
+ return toFlinkSplits(unboundedSource.split(numSplits, pipelineOptions));
+ }
+
+ static long getDesiredSizeBytes(
+ Source<?> beamSource,
+ PipelineOptions pipelineOptions,
+ int numSplits,
+ long estimatedSizeBytes) {
+ long desiredSizeBytes = estimatedSizeBytes / numSplits;
+
+ long maxSplitSizeMb =
+
pipelineOptions.as(FlinkPipelineOptions.class).getFileInputSplitMaxSizeMB();
+ if (beamSource instanceof FileBasedSource && maxSplitSizeMb > 0) {
+ return Math.min(desiredSizeBytes, mebibytesToBytes(maxSplitSizeMb));
+ }
+ return desiredSizeBytes;
+ }
+
+ static long mebibytesToBytes(long mebibytes) {
+ return mebibytes > Long.MAX_VALUE / MEBIBYTE ? Long.MAX_VALUE : mebibytes
* MEBIBYTE;
+ }
+
+ private static <T> ArrayList<FlinkSourceSplit<T>> toFlinkSplits(
+ List<? extends Source<T>> beamSplits) {
+ ArrayList<FlinkSourceSplit<T>> flinkSplits = new
ArrayList<>(beamSplits.size());
+ for (int i = 0; i < beamSplits.size(); i++) {
+ flinkSplits.add(new FlinkSourceSplit<>(i, beamSplits.get(i)));
+ }
+ return flinkSplits;
+ }
+}
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/LazyFlinkSourceSplitEnumerator.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/LazyFlinkSourceSplitEnumerator.java
index 94c14b2999b..bd07d00cd04 100644
---
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/LazyFlinkSourceSplitEnumerator.java
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/LazyFlinkSourceSplitEnumerator.java
@@ -19,123 +19,109 @@ package
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.CountDownLatch;
+import java.util.Optional;
import javax.annotation.Nullable;
-import org.apache.beam.runners.flink.FlinkPipelineOptions;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
import org.apache.beam.sdk.io.BoundedSource;
-import org.apache.beam.sdk.io.FileBasedSource;
import org.apache.beam.sdk.io.Source;
-import org.apache.beam.sdk.io.UnboundedSource;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.flink.api.connector.source.SplitEnumerator;
import org.apache.flink.api.connector.source.SplitEnumeratorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * A Flink {@link org.apache.flink.api.connector.source.SplitEnumerator
SplitEnumerator}
- * implementation that holds a Beam {@link Source} and does the following:
- *
- * <ul>
- * <li>Split the Beam {@link Source} to desired number of splits.
- * <li>Lazily assign the splits to the Flink Source Reader.
- * </ul>
- *
- * @param <T> The output type of the encapsulated Beam {@link Source}.
- */
+/** Splits a bounded Beam source and assigns one split for each reader
request. */
public class LazyFlinkSourceSplitEnumerator<T>
- implements SplitEnumerator<FlinkSourceSplit<T>, Map<Integer,
List<FlinkSourceSplit<T>>>> {
+ implements SplitEnumerator<FlinkSourceSplit<T>,
FlinkSourceEnumeratorState<T>> {
private static final Logger LOG =
LoggerFactory.getLogger(LazyFlinkSourceSplitEnumerator.class);
+
private final SplitEnumeratorContext<FlinkSourceSplit<T>> context;
private final Source<T> beamSource;
private final PipelineOptions pipelineOptions;
private final int numSplits;
private final List<FlinkSourceSplit<T>> pendingSplits;
- private volatile boolean splitsInitialized;
- private final CountDownLatch initializationLatch = new CountDownLatch(1);
+ private final Map<Integer, Optional<String>> pendingSplitRequests;
+
+ private boolean splitsInitialized;
+
+ public LazyFlinkSourceSplitEnumerator(
+ SplitEnumeratorContext<FlinkSourceSplit<T>> context,
+ Source<T> beamSource,
+ PipelineOptions pipelineOptions,
+ int numSplits) {
+ this(context, beamSource, pipelineOptions, numSplits, null);
+ }
public LazyFlinkSourceSplitEnumerator(
SplitEnumeratorContext<FlinkSourceSplit<T>> context,
Source<T> beamSource,
PipelineOptions pipelineOptions,
int numSplits,
- boolean splitInitialized) {
+ @Nullable FlinkSourceEnumeratorState<T> restoredState) {
this.context = context;
this.beamSource = beamSource;
this.pipelineOptions = pipelineOptions;
this.numSplits = numSplits;
this.pendingSplits = new ArrayList<>(numSplits);
- this.splitsInitialized = splitInitialized;
+ this.pendingSplitRequests = new LinkedHashMap<>();
+ this.splitsInitialized = restoredState != null;
+
+ if (restoredState != null) {
+ if (restoredState.getAssignmentMode() != AssignmentMode.LAZY) {
+ throw new IllegalArgumentException(
+ "Cannot restore the lazy source enumerator from "
+ + restoredState.getAssignmentMode()
+ + " state.");
+ }
+ pendingSplits.addAll(restoredState.getPendingSplits());
+ }
+
+ LOG.info(
+ "Created lazy source enumerator with parallelism {}, source {},
numSplits {}, "
+ + "initialized {}",
+ context.currentParallelism(),
+ beamSource,
+ numSplits,
+ splitsInitialized);
}
@Override
public void start() {
if (!splitsInitialized) {
initializeSplits();
+ } else {
+ sendPendingSplitRequests();
}
}
- public void initializeSplits() {
+ private void initializeSplits() {
context.callAsync(
- () -> {
- try {
- LOG.info("Starting source {}", beamSource);
- List<? extends Source<T>> beamSplitSourceList = splitBeamSource();
- int i = 0;
- for (Source<T> beamSplitSource : beamSplitSourceList) {
- pendingSplits.add(new FlinkSourceSplit<>(i, beamSplitSource));
- i++;
- }
- return pendingSplits;
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- initializationLatch.countDown();
- }
- },
+ this::splitBeamSource,
(sourceSplits, error) -> {
if (error != null) {
- pendingSplits.addAll(sourceSplits);
throw new RuntimeException("Failed to start source enumerator.",
error);
}
+ pendingSplits.addAll(sourceSplits);
splitsInitialized = true;
+ sendPendingSplitRequests();
});
}
@Override
- public void handleSplitRequest(int subtask, @Nullable String hostname) {
- if (!context.registeredReaders().containsKey(subtask)) {
- // reader failed between sending the request and now. skip this request.
+ public void handleSplitRequest(int subtaskId, @Nullable String
requesterHostname) {
+ if (!context.registeredReaders().containsKey(subtaskId)) {
return;
}
- if (LOG.isInfoEnabled()) {
- final String hostInfo =
- hostname == null ? "(no host locality info)" : "(on host '" +
hostname + "')";
- LOG.info("Subtask {} {} is requesting a file source split", subtask,
hostInfo);
- }
-
if (!splitsInitialized) {
- try {
- initializationLatch.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- LOG.warn("Interrupted while waiting for splits initialization", e);
- return;
- }
+ pendingSplitRequests.put(subtaskId,
Optional.ofNullable(requesterHostname));
+ return;
}
- if (!pendingSplits.isEmpty()) {
- final FlinkSourceSplit<T> split =
pendingSplits.remove(pendingSplits.size() - 1);
- context.assignSplit(split, subtask);
- LOG.info("Assigned split to subtask {} : {}", subtask, split);
- } else {
- context.signalNoMoreSplits(subtask);
- LOG.info("No more splits available for subtask {}", subtask);
- }
+ assignNextSplit(subtaskId, requesterHostname);
}
@Override
@@ -146,20 +132,14 @@ public class LazyFlinkSourceSplitEnumerator<T>
@Override
public void addReader(int subtaskId) {
- // this source is purely lazy-pull-based, nothing to do upon registration
+ // Readers request lazy splits when they are ready for work.
}
@Override
- public Map<Integer, List<FlinkSourceSplit<T>>> snapshotState(long
checkpointId) throws Exception {
+ public FlinkSourceEnumeratorState<T> snapshotState(long checkpointId) {
LOG.info("Taking snapshot for checkpoint {}", checkpointId);
- return snapshotState();
- }
-
- public Map<Integer, List<FlinkSourceSplit<T>>> snapshotState() throws
Exception {
- // For type compatibility reasons, we return a Map but we do not actually
care about the key
- Map<Integer, List<FlinkSourceSplit<T>>> state = new HashMap<>(1);
- state.put(1, pendingSplits);
- return state;
+ AssignmentMode mode = splitsInitialized ? AssignmentMode.LAZY :
AssignmentMode.UNDECIDED;
+ return new FlinkSourceEnumeratorState<>(mode, new
ArrayList<>(pendingSplits));
}
@Override
@@ -167,39 +147,37 @@ public class LazyFlinkSourceSplitEnumerator<T>
// NoOp
}
- private long getDesiredSizeBytes(int numSplits, BoundedSource<T>
boundedSource) throws Exception {
- long totalSize = boundedSource.getEstimatedSizeBytes(pipelineOptions);
- long defaultSplitSize = totalSize / numSplits;
- long maxSplitSize = 0;
- if (pipelineOptions != null) {
- maxSplitSize =
pipelineOptions.as(FlinkPipelineOptions.class).getFileInputSplitMaxSizeMB();
- }
- if (beamSource instanceof FileBasedSource && maxSplitSize > 0) {
- // Most of the time parallelism is < number of files in source.
- // Each file becomes a unique split which commonly create skew.
- // This limits the size of splits to reduce skew.
- return Math.min(defaultSplitSize, maxSplitSize * 1024 * 1024);
- } else {
- return defaultSplitSize;
+ private ArrayList<FlinkSourceSplit<T>> splitBeamSource() throws Exception {
+ if (!(beamSource instanceof BoundedSource)) {
+ throw new IllegalStateException("Lazy assignment requires a bounded
source.");
}
+ LOG.info("Starting source {}", beamSource);
+ BoundedSource<T> boundedSource = (BoundedSource<T>) beamSource;
+ long estimatedSizeBytes =
+ FlinkSourceSplitUtils.estimateBoundedSourceSize(boundedSource,
pipelineOptions);
+ return FlinkSourceSplitUtils.splitBoundedSource(
+ boundedSource, pipelineOptions, numSplits, estimatedSizeBytes);
}
- // -------------- Private helper methods ----------------------
- private List<? extends Source<T>> splitBeamSource() throws Exception {
- if (beamSource instanceof BoundedSource) {
- BoundedSource<T> boundedSource = (BoundedSource<T>) beamSource;
- long desiredSizeBytes = getDesiredSizeBytes(numSplits, boundedSource);
- List<? extends BoundedSource<T>> splits =
- ((BoundedSource<T>) beamSource).split(desiredSizeBytes,
pipelineOptions);
- LOG.info("Split bounded source {} in {} splits", beamSource,
splits.size());
- return splits;
- } else if (beamSource instanceof UnboundedSource) {
- List<? extends UnboundedSource<T, ?>> splits =
- ((UnboundedSource<T, ?>) beamSource).split(numSplits,
pipelineOptions);
- LOG.info("Split source {} to {} splits", beamSource, splits);
- return splits;
- } else {
- throw new IllegalStateException("Unknown source type " +
beamSource.getClass());
+ private void sendPendingSplitRequests() {
+ Map<Integer, Optional<String>> splitRequests = new
LinkedHashMap<>(pendingSplitRequests);
+ pendingSplitRequests.clear();
+ splitRequests.forEach(
+ (subtaskId, hostname) -> assignNextSplit(subtaskId,
hostname.orElse(null)));
+ }
+
+ private void assignNextSplit(int subtaskId, @Nullable String
requesterHostname) {
+ if (!context.registeredReaders().containsKey(subtaskId)) {
+ return;
+ }
+ if (pendingSplits.isEmpty()) {
+ context.signalNoMoreSplits(subtaskId);
+ LOG.info("No more splits available for subtask {}", subtaskId);
+ return;
}
+
+ FlinkSourceSplit<T> split = pendingSplits.remove(pendingSplits.size() - 1);
+ context.assignSplit(split, subtaskId);
+ LOG.info("Assigned split to subtask {} on host {}: {}", subtaskId,
requesterHostname, split);
}
}
diff --git
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/SizeBasedFlinkSourceSplitEnumerator.java
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/SizeBasedFlinkSourceSplitEnumerator.java
new file mode 100644
index 00000000000..78b0fb59f2c
--- /dev/null
+++
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/SizeBasedFlinkSourceSplitEnumerator.java
@@ -0,0 +1,205 @@
+/*
+ * 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.flink.translation.wrappers.streaming.io.source;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.beam.runners.flink.FlinkPipelineOptions;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
+import org.apache.beam.sdk.io.BoundedSource;
+import org.apache.beam.sdk.options.PipelineOptions;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Selects static or lazy assignment by estimated bounded-source size. */
+final class SizeBasedFlinkSourceSplitEnumerator<T>
+ implements SplitEnumerator<FlinkSourceSplit<T>,
FlinkSourceEnumeratorState<T>> {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SizeBasedFlinkSourceSplitEnumerator.class);
+
+ private final SplitEnumeratorContext<FlinkSourceSplit<T>> context;
+ private final BoundedSource<T> boundedSource;
+ private final PipelineOptions pipelineOptions;
+ private final int numSplits;
+ private final Map<Integer, Optional<String>> pendingSplitRequests;
+ private final List<ReturnedSplits<T>> returnedSplits;
+
+ private @Nullable SplitEnumerator<FlinkSourceSplit<T>,
FlinkSourceEnumeratorState<T>> delegate;
+
+ SizeBasedFlinkSourceSplitEnumerator(
+ SplitEnumeratorContext<FlinkSourceSplit<T>> context,
+ BoundedSource<T> boundedSource,
+ PipelineOptions pipelineOptions,
+ int numSplits) {
+ this.context = context;
+ this.boundedSource = boundedSource;
+ this.pipelineOptions = pipelineOptions;
+ this.numSplits = numSplits;
+ this.pendingSplitRequests = new LinkedHashMap<>();
+ this.returnedSplits = new ArrayList<>();
+ }
+
+ @Override
+ public void start() {
+ context.callAsync(
+ this::selectAndSplit,
+ (initialState, error) -> {
+ if (error != null) {
+ throw new RuntimeException("Failed to select a source split
assignment mode.", error);
+ }
+
+ SplitEnumerator<FlinkSourceSplit<T>, FlinkSourceEnumeratorState<T>>
selectedDelegate =
+ createDelegate(initialState);
+ delegate = selectedDelegate;
+ returnedSplits.forEach(
+ returned -> selectedDelegate.addSplitsBack(returned.splits,
returned.subtaskId));
+ returnedSplits.clear();
+ selectedDelegate.start();
+ pendingSplitRequests.forEach(
+ (subtaskId, hostname) ->
+ selectedDelegate.handleSplitRequest(subtaskId,
hostname.orElse(null)));
+ pendingSplitRequests.clear();
+ });
+ }
+
+ @Override
+ public void handleSplitRequest(int subtaskId, @Nullable String
requesterHostname) {
+ if (delegate == null) {
+ pendingSplitRequests.put(subtaskId,
Optional.ofNullable(requesterHostname));
+ } else {
+ delegate.handleSplitRequest(subtaskId, requesterHostname);
+ }
+ }
+
+ @Override
+ public void addSplitsBack(List<FlinkSourceSplit<T>> splits, int subtaskId) {
+ if (delegate == null) {
+ returnedSplits.add(new ReturnedSplits<>(new ArrayList<>(splits),
subtaskId));
+ } else {
+ delegate.addSplitsBack(splits, subtaskId);
+ }
+ }
+
+ @Override
+ public void addReader(int subtaskId) {
+ if (delegate != null) {
+ delegate.addReader(subtaskId);
+ }
+ }
+
+ @Override
+ public FlinkSourceEnumeratorState<T> snapshotState(long checkpointId) throws
Exception {
+ if (delegate == null) {
+ return new FlinkSourceEnumeratorState<>(AssignmentMode.UNDECIDED, new
ArrayList<>());
+ }
+ return delegate.snapshotState(checkpointId);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (delegate != null) {
+ delegate.close();
+ }
+ }
+
+ private FlinkSourceEnumeratorState<T> selectAndSplit() throws Exception {
+ long estimatedSizeBytes =
+ FlinkSourceSplitUtils.estimateBoundedSourceSize(boundedSource,
pipelineOptions);
+ AssignmentMode selectedMode = selectAssignmentMode(estimatedSizeBytes);
+ ArrayList<FlinkSourceSplit<T>> splits =
+ FlinkSourceSplitUtils.splitBoundedSource(
+ boundedSource, pipelineOptions, numSplits, estimatedSizeBytes);
+ LOG.info(
+ "Split bounded source {} into {} splits using {} assignment",
+ boundedSource,
+ splits.size(),
+ selectedMode);
+ return new FlinkSourceEnumeratorState<>(selectedMode, splits);
+ }
+
+ private AssignmentMode selectAssignmentMode(long estimatedSizeBytes) {
+ long thresholdMb =
+
pipelineOptions.as(FlinkPipelineOptions.class).getSourceStaticSplitThresholdMb();
+ if (thresholdMb <= 0) {
+ throw new IllegalArgumentException(
+ "Size-based source assignment requires a positive threshold, but
received "
+ + thresholdMb
+ + ".");
+ }
+ if (estimatedSizeBytes <= 0 || estimatedSizeBytes == Long.MAX_VALUE) {
+ LOG.info(
+ "Estimated size of bounded source {} is zero or unknown. Using lazy
split assignment.",
+ boundedSource);
+ return AssignmentMode.LAZY;
+ }
+
+ int sourceParallelism = context.currentParallelism();
+ if (sourceParallelism <= 0) {
+ throw new IllegalStateException(
+ "Source parallelism must be positive, but was " + sourceParallelism
+ ".");
+ }
+ long estimatedBytesPerReader = estimatedSizeBytes / sourceParallelism;
+ long thresholdBytes = FlinkSourceSplitUtils.mebibytesToBytes(thresholdMb);
+ AssignmentMode selectedMode =
+ estimatedBytesPerReader >= thresholdBytes ? AssignmentMode.LAZY :
AssignmentMode.STATIC;
+ LOG.info(
+ "Using {} split assignment for bounded source {}: estimated size {}
bytes, source "
+ + "parallelism {}, estimated bytes per reader {}, static
assignment threshold {} "
+ + "bytes",
+ selectedMode,
+ boundedSource,
+ estimatedSizeBytes,
+ sourceParallelism,
+ estimatedBytesPerReader,
+ thresholdBytes);
+ return selectedMode;
+ }
+
+ private SplitEnumerator<FlinkSourceSplit<T>, FlinkSourceEnumeratorState<T>>
createDelegate(
+ FlinkSourceEnumeratorState<T> initialState) {
+ if (initialState.getAssignmentMode() == AssignmentMode.LAZY) {
+ return new LazyFlinkSourceSplitEnumerator<>(
+ context, boundedSource, pipelineOptions, numSplits, initialState);
+ }
+ if (initialState.getAssignmentMode() == AssignmentMode.STATIC) {
+ return new FlinkSourceSplitEnumerator<>(
+ context, boundedSource, pipelineOptions, numSplits, initialState);
+ }
+ throw new IllegalArgumentException(
+ "Cannot create a source enumerator for "
+ + initialState.getAssignmentMode()
+ + " assignment.");
+ }
+
+ private static final class ReturnedSplits<T> {
+ private final List<FlinkSourceSplit<T>> splits;
+ private final int subtaskId;
+
+ private ReturnedSplits(List<FlinkSourceSplit<T>> splits, int subtaskId) {
+ this.splits = splits;
+ this.subtaskId = subtaskId;
+ }
+ }
+}
diff --git
a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumeratorTest.java
b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumeratorTest.java
index e911ee72db5..c4d220d2213 100644
---
a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumeratorTest.java
+++
b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/source/FlinkSourceSplitEnumeratorTest.java
@@ -18,23 +18,303 @@
package org.apache.beam.runners.flink.translation.wrappers.streaming.io.source;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.beam.runners.core.construction.SerializablePipelineOptions;
import org.apache.beam.runners.flink.FlinkPipelineOptions;
+import org.apache.beam.runners.flink.translation.utils.SerdeUtils;
import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.TestBoundedCountingSource;
import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.TestCountingSource;
+import
org.apache.beam.runners.flink.translation.wrappers.streaming.io.source.FlinkSourceEnumeratorState.AssignmentMode;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.StringUtf8Coder;
+import org.apache.beam.sdk.io.BoundedSource;
import org.apache.beam.sdk.io.Source;
+import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.values.KV;
+import org.apache.flink.api.connector.source.SplitEnumerator;
import
org.apache.flink.connector.testutils.source.reader.TestingSplitEnumeratorContext;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
import org.junit.Test;
-/** Unit tests for {@link FlinkSourceSplitEnumerator}. */
+/** Unit tests for the Flink source split enumerators. */
public class FlinkSourceSplitEnumeratorTest {
+ private static final long MEBIBYTE = 1024L * 1024L;
+ private static final long STATIC_SPLIT_THRESHOLD_MB = 6144L;
+ private static final int SOURCE_PARALLELISM = 2;
+ private static final int REQUESTED_SPLITS = 4;
+
+ @Test
+ public void testSelectsAssignmentModeFromEstimatedSizeAndConfiguration()
throws Exception {
+ assertEquals(0L, (long)
FlinkPipelineOptions.defaults().getSourceStaticSplitThresholdMb());
+
+ long thresholdBytes = STATIC_SPLIT_THRESHOLD_MB * MEBIBYTE;
+ long largeEstimate = 1024L * MEBIBYTE;
+ long[] estimatedSizes = {
+ SOURCE_PARALLELISM * thresholdBytes - 1L,
+ SOURCE_PARALLELISM * thresholdBytes,
+ -1L,
+ 0L,
+ Long.MAX_VALUE,
+ largeEstimate,
+ largeEstimate,
+ largeEstimate,
+ largeEstimate
+ };
+ long[] configuredThresholds = {
+ STATIC_SPLIT_THRESHOLD_MB,
+ STATIC_SPLIT_THRESHOLD_MB,
+ STATIC_SPLIT_THRESHOLD_MB,
+ STATIC_SPLIT_THRESHOLD_MB,
+ STATIC_SPLIT_THRESHOLD_MB,
+ -1L,
+ -100L,
+ 0L,
+ Long.MAX_VALUE
+ };
+ AssignmentMode[] expectedModes = {
+ AssignmentMode.STATIC,
+ AssignmentMode.LAZY,
+ AssignmentMode.LAZY,
+ AssignmentMode.LAZY,
+ AssignmentMode.LAZY,
+ AssignmentMode.STATIC,
+ AssignmentMode.STATIC,
+ AssignmentMode.LAZY,
+ AssignmentMode.STATIC
+ };
+
+ for (int i = 0; i < estimatedSizes.length; i++) {
+ assertAssignmentMode(
+ estimatedSizes[i], configuredThresholds[i], expectedModes[i],
REQUESTED_SPLITS);
+ }
+ }
+
+ @Test
+ public void testSizeBasedSelectionReplaysLazyRequestAfterInitialization()
throws Exception {
+ FlinkPipelineOptions options = thresholdOptions();
+ long estimatedSizeBytes = SOURCE_PARALLELISM * STATIC_SPLIT_THRESHOLD_MB *
MEBIBYTE;
+ TestEstimatedSizeBoundedSource testSource =
+ TestEstimatedSizeBoundedSource.create(estimatedSizeBytes,
REQUESTED_SPLITS);
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> context =
+ new TestingSplitEnumeratorContext<>(SOURCE_PARALLELISM);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, REQUESTED_SPLITS);
+
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> enumerator =
+ flinkSource.createEnumerator(context)) {
+ enumerator.start();
+ context.registerReader(0, "reader-0");
+ enumerator.addReader(0);
+ enumerator.handleSplitRequest(0, "reader-0");
+
+ context.getExecutorService().triggerAll();
+
+ assertEquals(1,
context.getSplitAssignments().get(0).getAssignedSplits().size());
+ assertEquals(AssignmentMode.LAZY,
enumerator.snapshotState(1L).getAssignmentMode());
+ }
+ }
+
+ @Test
+ public void testSizeBasedSelectionAssignsStaticSplitsToEarlyReaders() throws
Exception {
+ FlinkPipelineOptions options = thresholdOptions();
+ long estimatedSizeBytes = SOURCE_PARALLELISM * STATIC_SPLIT_THRESHOLD_MB *
MEBIBYTE - 1L;
+ TestEstimatedSizeBoundedSource testSource =
+ TestEstimatedSizeBoundedSource.create(estimatedSizeBytes,
REQUESTED_SPLITS);
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> context =
+ new TestingSplitEnumeratorContext<>(SOURCE_PARALLELISM);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, REQUESTED_SPLITS);
+
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> enumerator =
+ flinkSource.createEnumerator(context)) {
+ enumerator.start();
+ for (int subtaskId = 0; subtaskId < SOURCE_PARALLELISM; subtaskId++) {
+ context.registerReader(subtaskId, "reader-" + subtaskId);
+ enumerator.addReader(subtaskId);
+ }
+
+ context.getExecutorService().triggerAll();
+
+ assertEquals(REQUESTED_SPLITS, countAssignedSplits(context));
+ context
+ .getSplitAssignments()
+ .values()
+ .forEach(state -> assertTrue(state.hasReceivedNoMoreSplitsSignal()));
+ assertEquals(AssignmentMode.STATIC,
enumerator.snapshotState(1L).getAssignmentMode());
+ }
+ }
+
+ @Test
+ public void testRestoreKeepsLazyAssignmentAcrossRescaleWithoutEstimating()
throws Exception {
+ final int initialParallelism = 4;
+ final int restoredParallelism = 1;
+ final int generatedSplits = 4;
+ FlinkPipelineOptions options = thresholdOptions();
+ long thresholdBytes = STATIC_SPLIT_THRESHOLD_MB * MEBIBYTE;
+ long estimatedSizeBytes = initialParallelism * thresholdBytes;
+ TestEstimatedSizeBoundedSource testSource =
+ TestEstimatedSizeBoundedSource.create(estimatedSizeBytes,
generatedSplits);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, generatedSplits);
+ FlinkSourceEnumeratorState<String> checkpoint;
+
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> initialContext =
+ new TestingSplitEnumeratorContext<>(initialParallelism);
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> enumerator =
+ flinkSource.createEnumerator(initialContext)) {
+ enumerator.start();
+ initialContext.getExecutorService().triggerAll();
+ checkpoint = roundTripState(flinkSource, enumerator.snapshotState(1L));
+ assertEquals(AssignmentMode.LAZY, checkpoint.getAssignmentMode());
+ assertEquals(generatedSplits, checkpoint.getPendingSplits().size());
+ }
+
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> restoredContext =
+ new TestingSplitEnumeratorContext<>(restoredParallelism);
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> restored =
+ flinkSource.restoreEnumerator(restoredContext, checkpoint)) {
+ restored.start();
+ restoredContext.registerReader(0, "reader-0");
+ restored.addReader(0);
+ for (int i = 0; i < generatedSplits; i++) {
+ restored.handleSplitRequest(0, "reader-0");
+ }
+ restored.handleSplitRequest(0, "reader-0");
+
+ assertEquals(
+ generatedSplits,
restoredContext.getSplitAssignments().get(0).getAssignedSplits().size());
+
assertTrue(restoredContext.getSplitAssignments().get(0).hasReceivedNoMoreSplitsSignal());
+ assertEquals(
+ "restoring a decided strategy must not estimate the source again",
+ 1,
+ testSource.getEstimationCalls());
+ assertEquals(AssignmentMode.LAZY,
restored.snapshotState(2L).getAssignmentMode());
+ }
+ }
+
+ @Test
+ public void testLegacyLazyCheckpointMapUpgradesToStrategyNeutralState()
throws Exception {
+ FlinkPipelineOptions options = FlinkPipelineOptions.defaults();
+ TestEstimatedSizeBoundedSource testSource =
TestEstimatedSizeBoundedSource.create(1L, 1);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, 1);
+ FlinkSourceSplit<String> pendingSplit = new FlinkSourceSplit<>(0,
testSource);
+ Map<Integer, List<FlinkSourceSplit<String>>> legacyCheckpoint =
+ Collections.singletonMap(1, Collections.singletonList(pendingSplit));
+ byte[] serialized = SerdeUtils.serializeObject(legacyCheckpoint);
+
+ FlinkSourceEnumeratorState<String> upgraded =
+ flinkSource.getEnumeratorCheckpointSerializer().deserialize(0,
serialized);
+
+ assertEquals(AssignmentMode.LAZY, upgraded.getAssignmentMode());
+ assertEquals(1, upgraded.getPendingSplits().size());
+ assertEquals(0, upgraded.getPendingSplits().get(0).splitIndex());
+ }
+
+ @Test
+ public void testLegacyStaticCheckpointMapKeepsSplitsOnTheirOriginalReaders()
throws Exception {
+ final int parallelism = 2;
+ TestEstimatedSizeBoundedSource testSource =
TestEstimatedSizeBoundedSource.create(1L, 1);
+ Map<Integer, List<FlinkSourceSplit<String>>> legacyCheckpoint = new
HashMap<>();
+ legacyCheckpoint.put(
+ 0,
+ Arrays.asList(
+ new FlinkSourceSplit<>(0, testSource), new FlinkSourceSplit<>(2,
testSource)));
+ legacyCheckpoint.put(1, Collections.singletonList(new
FlinkSourceSplit<>(1, testSource)));
+ byte[] serialized = SerdeUtils.serializeObject(legacyCheckpoint);
+
+ FlinkSourceEnumeratorState<String> upgraded =
+ new FlinkSourceEnumeratorStateSerializer<String>(AssignmentMode.STATIC)
+ .deserialize(0, serialized);
+ assertEquals(AssignmentMode.STATIC, upgraded.getAssignmentMode());
+ assertEquals(3, upgraded.getPendingSplits().size());
+
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> restoredContext =
+ new TestingSplitEnumeratorContext<>(parallelism);
+ try (FlinkSourceSplitEnumerator<String> restored =
+ new FlinkSourceSplitEnumerator<>(
+ restoredContext, testSource, staticOptions(), 3, upgraded)) {
+ restored.start();
+ for (int subtaskId = 0; subtaskId < parallelism; subtaskId++) {
+ restoredContext.registerReader(subtaskId, "reader-" + subtaskId);
+ restored.addReader(subtaskId);
+ }
+
+ assertEquals(Arrays.asList(0, 2),
assignedSplitIndexesForSubtask(restoredContext, 0));
+ assertEquals(
+ Collections.singletonList(1),
assignedSplitIndexesForSubtask(restoredContext, 1));
+ }
+ }
+
+ @Test
+ public void testSerializerRejectsUnknownVersionsAndUnexpectedPayloads()
throws Exception {
+ FlinkSourceEnumeratorStateSerializer<String> serializer =
+ new FlinkSourceEnumeratorStateSerializer<>(AssignmentMode.LAZY);
+ byte[] legacyMapBytes =
+ SerdeUtils.serializeObject(Collections.singletonMap(1, new
ArrayList<>()));
+ byte[] stateBytes =
+ serializer.serialize(
+ new FlinkSourceEnumeratorState<>(AssignmentMode.LAZY, new
ArrayList<>()));
+
+ // The payload type must match the version it was written with.
+ assertThrows(IOException.class, () -> serializer.deserialize(1,
legacyMapBytes));
+ assertThrows(IOException.class, () -> serializer.deserialize(0,
stateBytes));
+ assertThrows(IOException.class, () -> serializer.deserialize(2,
stateBytes));
+ }
+
+ @Test
+ public void
testRestoreRepartitionsStaticSplitsForNewParallelismWithoutEstimating()
+ throws Exception {
+ final int initialParallelism = 2;
+ final int restoredParallelism = 3;
+ final int generatedSplits = 5;
+ FlinkPipelineOptions options = thresholdOptions();
+ long thresholdBytes = STATIC_SPLIT_THRESHOLD_MB * MEBIBYTE;
+ long estimatedSizeBytes = initialParallelism * thresholdBytes - 1L;
+ TestEstimatedSizeBoundedSource testSource =
+ TestEstimatedSizeBoundedSource.create(estimatedSizeBytes,
generatedSplits);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, generatedSplits);
+ FlinkSourceEnumeratorState<String> checkpoint;
+
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> initialContext =
+ new TestingSplitEnumeratorContext<>(initialParallelism);
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> enumerator =
+ flinkSource.createEnumerator(initialContext)) {
+ enumerator.start();
+ initialContext.getExecutorService().triggerAll();
+ checkpoint = roundTripState(flinkSource, enumerator.snapshotState(1L));
+ assertEquals(AssignmentMode.STATIC, checkpoint.getAssignmentMode());
+ assertEquals(generatedSplits, checkpoint.getPendingSplits().size());
+ }
+
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> restoredContext =
+ new TestingSplitEnumeratorContext<>(restoredParallelism);
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> restored =
+ flinkSource.restoreEnumerator(restoredContext, checkpoint)) {
+ restored.start();
+ for (int subtaskId = 0; subtaskId < restoredParallelism; subtaskId++) {
+ restoredContext.registerReader(subtaskId, "reader-" + subtaskId);
+ restored.addReader(subtaskId);
+ }
+
+ assertEquals(generatedSplits, countAssignedSplits(restoredContext));
+ assertEquals(2,
restoredContext.getSplitAssignments().get(0).getAssignedSplits().size());
+ assertEquals(2,
restoredContext.getSplitAssignments().get(1).getAssignedSplits().size());
+ assertEquals(1,
restoredContext.getSplitAssignments().get(2).getAssignedSplits().size());
+ restoredContext
+ .getSplitAssignments()
+ .values()
+ .forEach(state -> assertTrue(state.hasReceivedNoMoreSplitsSignal()));
+ assertEquals(1, testSource.getEstimationCalls());
+ }
+ }
@Test
public void testAssignSplitsWithBoundedSource() throws IOException {
@@ -54,13 +334,8 @@ public class FlinkSourceSplitEnumeratorTest {
.forEach(
(subtaskId, state) -> {
int expectedNumSplitsPerSubtask = numSplits / numSubtasks;
- assertEquals(
- "Each subtask should have " + expectedNumSplitsPerSubtask +
" assigned splits",
- expectedNumSplitsPerSubtask,
- state.getAssignedSplits().size());
- assertTrue(
- "Each subtask should have received NoMoreSplits",
- state.hasReceivedNoMoreSplitsSignal());
+ assertEquals(expectedNumSplitsPerSubtask,
state.getAssignedSplits().size());
+ assertTrue(state.hasReceivedNoMoreSplitsSignal());
state
.getAssignedSplits()
.forEach(
@@ -72,8 +347,8 @@ public class FlinkSourceSplitEnumeratorTest {
assertEquals(
expectedSplitSize,
source.getEstimatedSizeBytes(FlinkPipelineOptions.defaults()));
- } catch (Exception e) {
- fail("Received exception" + e);
+ } catch (Exception error) {
+ fail("Received exception " + error);
}
});
});
@@ -88,90 +363,79 @@ public class FlinkSourceSplitEnumeratorTest {
new TestingSplitEnumeratorContext<>(numSubtasks);
TestCountingSource testSource = new TestCountingSource(numRecordsPerSplit);
- assignSplits(testContext, testSource, numSplits);
+ try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> enumerator =
+ new FlinkSourceSplitEnumerator<>(
+ testContext, testSource, FlinkPipelineOptions.defaults(),
numSplits)) {
+ enumerator.start();
+ for (int subtaskId = 0; subtaskId < numSubtasks; subtaskId++) {
+ testContext.registerReader(subtaskId, String.valueOf(subtaskId));
+ enumerator.addReader(subtaskId);
+ }
+ testContext.getExecutorService().triggerAll();
+ }
testContext
.getSplitAssignments()
.forEach(
(subtaskId, state) -> {
- int expectedNumSplitsPerSubtask = numSplits / numSubtasks;
- assertEquals(
- "Each subtask should have " + expectedNumSplitsPerSubtask +
" assigned splits",
- expectedNumSplitsPerSubtask,
- state.getAssignedSplits().size());
- assertTrue(
- "Each subtask should have received NoMoreSplits",
- state.hasReceivedNoMoreSplitsSignal());
+ assertEquals(numSplits / numSubtasks,
state.getAssignedSplits().size());
+ assertTrue(state.hasReceivedNoMoreSplitsSignal());
});
}
@Test
- public void testAddSplitsBack() throws IOException {
+ public void testAddSplitsBackToStaticReader() throws IOException {
final int numSubtasks = 2;
final int numSplits = 10;
- final int totalNumRecords = 10;
TestingSplitEnumeratorContext<FlinkSourceSplit<KV<Integer, Integer>>>
testContext =
new TestingSplitEnumeratorContext<>(numSubtasks);
- TestBoundedCountingSource testSource =
- new TestBoundedCountingSource(numSplits, totalNumRecords);
- try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> splitEnumerator =
- new FlinkSourceSplitEnumerator<>(
- testContext, testSource, FlinkPipelineOptions.defaults(),
numSplits)) {
- splitEnumerator.start();
+ TestBoundedCountingSource testSource = new
TestBoundedCountingSource(numSplits, numSplits);
+
+ try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> enumerator =
+ new FlinkSourceSplitEnumerator<>(testContext, testSource,
staticOptions(), numSplits)) {
+ enumerator.start();
testContext.registerReader(0, "0");
- splitEnumerator.addReader(0);
+ enumerator.addReader(0);
testContext.getExecutorService().triggerAll();
- List<FlinkSourceSplit<KV<Integer, Integer>>> splitsForReader =
- testContext.getSplitAssignments().get(0).getAssignedSplits();
- assertEquals(numSplits / numSubtasks, splitsForReader.size());
+ List<FlinkSourceSplit<KV<Integer, Integer>>> returnedSplits =
+ new
ArrayList<>(testContext.getSplitAssignments().get(0).getAssignedSplits());
+ assertEquals(numSplits / numSubtasks, returnedSplits.size());
- splitEnumerator.addSplitsBack(splitsForReader, 0);
- splitEnumerator.addReader(0);
- assertEquals(2 * numSplits / numSubtasks, splitsForReader.size());
+ enumerator.addSplitsBack(returnedSplits, 0);
+ enumerator.addReader(0);
+ assertEquals(
+ 2 * numSplits / numSubtasks,
+ testContext.getSplitAssignments().get(0).getAssignedSplits().size());
}
}
- @Test
- public void testAddSplitsBackAfterRescale() throws Exception {
- final int numSubtasks = 2;
- final int numSplits = 10;
- final int totalNumRecords = 10;
- TestingSplitEnumeratorContext<FlinkSourceSplit<KV<Integer, Integer>>>
testContext =
- new TestingSplitEnumeratorContext<>(numSubtasks);
- TestBoundedCountingSource testSource =
- new TestBoundedCountingSource(numSplits, totalNumRecords);
- final Map<Integer, List<FlinkSourceSplit<KV<Integer, Integer>>>>
assignment;
- try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> splitEnumerator =
- new FlinkSourceSplitEnumerator<>(
- testContext, testSource, FlinkPipelineOptions.defaults(),
numSplits)) {
- splitEnumerator.start();
- for (int i = 0; i < numSubtasks; i++) {
- testContext.registerReader(i, String.valueOf(i));
- splitEnumerator.addReader(i);
- }
- testContext.getExecutorService().triggerAll();
- assignment =
- testContext.getSplitAssignments().entrySet().stream()
- .map(e -> KV.of(e.getKey(), e.getValue().getAssignedSplits()))
- .collect(Collectors.toMap(KV::getKey, KV::getValue));
- }
+ private void assertAssignmentMode(
+ long estimatedSizeBytes,
+ long configuredThresholdMb,
+ AssignmentMode expectedMode,
+ int generatedSplits)
+ throws Exception {
+ FlinkPipelineOptions options = FlinkPipelineOptions.defaults();
+ options.setSourceStaticSplitThresholdMb(configuredThresholdMb);
+ TestEstimatedSizeBoundedSource testSource =
+ TestEstimatedSizeBoundedSource.create(estimatedSizeBytes,
generatedSplits);
+ TestingSplitEnumeratorContext<FlinkSourceSplit<String>> context =
+ new TestingSplitEnumeratorContext<>(SOURCE_PARALLELISM);
+ FlinkSource<String, ?> flinkSource = createBoundedSource(testSource,
options, generatedSplits);
- // add tasks back
- testContext = new TestingSplitEnumeratorContext<>(numSubtasks);
- try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> splitEnumerator =
- new FlinkSourceSplitEnumerator<>(
- testContext, testSource, FlinkPipelineOptions.defaults(),
numSplits, true)) {
- splitEnumerator.start();
- assignment.forEach(
- (splitId, assignedSplits) ->
splitEnumerator.addSplitsBack(assignedSplits, splitId));
- testContext.registerReader(0, "0");
- splitEnumerator.addReader(0);
- testContext.getExecutorService().triggerAll();
-
- List<FlinkSourceSplit<KV<Integer, Integer>>> splitsForReader =
- testContext.getSplitAssignments().get(0).getAssignedSplits();
- assertEquals(numSplits / numSubtasks, splitsForReader.size());
+ try (SplitEnumerator<FlinkSourceSplit<String>,
FlinkSourceEnumeratorState<String>> enumerator =
+ flinkSource.createEnumerator(context)) {
+ if (configuredThresholdMb < 0) {
+ assertTrue(enumerator instanceof FlinkSourceSplitEnumerator);
+ } else if (configuredThresholdMb == 0) {
+ assertTrue(enumerator instanceof LazyFlinkSourceSplitEnumerator);
+ } else {
+ assertTrue(enumerator instanceof SizeBasedFlinkSourceSplitEnumerator);
+ }
+ enumerator.start();
+ context.getExecutorService().triggerAll();
+ assertEquals(expectedMode,
enumerator.snapshotState(1L).getAssignmentMode());
}
}
@@ -180,17 +444,107 @@ public class FlinkSourceSplitEnumeratorTest {
Source<KV<Integer, Integer>> source,
int numSplits)
throws IOException {
- try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> splitEnumerator =
- new FlinkSourceSplitEnumerator<>(
- context, source, FlinkPipelineOptions.defaults(), numSplits)) {
- splitEnumerator.start();
- // Add a reader before splitting the beam source.
- context.registerReader(0, "0");
- splitEnumerator.addReader(0);
+ try (FlinkSourceSplitEnumerator<KV<Integer, Integer>> enumerator =
+ new FlinkSourceSplitEnumerator<>(context, source, staticOptions(),
numSplits)) {
+ enumerator.start();
+ for (int subtaskId = 0; subtaskId < context.currentParallelism();
subtaskId++) {
+ context.registerReader(subtaskId, String.valueOf(subtaskId));
+ enumerator.addReader(subtaskId);
+ }
context.getExecutorService().triggerAll();
- context.registerReader(1, "1");
- // Add another reader after splitting the beam source.
- splitEnumerator.addReader(1);
+ }
+ }
+
+ private static FlinkPipelineOptions staticOptions() {
+ FlinkPipelineOptions options = FlinkPipelineOptions.defaults();
+ options.setSourceStaticSplitThresholdMb(-1L);
+ return options;
+ }
+
+ private static FlinkPipelineOptions thresholdOptions() {
+ FlinkPipelineOptions options = FlinkPipelineOptions.defaults();
+ options.setSourceStaticSplitThresholdMb(STATIC_SPLIT_THRESHOLD_MB);
+ return options;
+ }
+
+ private static FlinkSource<String, ?> createBoundedSource(
+ BoundedSource<String> source, FlinkPipelineOptions options, int
numSplits) {
+ return FlinkSource.bounded(
+ "test-bounded-source", source, new
SerializablePipelineOptions(options), numSplits);
+ }
+
+ private static FlinkSourceEnumeratorState<String> roundTripState(
+ FlinkSource<String, ?> source, FlinkSourceEnumeratorState<String> state)
throws IOException {
+ SimpleVersionedSerializer<FlinkSourceEnumeratorState<String>> serializer =
+ source.getEnumeratorCheckpointSerializer();
+ byte[] serialized = serializer.serialize(state);
+ return serializer.deserialize(serializer.getVersion(), serialized);
+ }
+
+ private static <T> int countAssignedSplits(
+ TestingSplitEnumeratorContext<FlinkSourceSplit<T>> context) {
+ return context.getSplitAssignments().values().stream()
+ .mapToInt(state -> state.getAssignedSplits().size())
+ .sum();
+ }
+
+ private static <T> List<Integer> assignedSplitIndexesForSubtask(
+ TestingSplitEnumeratorContext<FlinkSourceSplit<T>> context, int
subtaskId) {
+ List<Integer> splitIndexes = new ArrayList<>();
+ for (FlinkSourceSplit<T> split :
+ context.getSplitAssignments().get(subtaskId).getAssignedSplits()) {
+ splitIndexes.add(split.splitIndex());
+ }
+ Collections.sort(splitIndexes);
+ return splitIndexes;
+ }
+
+ private static final class TestEstimatedSizeBoundedSource extends
BoundedSource<String> {
+ private final long estimatedSizeBytes;
+ private final int generatedSplits;
+ private final AtomicInteger estimationCalls;
+
+ private TestEstimatedSizeBoundedSource(
+ long estimatedSizeBytes, int generatedSplits, AtomicInteger
estimationCalls) {
+ this.estimatedSizeBytes = estimatedSizeBytes;
+ this.generatedSplits = generatedSplits;
+ this.estimationCalls = estimationCalls;
+ }
+
+ private static TestEstimatedSizeBoundedSource create(
+ long estimatedSizeBytes, int generatedSplits) {
+ return new TestEstimatedSizeBoundedSource(
+ estimatedSizeBytes, generatedSplits, new AtomicInteger());
+ }
+
+ @Override
+ public List<? extends BoundedSource<String>> split(
+ long desiredBundleSizeBytes, PipelineOptions options) {
+ List<TestEstimatedSizeBoundedSource> splits = new
ArrayList<>(generatedSplits);
+ for (int i = 0; i < generatedSplits; i++) {
+ splits.add(new TestEstimatedSizeBoundedSource(1L, 1, estimationCalls));
+ }
+ return splits;
+ }
+
+ @Override
+ public long getEstimatedSizeBytes(PipelineOptions options) {
+ estimationCalls.incrementAndGet();
+ return estimatedSizeBytes;
+ }
+
+ @Override
+ public BoundedReader<String> createReader(PipelineOptions options) {
+ throw new UnsupportedOperationException("This source is only used to
test split assignment");
+ }
+
+ @Override
+ public Coder<String> getOutputCoder() {
+ return StringUtf8Coder.of();
+ }
+
+ private int getEstimationCalls() {
+ return estimationCalls.get();
}
}
}
diff --git
a/website/www/site/layouts/shortcodes/flink_java_pipeline_options.html
b/website/www/site/layouts/shortcodes/flink_java_pipeline_options.html
index 34d6c524377..8e0fc007f13 100644
--- a/website/www/site/layouts/shortcodes/flink_java_pipeline_options.html
+++ b/website/www/site/layouts/shortcodes/flink_java_pipeline_options.html
@@ -187,6 +187,11 @@ Should be called before running the tests.
<td>Shuts down sources which have been idle for the configured time of
milliseconds. Once a source has been shut down, checkpointing is not possible
anymore. Shutting down the sources eventually leads to pipeline shutdown
(=Flink job finishes) once all input has been processed. Unless explicitly set,
this will default to Long.MAX_VALUE when checkpointing is enabled and to 0 when
checkpointing is disabled. See https://issues.apache.org/jira/browse/FLINK-2491
for progress on this issue.</td>
<td>Default: <code>-1</code></td>
</tr>
+<tr>
+ <td><code>sourceStaticSplitThresholdMb</code></td>
+ <td>Static split assignment threshold in MiB per source reader for bounded
sources in Flink DataStream mode. The default of 0 always uses lazy assignment.
A positive value selects static round-robin assignment for sources with a
known, positive estimate below the threshold and lazy assignment otherwise. Any
negative value always uses static assignment.</td>
+ <td>Default: <code>0</code></td>
+</tr>
<tr>
<td><code>stateBackend</code></td>
<td>State backend to store Beam's state. Use 'rocksdb' or 'hashmap' (same as
'filesystem').</td>
diff --git
a/website/www/site/layouts/shortcodes/flink_python_pipeline_options.html
b/website/www/site/layouts/shortcodes/flink_python_pipeline_options.html
index e3fe24216a5..d2ab687c42b 100644
--- a/website/www/site/layouts/shortcodes/flink_python_pipeline_options.html
+++ b/website/www/site/layouts/shortcodes/flink_python_pipeline_options.html
@@ -187,6 +187,11 @@ Should be called before running the tests.
<td>Shuts down sources which have been idle for the configured time of
milliseconds. Once a source has been shut down, checkpointing is not possible
anymore. Shutting down the sources eventually leads to pipeline shutdown
(=Flink job finishes) once all input has been processed. Unless explicitly set,
this will default to Long.MAX_VALUE when checkpointing is enabled and to 0 when
checkpointing is disabled. See https://issues.apache.org/jira/browse/FLINK-2491
for progress on this issue.</td>
<td>Default: <code>-1</code></td>
</tr>
+<tr>
+ <td><code>source_static_split_threshold_mb</code></td>
+ <td>Static split assignment threshold in MiB per source reader for bounded
sources in Flink DataStream mode. The default of 0 always uses lazy assignment.
A positive value selects static round-robin assignment for sources with a
known, positive estimate below the threshold and lazy assignment otherwise. Any
negative value always uses static assignment.</td>
+ <td>Default: <code>0</code></td>
+</tr>
<tr>
<td><code>state_backend</code></td>
<td>State backend to store Beam's state. Use 'rocksdb' or 'hashmap' (same as
'filesystem').</td>