2222
2323import java .io .Closeable ;
2424import java .util .Collection ;
25+ import java .util .Collections ;
2526import java .util .HashMap ;
27+ import java .util .Iterator ;
2628import java .util .List ;
2729import java .util .Map ;
2830import org .apache .beam .runners .core .DoFnRunner ;
5961import org .apache .beam .sdk .values .WindowingStrategy ;
6062import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .annotations .VisibleForTesting ;
6163import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .ImmutableList ;
64+ import org .apache .beam .vendor .guava .v32_1_2_jre .com .google .common .collect .Lists ;
6265import org .checkerframework .checker .nullness .qual .Nullable ;
6366import org .joda .time .Duration ;
6467import org .joda .time .Instant ;
7679 "rawtypes" , // TODO(https://github.com/apache/beam/issues/20447)
7780 "nullness" // TODO(https://github.com/apache/beam/issues/20497)
7881})
79- public class SimpleParDoFn <InputT , OutputT > implements ParDoFn {
82+ public class SimpleParDoFn <InputT , OutputT , W extends BoundedWindow > implements ParDoFn {
8083
8184 // TODO: Remove once Distributions has shipped.
8285 @ VisibleForTesting
@@ -112,6 +115,8 @@ public class SimpleParDoFn<InputT, OutputT> implements ParDoFn {
112115 // GroupAlsoByWindowViaWindowSetDoFn
113116 private @ Nullable DoFnSignature fnSignature ;
114117
118+ private @ Nullable StreamingSideInputProcessor <InputT , W > sideInputProcessor ;
119+
115120 /** Creates a {@link SimpleParDoFn} using basic information about the step being executed. */
116121 SimpleParDoFn (
117122 PipelineOptions options ,
@@ -317,8 +322,32 @@ public <TagT> void output(TupleTag<TagT> tag, WindowedValue<TagT> output) {
317322 outputManager ,
318323 doFnSchemaInformation ,
319324 sideInputMapping );
325+ if (hasStreamingSideInput ) {
326+ sideInputProcessor =
327+ new StreamingSideInputProcessor <>(
328+ new StreamingSideInputFetcher <InputT , W >(
329+ fnInfo .getSideInputViews (),
330+ fnInfo .getInputCoder (),
331+ (WindowingStrategy <?, W >) fnInfo .getWindowingStrategy (),
332+ (StreamingModeExecutionContext .StreamingModeStepContext ) userStepContext ));
333+ }
320334
321335 fnRunner .startBundle ();
336+ if (sideInputProcessor != null ) {
337+ boolean hasState = fnSignature != null && !fnSignature .stateDeclarations ().isEmpty ();
338+ Iterator <WindowedValue <InputT >> unblockedElements = sideInputProcessor .tryUnblockElements ();
339+ for (Iterator <WindowedValue <InputT >> it = unblockedElements ; it .hasNext (); ) {
340+ WindowedValue <InputT > unblockedElement = it .next ();
341+ fnRunner .processElement (unblockedElement );
342+ if (hasState ) {
343+ // These elements are now processed. Register cleanup timers for all the unblocked
344+ // windows.
345+ registerStateCleanup (
346+ (WindowingStrategy <?, W >) getDoFnInfo ().getWindowingStrategy (),
347+ (Collection <W >) unblockedElement .getWindows ());
348+ }
349+ }
350+ }
322351 }
323352
324353 @ Override
@@ -334,14 +363,32 @@ public void processElement(Object untypedElem) throws Exception {
334363
335364 WindowedValue <InputT > elem = (WindowedValue <InputT >) untypedElem ;
336365
337- if (fnSignature != null && fnSignature .stateDeclarations ().size () > 0 ) {
366+ boolean hasState = fnSignature != null && !fnSignature .stateDeclarations ().isEmpty ();
367+ outputsPerElementTracker .onProcessElement ();
368+
369+ Collection <W > windowsProcessed ;
370+ if (sideInputProcessor != null ) {
371+ windowsProcessed = hasState ? Lists .newArrayList () : Collections .emptyList ();
372+ for (Iterator <? extends WindowedValue <InputT >> it =
373+ sideInputProcessor .handleProcessElement (elem );
374+ it .hasNext (); ) {
375+ WindowedValue <InputT > toProcess = it .next ();
376+ fnRunner .processElement (toProcess );
377+ if (hasState ) {
378+ windowsProcessed .addAll ((Collection <W >) toProcess .getWindows ());
379+ // If the element was blocked, don't register a cleanup timer. The timer will be
380+ // registered
381+ // when the window is unblocked ensuring that it is not processed until the element is.
382+ }
383+ }
384+ } else {
385+ fnRunner .processElement (elem );
386+ windowsProcessed = (Collection <W >) elem .getWindows ();
387+ }
388+ if (hasState ) {
338389 registerStateCleanup (
339- (WindowingStrategy <?, BoundedWindow >) getDoFnInfo ().getWindowingStrategy (),
340- (Collection <BoundedWindow >) elem .getWindows ());
390+ (WindowingStrategy <?, W >) getDoFnInfo ().getWindowingStrategy (), windowsProcessed );
341391 }
342-
343- outputsPerElementTracker .onProcessElement ();
344- fnRunner .processElement (elem );
345392 outputsPerElementTracker .onProcessElementSuccess ();
346393 }
347394
@@ -367,6 +414,9 @@ private void processUserTimer(TimerData timer) throws Exception {
367414 if (fnSignature .timerDeclarations ().containsKey (timer .getTimerId ())
368415 || fnSignature .timerFamilyDeclarations ().containsKey (timer .getTimerFamilyId ())) {
369416 BoundedWindow window = ((WindowNamespace ) timer .getNamespace ()).getWindow ();
417+ if (sideInputProcessor != null ) {
418+ sideInputProcessor .handleProcessTimer (timer );
419+ }
370420 fnRunner .onTimer (
371421 timer .getTimerId (),
372422 timer .getTimerFamilyId (),
@@ -380,7 +430,6 @@ private void processUserTimer(TimerData timer) throws Exception {
380430 }
381431
382432 private void processSystemTimer (TimerData timer ) throws Exception {
383-
384433 // Timer owned by this class, for cleaning up state in expired windows
385434 if (timer .getTimerId ().equals (CLEANUP_TIMER_ID )) {
386435 checkState (
@@ -396,6 +445,13 @@ private void processSystemTimer(TimerData timer) throws Exception {
396445 WindowNamespace .class .getSimpleName (),
397446 timer );
398447
448+ if (sideInputProcessor != null ) {
449+ // We must call this to ensure the side-input is cached for onWindowExpiration. Since we
450+ // don't set cleanup
451+ // timers until we actually call processElement, the window must be unblocked here.
452+ sideInputProcessor .handleProcessTimer (timer );
453+ }
454+
399455 BoundedWindow window = ((WindowNamespace ) timer .getNamespace ()).getWindow ();
400456 Instant targetTime = earliestAllowableCleanupTime (window , fnInfo .getWindowingStrategy ());
401457
@@ -436,10 +492,14 @@ private void processSystemTimer(TimerData timer) throws Exception {
436492 public void finishBundle () throws Exception {
437493 if (fnRunner != null ) {
438494 fnRunner .finishBundle ();
495+ if (sideInputProcessor != null ) {
496+ sideInputProcessor .handleFinishBundle ();
497+ }
439498 doFnInstanceManager .complete (fnInfo );
440499 fnRunner = null ;
441500 fnInfo = null ;
442501 fnSignature = null ;
502+ sideInputProcessor = null ;
443503 }
444504 }
445505
@@ -490,7 +550,7 @@ private void processTimers(
490550 }
491551 }
492552
493- private < W extends BoundedWindow > void registerStateCleanup (
553+ private void registerStateCleanup (
494554 WindowingStrategy <?, W > windowingStrategy , Collection <W > windowsToCleanup ) {
495555 Coder <W > windowCoder = windowingStrategy .getWindowFn ().windowCoder ();
496556
0 commit comments