1919
2020import java .io .IOException ;
2121import java .io .Serializable ;
22+ import java .util .ArrayList ;
2223import java .util .Collections ;
2324import java .util .EnumMap ;
2425import java .util .Iterator ;
2728import java .util .Map ;
2829import java .util .concurrent .ConcurrentLinkedQueue ;
2930import java .util .stream .Collectors ;
31+ import org .apache .beam .model .fnexecution .v1 .BeamFnApi .DelayedBundleApplication ;
3032import org .apache .beam .model .fnexecution .v1 .BeamFnApi .ProcessBundleProgressResponse ;
3133import org .apache .beam .model .fnexecution .v1 .BeamFnApi .ProcessBundleResponse ;
3234import org .apache .beam .model .fnexecution .v1 .BeamFnApi .StateKey ;
3638import org .apache .beam .runners .core .TimerInternals ;
3739import org .apache .beam .runners .core .construction .SerializablePipelineOptions ;
3840import org .apache .beam .runners .core .metrics .MetricsContainerImpl ;
41+ import org .apache .beam .runners .fnexecution .control .BundleCheckpointHandler ;
42+ import org .apache .beam .runners .fnexecution .control .BundleFinalizationHandlers ;
43+ import org .apache .beam .runners .fnexecution .control .BundleFinalizationHandlers .InMemoryFinalizer ;
3944import org .apache .beam .runners .fnexecution .control .BundleProgressHandler ;
4045import org .apache .beam .runners .fnexecution .control .ExecutableStageContext ;
4146import org .apache .beam .runners .fnexecution .control .JobBundleFactory ;
5964import org .apache .beam .sdk .io .FileSystems ;
6065import org .apache .beam .sdk .transforms .join .RawUnionValue ;
6166import org .apache .beam .sdk .transforms .windowing .BoundedWindow ;
67+ import org .apache .beam .sdk .util .CoderUtils ;
6268import org .apache .beam .sdk .util .construction .Timer ;
6369import org .apache .beam .sdk .util .construction .graph .ExecutableStage ;
6470import org .apache .beam .sdk .values .WindowedValue ;
8591class SparkExecutableStageFunction <InputT , SideInputT >
8692 implements FlatMapFunction <Iterator <WindowedValue <InputT >>, RawUnionValue > {
8793
94+ // Union tag carrying serialized SDF residuals to the streaming residual relay. Regular output
95+ // tags start at 0.
96+ static final int SDF_RESIDUAL_TAG = -1 ;
97+
8898 // Pipeline options for initializing the FileSystems
8999 private final SerializablePipelineOptions pipelineOptions ;
90100 private final RunnerApi .ExecutableStagePayload stagePayload ;
@@ -95,6 +105,10 @@ class SparkExecutableStageFunction<InputT, SideInputT>
95105 sideInputs ;
96106 private final MetricsContainerStepMapAccumulator metricsAccumulator ;
97107 private final Coder windowCoder ;
108+ // Coder for the stage input, used to re-feed SDF self-checkpoint residuals.
109+ private final Coder <WindowedValue <InputT >> inputCoder ;
110+ // Streaming emits residuals to the cross-batch relay; batch drains them in place.
111+ private final boolean emitSdfResiduals ;
98112 private final JobInfo jobInfo ;
99113
100114 private transient InMemoryBagUserStateFactory bagUserStateHandlerFactory ;
@@ -108,7 +122,9 @@ class SparkExecutableStageFunction<InputT, SideInputT>
108122 SparkExecutableStageContextFactory contextFactory ,
109123 Map <String , Tuple2 <Broadcast <List <byte []>>, WindowedValueCoder <SideInputT >>> sideInputs ,
110124 MetricsContainerStepMapAccumulator metricsAccumulator ,
111- Coder windowCoder ) {
125+ Coder windowCoder ,
126+ Coder <WindowedValue <InputT >> inputCoder ,
127+ boolean emitSdfResiduals ) {
112128 this .pipelineOptions = pipelineOptions ;
113129 this .stagePayload = stagePayload ;
114130 this .jobInfo = jobInfo ;
@@ -117,6 +133,8 @@ class SparkExecutableStageFunction<InputT, SideInputT>
117133 this .sideInputs = sideInputs ;
118134 this .metricsAccumulator = metricsAccumulator ;
119135 this .windowCoder = windowCoder ;
136+ this .inputCoder = inputCoder ;
137+ this .emitSdfResiduals = emitSdfResiduals ;
120138 }
121139
122140 /** Call the executable stage function on the values of a PairRDD, ignoring the key. */
@@ -146,7 +164,30 @@ public Iterator<RawUnionValue> call(Iterator<WindowedValue<InputT>> inputs) thro
146164 executableStage , stageBundleFactory .getProcessBundleDescriptor ());
147165 if (executableStage .getTimers ().size () == 0 ) {
148166 ReceiverFactory receiverFactory = new ReceiverFactory (collector , outputMap );
149- processElements (stateRequestHandler , receiverFactory , null , stageBundleFactory , inputs );
167+ ResidualCollector residualCollector = new ResidualCollector ();
168+ InMemoryFinalizer finalizer =
169+ BundleFinalizationHandlers .inMemoryFinalizer (
170+ stageBundleFactory .getInstructionRequestHandler ());
171+ processElements (
172+ stateRequestHandler ,
173+ receiverFactory ,
174+ null ,
175+ stageBundleFactory ,
176+ inputs ,
177+ residualCollector ,
178+ finalizer );
179+ if (emitSdfResiduals ) {
180+ for (DelayedBundleApplication residual : residualCollector .drain ()) {
181+ collector .add (new RawUnionValue (SDF_RESIDUAL_TAG , residual .toByteArray ()));
182+ }
183+ } else {
184+ processResiduals (
185+ stateRequestHandler ,
186+ receiverFactory ,
187+ stageBundleFactory ,
188+ residualCollector ,
189+ finalizer );
190+ }
150191 return collector .iterator ();
151192 }
152193 // Used with Batch, we know that all the data is available for this key. We can't use the
@@ -172,8 +213,18 @@ public Iterator<RawUnionValue> call(Iterator<WindowedValue<InputT>> inputs) thro
172213 windowCoder );
173214
174215 // Process inputs.
216+ ResidualCollector residualCollector = new ResidualCollector ();
217+ InMemoryFinalizer finalizer =
218+ BundleFinalizationHandlers .inMemoryFinalizer (
219+ stageBundleFactory .getInstructionRequestHandler ());
175220 processElements (
176- stateRequestHandler , receiverFactory , timerReceiverFactory , stageBundleFactory , inputs );
221+ stateRequestHandler ,
222+ receiverFactory ,
223+ timerReceiverFactory ,
224+ stageBundleFactory ,
225+ inputs ,
226+ residualCollector ,
227+ finalizer );
177228
178229 // Finish any pending windows by advancing the input watermark to infinity.
179230 timerInternals .advanceInputWatermark (BoundedWindow .TIMESTAMP_MAX_VALUE );
@@ -189,12 +240,17 @@ public Iterator<RawUnionValue> call(Iterator<WindowedValue<InputT>> inputs) thro
189240 receiverFactory ,
190241 timerReceiverFactory ,
191242 stateRequestHandler ,
192- getBundleProgressHandler ())) {
243+ getBundleProgressHandler (),
244+ finalizer ,
245+ residualCollector )) {
193246
194247 PipelineTranslatorUtils .fireEligibleTimers (
195248 timerInternals , bundle .getTimerReceivers (), currentTimerKey );
196249 }
250+ finalizer .finalizeAllOutstandingBundles ();
197251 }
252+ processResiduals (
253+ stateRequestHandler , receiverFactory , stageBundleFactory , residualCollector , finalizer );
198254 return collector .iterator ();
199255 }
200256 }
@@ -207,21 +263,116 @@ private void processElements(
207263 ReceiverFactory receiverFactory ,
208264 TimerReceiverFactory timerReceiverFactory ,
209265 StageBundleFactory stageBundleFactory ,
210- Iterator <WindowedValue <InputT >> inputs )
266+ Iterator <WindowedValue <InputT >> inputs ,
267+ BundleCheckpointHandler checkpointHandler ,
268+ InMemoryFinalizer finalizer )
211269 throws Exception {
212270 try (RemoteBundle bundle =
213271 stageBundleFactory .getBundle (
214272 receiverFactory ,
215273 timerReceiverFactory ,
216274 stateRequestHandler ,
217- getBundleProgressHandler ())) {
275+ getBundleProgressHandler (),
276+ finalizer ,
277+ checkpointHandler )) {
218278 FnDataReceiver <WindowedValue <?>> mainReceiver =
219279 Iterables .getOnlyElement (bundle .getInputReceivers ().values ());
220280 while (inputs .hasNext ()) {
221281 WindowedValue <InputT > input = inputs .next ();
222282 mainReceiver .accept (input );
223283 }
224284 }
285+ finalizer .finalizeAllOutstandingBundles ();
286+ }
287+
288+ // Re-feeds SDF self-checkpoint residuals in fresh bundles until the SDK returns none. Each
289+ // residual resumes at its own requested time. Bounded restrictions always run out; unbounded ones
290+ // never would, so they are rejected rather than drained forever.
291+ private void processResiduals (
292+ StateRequestHandler stateRequestHandler ,
293+ ReceiverFactory receiverFactory ,
294+ StageBundleFactory stageBundleFactory ,
295+ ResidualCollector residualCollector ,
296+ InMemoryFinalizer finalizer )
297+ throws Exception {
298+ List <ScheduledResidual > scheduled = schedule (residualCollector .drain ());
299+ while (!scheduled .isEmpty ()) {
300+ List <ScheduledResidual > due = takeDue (scheduled );
301+ try (RemoteBundle bundle =
302+ stageBundleFactory .getBundle (
303+ receiverFactory ,
304+ null ,
305+ stateRequestHandler ,
306+ getBundleProgressHandler (),
307+ finalizer ,
308+ residualCollector )) {
309+ FnDataReceiver <WindowedValue <?>> mainReceiver =
310+ Iterables .getOnlyElement (bundle .getInputReceivers ().values ());
311+ for (ScheduledResidual residual : due ) {
312+ mainReceiver .accept (CoderUtils .decodeFromByteArray (inputCoder , residual .elementBytes ));
313+ }
314+ }
315+ finalizer .finalizeAllOutstandingBundles ();
316+ scheduled .addAll (schedule (residualCollector .drain ()));
317+ }
318+ }
319+
320+ private static List <ScheduledResidual > schedule (List <DelayedBundleApplication > residuals ) {
321+ long now = System .currentTimeMillis ();
322+ List <ScheduledResidual > scheduled = new ArrayList <>();
323+ for (DelayedBundleApplication residual : residuals ) {
324+ if (residual .getApplication ().getElement ().isEmpty ()) {
325+ continue ;
326+ }
327+ if (residual .getApplication ().getIsBounded () == RunnerApi .IsBounded .Enum .UNBOUNDED ) {
328+ throw new UnsupportedOperationException (
329+ "Unbounded splittable DoFn is not supported in batch mode on the Spark runner. See "
330+ + "https://github.com/apache/beam/issues/19468." );
331+ }
332+ long delayMillis =
333+ residual .hasRequestedTimeDelay ()
334+ ? residual .getRequestedTimeDelay ().getSeconds () * 1000
335+ + residual .getRequestedTimeDelay ().getNanos () / 1_000_000
336+ : 0 ;
337+ scheduled .add (
338+ new ScheduledResidual (
339+ now + delayMillis , residual .getApplication ().getElement ().toByteArray ()));
340+ }
341+ return scheduled ;
342+ }
343+
344+ // Waits for the earliest resume time, then removes and returns everything due by then.
345+ private static List <ScheduledResidual > takeDue (List <ScheduledResidual > scheduled )
346+ throws InterruptedException {
347+ long earliest = Long .MAX_VALUE ;
348+ for (ScheduledResidual residual : scheduled ) {
349+ earliest = Math .min (earliest , residual .dueMillis );
350+ }
351+ long waitMillis = earliest - System .currentTimeMillis ();
352+ if (waitMillis > 0 ) {
353+ Thread .sleep (waitMillis );
354+ }
355+ long now = System .currentTimeMillis ();
356+ List <ScheduledResidual > due = new ArrayList <>();
357+ Iterator <ScheduledResidual > iterator = scheduled .iterator ();
358+ while (iterator .hasNext ()) {
359+ ScheduledResidual residual = iterator .next ();
360+ if (residual .dueMillis <= now ) {
361+ due .add (residual );
362+ iterator .remove ();
363+ }
364+ }
365+ return due ;
366+ }
367+
368+ private static class ScheduledResidual {
369+ private final long dueMillis ;
370+ private final byte [] elementBytes ;
371+
372+ ScheduledResidual (long dueMillis , byte [] elementBytes ) {
373+ this .dueMillis = dueMillis ;
374+ this .elementBytes = elementBytes ;
375+ }
225376 }
226377
227378 private BundleProgressHandler getBundleProgressHandler () {
@@ -295,6 +446,27 @@ interface JobBundleFactoryCreator extends Serializable {
295446 JobBundleFactory create ();
296447 }
297448
449+ /** Collects SDF self-checkpoint residuals returned by the SDK harness. */
450+ private static class ResidualCollector implements BundleCheckpointHandler {
451+
452+ private final ConcurrentLinkedQueue <DelayedBundleApplication > residuals =
453+ new ConcurrentLinkedQueue <>();
454+
455+ @ Override
456+ public void onCheckpoint (ProcessBundleResponse response ) {
457+ residuals .addAll (response .getResidualRootsList ());
458+ }
459+
460+ private List <DelayedBundleApplication > drain () {
461+ List <DelayedBundleApplication > pending = new ArrayList <>();
462+ DelayedBundleApplication residual ;
463+ while ((residual = residuals .poll ()) != null ) {
464+ pending .add (residual );
465+ }
466+ return pending ;
467+ }
468+ }
469+
298470 /**
299471 * Receiver factory that wraps outgoing elements with the corresponding union tag for a
300472 * multiplexed PCollection.
0 commit comments