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 355e8bd48b5 Expose desiredNumUnboundedSourceSplits for Java and Python
portable pipeline (#40345)
355e8bd48b5 is described below
commit 355e8bd48b52fec351fd9067630030789dbdf09f
Author: Yi Hu <[email protected]>
AuthorDate: Thu Oct 1 12:49:05 2026 -0400
Expose desiredNumUnboundedSourceSplits for Java and Python portable
pipeline (#40345)
- Port desiredNumUnboundedSourceSplits currently honored by Dataflow
non-portable
pipeline submission to Java portable (UnboundedSource translated to SDF
wrapper)
and Python SDK
---
.../src/main/java/org/apache/beam/sdk/io/Read.java | 12 +++++++-
.../apache/beam/sdk/options/StreamingOptions.java | 10 ++++++
.../test/java/org/apache/beam/sdk/io/ReadTest.java | 36 ++++++++++++++++++++++
sdks/python/apache_beam/io/unbounded_source.py | 13 ++++++--
.../python/apache_beam/io/unbounded_source_test.py | 21 +++++++++++++
.../python/apache_beam/options/pipeline_options.py | 5 +++
6 files changed, 94 insertions(+), 3 deletions(-)
diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/Read.java
b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/Read.java
index 43920abb737..b4b575c881d 100644
--- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/Read.java
+++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/Read.java
@@ -43,6 +43,7 @@ import
org.apache.beam.sdk.io.UnboundedSource.CheckpointMark.NoopCheckpointMark;
import org.apache.beam.sdk.io.UnboundedSource.UnboundedReader;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
+import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.transforms.Deduplicate;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.DoFn.UnboundedPerElement;
@@ -550,9 +551,18 @@ public class Read {
receiver.output(restriction);
}
+ int desiredNumSplits = DEFAULT_DESIRED_NUM_SPLITS;
+ if (pipelineOptions != null) {
+ int configuredSplits =
+
pipelineOptions.as(StreamingOptions.class).getDesiredNumUnboundedSourceSplits();
+ if (configuredSplits > 0) {
+ desiredNumSplits = configuredSplits;
+ }
+ }
+
try {
for (UnboundedSource<OutputT, CheckpointT> split :
- restriction.getSource().split(DEFAULT_DESIRED_NUM_SPLITS,
pipelineOptions)) {
+ restriction.getSource().split(desiredNumSplits, pipelineOptions)) {
receiver.output(
UnboundedSourceRestriction.create(split, null,
restriction.getWatermark()));
}
diff --git
a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java
b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java
index 53f5a1870e4..250c871a735 100644
---
a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java
+++
b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java
@@ -45,6 +45,16 @@ public interface StreamingOptions extends
ApplicationNameOptions, PipelineOption
void setUpdateCompatibilityVersion(@Nullable String
updateCompatibilityVersion);
+ /**
+ * The desired number of initial splits for UnboundedSources. If this value
is <=0, a default
+ * number of splits will be chosen.
+ */
+ @Description("The desired number of initial splits for UnboundedSources.")
+ @Default.Integer(0)
+ int getDesiredNumUnboundedSourceSplits();
+
+ void setDesiredNumUnboundedSourceSplits(int value);
+
static boolean updateCompatibilityVersionLessThan(PipelineOptions options,
String version) {
if (options == null) {
return false;
diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/ReadTest.java
b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/ReadTest.java
index 47dad6b8565..9ac46a8bc4a 100644
--- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/ReadTest.java
+++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/ReadTest.java
@@ -49,8 +49,11 @@ import org.apache.beam.sdk.io.UnboundedSource.CheckpointMark;
import org.apache.beam.sdk.io.UnboundedSource.UnboundedReader;
import org.apache.beam.sdk.options.ExperimentalOptions;
import org.apache.beam.sdk.options.PipelineOptions;
+import org.apache.beam.sdk.options.PipelineOptionsFactory;
+import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.testing.NeedsRunner;
import org.apache.beam.sdk.testing.PAssert;
+import org.apache.beam.sdk.testing.TestOutputReceiver;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.testing.UsesUnboundedPCollections;
import org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo;
@@ -222,6 +225,39 @@ public class ReadTest implements Serializable {
}
}
+ @Test
+ public void testUnboundedSdfSplitRestrictionDesiredNumSplits() throws
Exception {
+ List<Integer> recordedDesiredNumSplits = new ArrayList<>();
+ CustomUnboundedSource source =
+ new CustomUnboundedSource() {
+ @Override
+ public List<? extends UnboundedSource<String, NoOpCheckpointMark>>
split(
+ int desiredNumSplits, PipelineOptions options) {
+ recordedDesiredNumSplits.add(desiredNumSplits);
+ return Collections.singletonList(this);
+ }
+ };
+
+ Read.UnboundedSourceAsSDFWrapperFn<String, NoOpCheckpointMark> wrapperFn =
+ new Read.UnboundedSourceAsSDFWrapperFn<>(null);
+ Read.UnboundedSourceAsSDFWrapperFn.UnboundedSourceRestriction<String,
NoOpCheckpointMark>
+ restriction = wrapperFn.initialRestriction(source);
+ TestOutputReceiver<
+ Read.UnboundedSourceAsSDFWrapperFn.UnboundedSourceRestriction<
+ String, NoOpCheckpointMark>>
+ receiver = new TestOutputReceiver<>();
+
+ PipelineOptions defaultOptions = PipelineOptionsFactory.create();
+ wrapperFn.splitRestriction(restriction, receiver, defaultOptions);
+ assertEquals(Collections.singletonList(20), recordedDesiredNumSplits);
+
+ recordedDesiredNumSplits.clear();
+ PipelineOptions customOptions = PipelineOptionsFactory.create();
+
customOptions.as(StreamingOptions.class).setDesiredNumUnboundedSourceSplits(5);
+ wrapperFn.splitRestriction(restriction, receiver, customOptions);
+ assertEquals(Collections.singletonList(5), recordedDesiredNumSplits);
+ }
+
private <T extends Serializable & Consumer<Instant>>
Read.Unbounded<Long> newUnboundedReadInterceptingWatermark(
long numElements, T interceptedWatermarkReceiver) {
diff --git a/sdks/python/apache_beam/io/unbounded_source.py
b/sdks/python/apache_beam/io/unbounded_source.py
index ac5f17ce45a..99e987522f5 100644
--- a/sdks/python/apache_beam/io/unbounded_source.py
+++ b/sdks/python/apache_beam/io/unbounded_source.py
@@ -104,6 +104,8 @@ from apache_beam.coders.coders import TupleCoder
from apache_beam.coders.coders import _MemoizingPickleCoder
from apache_beam.io import iobase
from apache_beam.io.watermark_estimators import ManualWatermarkEstimator
+from apache_beam.options.pipeline_options import StreamingOptions
+from apache_beam.options.pipeline_options_context import get_pipeline_options
from apache_beam.runners import sdf_utils
from apache_beam.transforms import PTransform
from apache_beam.transforms import core
@@ -709,11 +711,18 @@ class
_UnboundedSourceRestrictionProvider(core.RestrictionProvider):
yield restriction
return
+ desired_num_splits = _DEFAULT_DESIRED_NUM_SPLITS
+ options = get_pipeline_options()
+ if options is not None:
+ configured_splits = options.view_as(
+ StreamingOptions).desired_num_unbounded_source_splits
+ if configured_splits > 0:
+ desired_num_splits = configured_splits
+
# ``source.split`` is user code and may refuse to split; fall back to a
# single restriction on error.
try:
- split_sources = list(
- restriction.source.split(_DEFAULT_DESIRED_NUM_SPLITS, None))
+ split_sources = list(restriction.source.split(desired_num_splits, None))
except Exception: # pylint: disable=broad-except
_LOGGER.warning(
'Exception while splitting UnboundedSource. Source not split.',
diff --git a/sdks/python/apache_beam/io/unbounded_source_test.py
b/sdks/python/apache_beam/io/unbounded_source_test.py
index 6c6651cd0db..d4646e94de5 100644
--- a/sdks/python/apache_beam/io/unbounded_source_test.py
+++ b/sdks/python/apache_beam/io/unbounded_source_test.py
@@ -47,6 +47,8 @@ from apache_beam.io.unbounded_source import
_UnboundedSourceRestrictionCoder
from apache_beam.io.unbounded_source import _UnboundedSourceRestrictionProvider
from apache_beam.io.unbounded_source import _UnboundedSourceRestrictionTracker
from apache_beam.io.watermark_estimators import ManualWatermarkEstimator
+from apache_beam.options.pipeline_options import PipelineOptions
+from apache_beam.options.pipeline_options_context import
scoped_pipeline_options
from apache_beam.runners import sdf_utils
from apache_beam.testing.test_pipeline import TestPipeline
from apache_beam.testing.util import assert_that
@@ -361,6 +363,25 @@ class RestrictionProviderTest(unittest.TestCase):
self.assertTrue(
all(split.finalization_checkpoint_mark is None for split in splits))
+ def test_initial_split_uses_desired_num_unbounded_source_splits_option(self):
+ split_log = []
+
+ class _SplitSource(UnboundedCountingSource):
+ @override
+ def split(self, desired_num_splits, options=None):
+ split_log.append((desired_num_splits, options))
+ return [self]
+
+ source = _SplitSource(5)
+ provider = _UnboundedSourceRestrictionProvider()
+ restriction = _UnboundedSourceRestriction(source=source)
+ options = PipelineOptions(['--desired_num_unbounded_source_splits=5'])
+
+ with scoped_pipeline_options(options):
+ list(provider.split(source, restriction))
+
+ self.assertEqual(split_log, [(5, None)])
+
def test_initial_split_does_not_split_checkpointed_restriction(self):
split_log = []
diff --git a/sdks/python/apache_beam/options/pipeline_options.py
b/sdks/python/apache_beam/options/pipeline_options.py
index 75492344a60..743ab967b04 100644
--- a/sdks/python/apache_beam/options/pipeline_options.py
+++ b/sdks/python/apache_beam/options/pipeline_options.py
@@ -812,6 +812,11 @@ class StreamingOptions(PipelineOptions):
'version of the Beam SDK. '
'See for example, https://cloud.google.com/dataflow/docs/guides/'
'updating-a-pipeline')
+ parser.add_argument(
+ '--desired_num_unbounded_source_splits',
+ type=int,
+ default=0,
+ help='The desired number of initial splits for UnboundedSources.')
class CrossLanguageOptions(PipelineOptions):