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 f9ca09b3183 [Spark] Support splittable DoFn self-checkpointing in
portable batch (#39331)
f9ca09b3183 is described below
commit f9ca09b31834a367d671cb76397734c7b1ea3b92
Author: Elia Liu <[email protected]>
AuthorDate: Sat Aug 8 01:03:17 2026 +1000
[Spark] Support splittable DoFn self-checkpointing in portable batch
(#39331)
* [Spark] Support splittable DoFn self-checkpointing in portable batch
The portable Spark runner never passed a BundleCheckpointHandler to
StageBundleFactory.getBundle, so a splittable DoFn that self-checkpoints
failed on its first bundle and could not run at all.
In batch, a stage containing a splittable DoFn now holds each residual in
memory under a processing time timer, the way the portable Flink batch
runner does. Once the stage has drained its inputs, processing time
advances to infinity and the held residuals are replayed until the SDK
stops asking to resume, so a bounded restriction always runs out.
Streaming keeps rejecting self-checkpointing, with a message naming the
issue, since a residual has nowhere to live across micro-batches. Bundle
finalization is likewise rejected rather than run early, since this
runner cannot report that a bundle's output is durably committed.
Unskips the bounded splittable DoFn tests for the Spark runner.
---
.../beam_PostCommit_Java_PVR_Spark3_Streaming.json | 2 +-
.../beam_PostCommit_Java_PVR_Spark_Batch.json | 2 +-
...beam_PostCommit_Java_ValidatesRunner_Spark.json | 2 +-
...am_PostCommit_Python_ValidatesRunner_Spark.json | 3 +-
runners/spark/job-server/spark_job_server.gradle | 5 +-
.../SparkBatchPortablePipelineTranslator.java | 8 +-
.../translation/SparkExecutableStageFunction.java | 172 +++++++++++++++++++--
.../SparkStreamingPortablePipelineTranslator.java | 4 +-
.../SparkExecutableStageFunctionTest.java | 138 ++++++++++++++++-
.../runners/portability/spark_runner_test.py | 20 ---
10 files changed, 307 insertions(+), 49 deletions(-)
diff --git
a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json
b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json
index 455144f02a3..d6a91b7e2e8 100644
--- a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json
+++ b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run",
- "modification": 6
+ "modification": 7
}
diff --git a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json
b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json
index 455144f02a3..d6a91b7e2e8 100644
--- a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json
+++ b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run",
- "modification": 6
+ "modification": 7
}
diff --git
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json
index 1efc8e9e440..3f63c0c9975 100644
--- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json
+++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run",
- "modification": 1
+ "modification": 2
}
diff --git
a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
index f4ec72dc416..6384446f50e 100644
--- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
+++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
@@ -3,5 +3,6 @@
"https://github.com/apache/beam/issues/35429": "testing",
"trigger-2026-04-04": "portable_runner expand_sdf opt-in",
"https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR
test",
- "modification": 1
+ "modification": 1,
+ "https://github.com/apache/beam/issues/19468": "SDF self-checkpointing and
bundle finalization"
}
diff --git a/runners/spark/job-server/spark_job_server.gradle
b/runners/spark/job-server/spark_job_server.gradle
index 5240bb310d0..2811f875f84 100644
--- a/runners/spark/job-server/spark_job_server.gradle
+++ b/runners/spark/job-server/spark_job_server.gradle
@@ -199,10 +199,11 @@ def portableValidatesRunnerTask(String name, boolean
streaming, boolean docker,
excludeCategories 'org.apache.beam.sdk.testing.UsesKeyInParDo'
excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration'
excludeCategories 'org.apache.beam.sdk.testing.UsesTestStream'
- // TODO (https://github.com/apache/beam/issues/19468)
SplittableDoFnTests
- excludeCategories
'org.apache.beam.sdk.testing.UsesBoundedSplittableParDo'
+ // TODO (https://github.com/apache/beam/issues/19468) unbounded SDF
needs residuals to
+ // survive across micro-batches, which the streaming path cannot do
yet.
excludeCategories
'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo'
excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering'
+ // TODO (https://github.com/apache/beam/issues/19517) bundle
finalization
excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer'
}
testFilter = {
diff --git
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java
index ba3aa0e4d24..521f9583597 100644
---
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java
+++
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java
@@ -262,7 +262,9 @@ public class SparkBatchPortablePipelineTranslator
SparkExecutableStageContextFactory.getInstance(),
broadcastVariables,
MetricsAccumulator.getInstance(),
- windowCoder);
+ windowCoder,
+ getWindowedValueCoder(inputPCollectionId, components),
+ true);
staged = groupedByKey.flatMap(function.forPair());
} else {
JavaRDD<WindowedValue<InputT>> inputRdd2 = ((BoundedDataset<InputT>)
inputDataset).getRDD();
@@ -275,7 +277,9 @@ public class SparkBatchPortablePipelineTranslator
SparkExecutableStageContextFactory.getInstance(),
broadcastVariables,
MetricsAccumulator.getInstance(),
- windowCoder);
+ windowCoder,
+ getWindowedValueCoder(inputPCollectionId, components),
+ true);
staged = inputRdd2.mapPartitions(function2);
}
diff --git
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java
index 757740e2df5..a04899bf9c9 100644
---
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java
+++
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java
@@ -19,6 +19,7 @@ package org.apache.beam.runners.spark.translation;
import java.io.IOException;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
@@ -32,10 +33,15 @@ import
org.apache.beam.model.fnexecution.v1.BeamFnApi.ProcessBundleResponse;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateKey;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateKey.TypeCase;
import org.apache.beam.model.pipeline.v1.RunnerApi;
+import org.apache.beam.runners.core.InMemoryStateInternals;
import org.apache.beam.runners.core.InMemoryTimerInternals;
+import org.apache.beam.runners.core.StateInternals;
import org.apache.beam.runners.core.TimerInternals;
import org.apache.beam.runners.core.construction.SerializablePipelineOptions;
import org.apache.beam.runners.core.metrics.MetricsContainerImpl;
+import org.apache.beam.runners.fnexecution.control.BundleCheckpointHandler;
+import org.apache.beam.runners.fnexecution.control.BundleCheckpointHandlers;
+import org.apache.beam.runners.fnexecution.control.BundleFinalizationHandler;
import org.apache.beam.runners.fnexecution.control.BundleProgressHandler;
import org.apache.beam.runners.fnexecution.control.ExecutableStageContext;
import org.apache.beam.runners.fnexecution.control.JobBundleFactory;
@@ -57,8 +63,10 @@ import org.apache.beam.runners.spark.util.ByteArray;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.fn.data.FnDataReceiver;
import org.apache.beam.sdk.io.FileSystems;
+import org.apache.beam.sdk.state.MapState;
import org.apache.beam.sdk.transforms.join.RawUnionValue;
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
+import org.apache.beam.sdk.util.construction.PTransformTranslation;
import org.apache.beam.sdk.util.construction.Timer;
import org.apache.beam.sdk.util.construction.graph.ExecutableStage;
import org.apache.beam.sdk.values.WindowedValue;
@@ -95,10 +103,16 @@ class SparkExecutableStageFunction<InputT, SideInputT>
sideInputs;
private final MetricsContainerStepMapAccumulator metricsAccumulator;
private final Coder windowCoder;
+ // Coder for this stage's input, used to hold and replay splittable DoFn
residuals.
+ private final Coder<WindowedValue<InputT>> inputCoder;
+ // Batch replays residuals in place. Streaming has nowhere to hold them
across micro-batches yet.
+ private final boolean batch;
private final JobInfo jobInfo;
private transient InMemoryBagUserStateFactory bagUserStateHandlerFactory;
private transient Object currentTimerKey;
+ private transient InMemoryTimerInternals sdfTimerInternals;
+ private transient StateInternals sdfStateInternals;
SparkExecutableStageFunction(
SerializablePipelineOptions pipelineOptions,
@@ -108,7 +122,9 @@ class SparkExecutableStageFunction<InputT, SideInputT>
SparkExecutableStageContextFactory contextFactory,
Map<String, Tuple2<Broadcast<List<byte[]>>,
WindowedValueCoder<SideInputT>>> sideInputs,
MetricsContainerStepMapAccumulator metricsAccumulator,
- Coder windowCoder) {
+ Coder windowCoder,
+ Coder<WindowedValue<InputT>> inputCoder,
+ boolean batch) {
this.pipelineOptions = pipelineOptions;
this.stagePayload = stagePayload;
this.jobInfo = jobInfo;
@@ -117,6 +133,8 @@ class SparkExecutableStageFunction<InputT, SideInputT>
this.sideInputs = sideInputs;
this.metricsAccumulator = metricsAccumulator;
this.windowCoder = windowCoder;
+ this.inputCoder = inputCoder;
+ this.batch = batch;
}
/** Call the executable stage function on the values of a PairRDD, ignoring
the key. */
@@ -144,9 +162,18 @@ class SparkExecutableStageFunction<InputT, SideInputT>
StateRequestHandler stateRequestHandler =
getStateRequestHandler(
executableStage,
stageBundleFactory.getProcessBundleDescriptor());
+ BundleCheckpointHandler checkpointHandler =
getBundleCheckpointHandler(executableStage);
if (executableStage.getTimers().size() == 0) {
ReceiverFactory receiverFactory = new ReceiverFactory(collector,
outputMap);
- processElements(stateRequestHandler, receiverFactory, null,
stageBundleFactory, inputs);
+ processElements(
+ stateRequestHandler,
+ receiverFactory,
+ null,
+ stageBundleFactory,
+ inputs,
+ checkpointHandler);
+ replaySdfResiduals(
+ stateRequestHandler, receiverFactory, null, stageBundleFactory,
checkpointHandler);
return collector.iterator();
}
// Used with Batch, we know that all the data is available for this
key. We can't use the
@@ -173,7 +200,12 @@ class SparkExecutableStageFunction<InputT, SideInputT>
// Process inputs.
processElements(
- stateRequestHandler, receiverFactory, timerReceiverFactory,
stageBundleFactory, inputs);
+ stateRequestHandler,
+ receiverFactory,
+ timerReceiverFactory,
+ stageBundleFactory,
+ inputs,
+ checkpointHandler);
// Finish any pending windows by advancing the input watermark to
infinity.
timerInternals.advanceInputWatermark(BoundedWindow.TIMESTAMP_MAX_VALUE);
@@ -182,19 +214,30 @@ class SparkExecutableStageFunction<InputT, SideInputT>
timerInternals.advanceSynchronizedProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE);
// Now we fire the timers and process elements generated by timers
(which may be timers
- // itself)
- while (timerInternals.hasPendingTimers()) {
- try (RemoteBundle bundle =
- stageBundleFactory.getBundle(
- receiverFactory,
- timerReceiverFactory,
- stateRequestHandler,
- getBundleProgressHandler())) {
+ // itself). A replayed splittable DoFn residual can set a timer, and a
fired timer can
+ // produce a residual, so alternate until neither has anything left.
+ do {
+ while (timerInternals.hasPendingTimers()) {
+ try (RemoteBundle bundle =
+ stageBundleFactory.getBundle(
+ receiverFactory,
+ timerReceiverFactory,
+ stateRequestHandler,
+ getBundleProgressHandler(),
+ getBundleFinalizationHandler(),
+ checkpointHandler)) {
- PipelineTranslatorUtils.fireEligibleTimers(
- timerInternals, bundle.getTimerReceivers(), currentTimerKey);
+ PipelineTranslatorUtils.fireEligibleTimers(
+ timerInternals, bundle.getTimerReceivers(), currentTimerKey);
+ }
}
- }
+ replaySdfResiduals(
+ stateRequestHandler,
+ receiverFactory,
+ timerReceiverFactory,
+ stageBundleFactory,
+ checkpointHandler);
+ } while (timerInternals.hasPendingTimers());
return collector.iterator();
}
}
@@ -207,14 +250,17 @@ class SparkExecutableStageFunction<InputT, SideInputT>
ReceiverFactory receiverFactory,
TimerReceiverFactory timerReceiverFactory,
StageBundleFactory stageBundleFactory,
- Iterator<WindowedValue<InputT>> inputs)
+ Iterator<WindowedValue<InputT>> inputs,
+ BundleCheckpointHandler checkpointHandler)
throws Exception {
try (RemoteBundle bundle =
stageBundleFactory.getBundle(
receiverFactory,
timerReceiverFactory,
stateRequestHandler,
- getBundleProgressHandler())) {
+ getBundleProgressHandler(),
+ getBundleFinalizationHandler(),
+ checkpointHandler)) {
FnDataReceiver<WindowedValue<?>> mainReceiver =
Iterables.getOnlyElement(bundle.getInputReceivers().values());
while (inputs.hasNext()) {
@@ -224,6 +270,100 @@ class SparkExecutableStageFunction<InputT, SideInputT>
}
}
+ private static boolean hasSdf(ExecutableStage executableStage) {
+ return executableStage.getTransforms().stream()
+ .anyMatch(
+ transform ->
+ transform
+ .getTransform()
+ .getSpec()
+ .getUrn()
+ .equals(
+ PTransformTranslation
+
.SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN));
+ }
+
+ // Holds a splittable DoFn's self-checkpoint residual in memory under a
processing time timer, so
+ // it can be replayed once this stage has drained its inputs.
+ private BundleCheckpointHandler getBundleCheckpointHandler(ExecutableStage
executableStage) {
+ sdfTimerInternals = null;
+ sdfStateInternals = null;
+ if (!batch) {
+ return response -> {
+ throw new UnsupportedOperationException(
+ "Splittable DoFn self-checkpointing is not supported on the
portable Spark runner in "
+ + "streaming mode. For more details, please refer to "
+ + "https://github.com/apache/beam/issues/19468.");
+ };
+ }
+ if (!hasSdf(executableStage)) {
+ return response -> {
+ throw new UnsupportedOperationException(
+ "Self-checkpoint is only supported on splittable DoFn.");
+ };
+ }
+ sdfTimerInternals = new InMemoryTimerInternals();
+ sdfStateInternals = InMemoryStateInternals.forKey("sdf_state");
+ return new BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler<>(
+ key -> sdfTimerInternals, key -> sdfStateInternals, inputCoder,
windowCoder);
+ }
+
+ // Bundle finalization needs the runner to have durably committed the
bundle's output first, which
+ // this runner cannot report, so it is rejected rather than silently
finalized early.
+ private BundleFinalizationHandler getBundleFinalizationHandler() {
+ return bundleId -> {
+ throw new UnsupportedOperationException(
+ "The portable Spark runner does not support bundle finalization. For
more details, please "
+ + "refer to https://github.com/apache/beam/issues/19517.");
+ };
+ }
+
+ // Replays held residuals until the splittable DoFn stops asking to resume.
Processing time is at
+ // infinity, so every residual is due immediately and a bounded restriction
always runs out.
+ private void replaySdfResiduals(
+ StateRequestHandler stateRequestHandler,
+ ReceiverFactory receiverFactory,
+ TimerReceiverFactory timerReceiverFactory,
+ StageBundleFactory stageBundleFactory,
+ BundleCheckpointHandler checkpointHandler)
+ throws Exception {
+ if (sdfTimerInternals == null) {
+ return;
+ }
+ sdfTimerInternals.advanceProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE);
+
sdfTimerInternals.advanceSynchronizedProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE);
+ while (sdfTimerInternals.hasPendingTimers()) {
+ try (RemoteBundle bundle =
+ stageBundleFactory.getBundle(
+ receiverFactory,
+ timerReceiverFactory,
+ stateRequestHandler,
+ getBundleProgressHandler(),
+ getBundleFinalizationHandler(),
+ checkpointHandler)) {
+ List<WindowedValue<InputT>> residuals = new ArrayList<>();
+ TimerInternals.TimerData timer;
+ while ((timer = sdfTimerInternals.removeNextProcessingTimer()) !=
null) {
+ MapState<String, WindowedValue<InputT>> residualState =
+ sdfStateInternals.state(
+ timer.getNamespace(),
+
BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler.residualStateTag(
+ inputCoder));
+ WindowedValue<InputT> residual =
residualState.get(timer.getTimerId()).read();
+ residualState.remove(timer.getTimerId());
+ if (residual != null) {
+ residuals.add(residual);
+ }
+ }
+ FnDataReceiver<WindowedValue<?>> mainReceiver =
+ Iterables.getOnlyElement(bundle.getInputReceivers().values());
+ for (WindowedValue<InputT> residual : residuals) {
+ mainReceiver.accept(residual);
+ }
+ }
+ }
+ }
+
private BundleProgressHandler getBundleProgressHandler() {
String stageName = stagePayload.getInput();
MetricsContainerImpl container =
metricsAccumulator.value().getContainer(stageName);
diff --git
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java
index 9975c81b56a..db3551454dc 100644
---
a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java
+++
b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java
@@ -251,7 +251,9 @@ public class SparkStreamingPortablePipelineTranslator
SparkExecutableStageContextFactory.getInstance(),
broadcastVariables,
MetricsAccumulator.getInstance(),
- windowCoder);
+ windowCoder,
+ getWindowedValueCoder(inputPCollectionId, components),
+ false);
JavaDStream<RawUnionValue> staged = inputDStream.mapPartitions(function);
String intermediateId = getExecutableStageIntermediateId(transformNode);
diff --git
a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java
b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java
index 98601389f5c..31ba649cfff 100644
---
a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java
+++
b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java
@@ -18,6 +18,7 @@
package org.apache.beam.runners.spark.translation;
import static
org.apache.beam.sdk.util.construction.PTransformTranslation.PAR_DO_TRANSFORM_URN;
+import static
org.apache.beam.sdk.util.construction.PTransformTranslation.SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.mockito.ArgumentMatchers.any;
@@ -33,6 +34,9 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import org.apache.beam.model.fnexecution.v1.BeamFnApi.BundleApplication;
+import org.apache.beam.model.fnexecution.v1.BeamFnApi.DelayedBundleApplication;
+import org.apache.beam.model.fnexecution.v1.BeamFnApi.ProcessBundleResponse;
import org.apache.beam.model.pipeline.v1.RunnerApi;
import org.apache.beam.model.pipeline.v1.RunnerApi.Components;
import org.apache.beam.model.pipeline.v1.RunnerApi.ExecutableStagePayload;
@@ -52,13 +56,18 @@ import
org.apache.beam.runners.fnexecution.control.StageBundleFactory;
import org.apache.beam.runners.fnexecution.control.TimerReceiverFactory;
import org.apache.beam.runners.fnexecution.state.StateRequestHandler;
import
org.apache.beam.runners.spark.metrics.MetricsContainerStepMapAccumulator;
+import org.apache.beam.sdk.coders.Coder;
+import org.apache.beam.sdk.coders.VarIntCoder;
import org.apache.beam.sdk.fn.data.FnDataReceiver;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.join.RawUnionValue;
+import org.apache.beam.sdk.transforms.windowing.GlobalWindow;
+import org.apache.beam.sdk.util.CoderUtils;
import org.apache.beam.sdk.util.construction.Timer;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.WindowedValue;
import org.apache.beam.sdk.values.WindowedValues;
+import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
@@ -97,12 +106,31 @@ public class SparkExecutableStageFunctionTest {
.build())
.build();
+ private final ExecutableStagePayload sdfStagePayload =
+ ExecutableStagePayload.newBuilder()
+ .setInput(inputId)
+ .addTransforms("sdf-transform-id")
+ .setComponents(
+ Components.newBuilder()
+ .putTransforms(
+ "sdf-transform-id",
+ RunnerApi.PTransform.newBuilder()
+ .putInputs("input-name", inputId)
+ .setSpec(
+ RunnerApi.FunctionSpec.newBuilder()
+
.setUrn(SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN))
+ .build())
+ .putPcollections(inputId, PCollection.getDefaultInstance())
+ .build())
+ .build();
+
@Before
public void setUpMocks() throws Exception {
MockitoAnnotations.initMocks(this);
when(contextFactory.get(any())).thenReturn(stageContext);
when(stageContext.getStageBundleFactory(any())).thenReturn(stageBundleFactory);
- when(stageBundleFactory.getBundle(any(), any(), any(),
any(BundleProgressHandler.class)))
+ when(stageBundleFactory.getBundle(
+ any(), any(), any(), any(BundleProgressHandler.class), any(),
any()))
.thenReturn(remoteBundle);
@SuppressWarnings("unchecked")
ImmutableMap<String, FnDataReceiver> inputReceiver =
@@ -126,7 +154,8 @@ public class SparkExecutableStageFunctionTest {
SparkExecutableStageFunction<Integer, ?> function =
getFunction(Collections.emptyMap());
RemoteBundle bundle = Mockito.mock(RemoteBundle.class);
- when(stageBundleFactory.getBundle(any(), any(), any(),
any(BundleProgressHandler.class)))
+ when(stageBundleFactory.getBundle(
+ any(), any(), any(), any(BundleProgressHandler.class), any(),
any()))
.thenReturn(bundle);
@SuppressWarnings("unchecked")
@@ -247,7 +276,8 @@ public class SparkExecutableStageFunctionTest {
List<WindowedValue<Integer>> inputs = new ArrayList<>();
inputs.add(WindowedValues.valueInGlobalWindow(0));
function.call(inputs.iterator());
- verify(stageBundleFactory).getBundle(any(), any(), any(),
any(BundleProgressHandler.class));
+ verify(stageBundleFactory)
+ .getBundle(any(), any(), any(), any(BundleProgressHandler.class),
any(), any());
verify(stageBundleFactory).getProcessBundleDescriptor();
verify(stageBundleFactory).close();
verifyNoMoreInteractions(stageBundleFactory);
@@ -260,6 +290,104 @@ public class SparkExecutableStageFunctionTest {
verifyNoInteractions(stageBundleFactory);
}
+ @Test
+ public void sdfResidualsAreReplayedUntilDrained() throws Exception {
+ // A stage whose bundle self-checkpoints once: the first bundle returns a
residual, the replay
+ // bundle returns none.
+ List<WindowedValue<?>> received = new ArrayList<>();
+ WindowedValue<Integer> residualValue =
WindowedValues.valueInGlobalWindow(7);
+ Coder<WindowedValue<Integer>> residualCoder =
+ WindowedValues.getFullCoder(VarIntCoder.of(),
GlobalWindow.Coder.INSTANCE);
+ ProcessBundleResponse withResidual =
+ ProcessBundleResponse.newBuilder()
+ .addResidualRoots(
+ DelayedBundleApplication.newBuilder()
+ .setApplication(
+ BundleApplication.newBuilder()
+ .setElement(
+ ByteString.copyFrom(
+
CoderUtils.encodeToByteArray(residualCoder, residualValue)))))
+ .build();
+
+ StageBundleFactory bundleFactory =
+ new StageBundleFactory() {
+ private int bundles;
+
+ @Override
+ public RemoteBundle getBundle(
+ OutputReceiverFactory receiverFactory,
+ TimerReceiverFactory timerReceiverFactory,
+ StateRequestHandler stateRequestHandler,
+ BundleProgressHandler progressHandler,
+ BundleFinalizationHandler finalizationHandler,
+ BundleCheckpointHandler checkpointHandler) {
+ boolean checkpointThisBundle = bundles++ == 0;
+ return new RemoteBundle() {
+ @Override
+ public String getId() {
+ return "bundle-id";
+ }
+
+ @Override
+ public Map<String, FnDataReceiver> getInputReceivers() {
+ FnDataReceiver<WindowedValue<?>> receiver = received::add;
+ return ImmutableMap.of("input", receiver);
+ }
+
+ @Override
+ public Map<KV<String, String>, FnDataReceiver<Timer>>
getTimerReceivers() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public void requestProgress() {}
+
+ @Override
+ public void split(double fractionOfRemainder) {}
+
+ @Override
+ public void close() {
+ if (checkpointThisBundle) {
+ checkpointHandler.onCheckpoint(withResidual);
+ }
+ }
+ };
+ }
+
+ @Override
+ public ProcessBundleDescriptors.ExecutableProcessBundleDescriptor
+ getProcessBundleDescriptor() {
+ return null;
+ }
+
+ @Override
+ public InstructionRequestHandler getInstructionRequestHandler() {
+ return null;
+ }
+
+ @Override
+ public void close() {}
+ };
+ when(stageContext.getStageBundleFactory(any())).thenReturn(bundleFactory);
+
+ SparkExecutableStageFunction<Integer, ?> function =
+ new SparkExecutableStageFunction<>(
+ pipelineOptions,
+ sdfStagePayload,
+ null,
+ Collections.emptyMap(),
+ contextFactory,
+ Collections.emptyMap(),
+ metricsAccumulator,
+ GlobalWindow.Coder.INSTANCE,
+ residualCoder,
+ true);
+
+
function.call(Collections.singletonList(WindowedValues.valueInGlobalWindow(1)).iterator());
+
+ assertThat(received, contains(WindowedValues.valueInGlobalWindow(1),
residualValue));
+ }
+
private <InputT, SideInputT> SparkExecutableStageFunction<InputT,
SideInputT> getFunction(
Map<String, Integer> outputMap) {
return new SparkExecutableStageFunction<>(
@@ -270,6 +398,8 @@ public class SparkExecutableStageFunctionTest {
contextFactory,
Collections.emptyMap(),
metricsAccumulator,
- null);
+ null,
+ null,
+ true);
}
}
diff --git a/sdks/python/apache_beam/runners/portability/spark_runner_test.py
b/sdks/python/apache_beam/runners/portability/spark_runner_test.py
index 4152b8d09f4..40774eb9602 100644
--- a/sdks/python/apache_beam/runners/portability/spark_runner_test.py
+++ b/sdks/python/apache_beam/runners/portability/spark_runner_test.py
@@ -144,26 +144,10 @@ class
SparkRunnerTest(portable_runner_test.PortableRunnerTest):
# Skip until Spark runner supports metrics.
raise unittest.SkipTest("https://github.com/apache/beam/issues/19496")
- def test_sdf(self):
- # Skip until Spark runner supports SDF.
- raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
-
def test_unbounded_source_read(self):
# Skip until Spark runner supports SDF.
raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
- def test_sdf_with_watermark_tracking(self):
- # Skip until Spark runner supports SDF.
- raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
-
- def test_sdf_with_sdf_initiated_checkpointing(self):
- # Skip until Spark runner supports SDF.
- raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
-
- def test_sdf_synthetic_source(self):
- # Skip until Spark runner supports SDF.
- raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
-
def test_callbacks_with_exception(self):
# Skip until Spark runner supports bundle finalization.
raise unittest.SkipTest("https://github.com/apache/beam/issues/19517")
@@ -172,10 +156,6 @@ class
SparkRunnerTest(portable_runner_test.PortableRunnerTest):
# Skip until Spark runner supports bundle finalization.
raise unittest.SkipTest("https://github.com/apache/beam/issues/19517")
- def test_sdf_with_dofn_as_watermark_estimator(self):
- # Skip until Spark runner supports SDF and self-checkpoint.
- raise unittest.SkipTest("https://github.com/apache/beam/issues/19468")
-
def test_pardo_dynamic_timer(self):
raise unittest.SkipTest("https://github.com/apache/beam/issues/20179")