@@ -284,17 +284,20 @@ def _windowed_group(kv, window=beam.DoFn.WindowParam):
284284
285285
286286class WatchDoFnProcessTest (unittest .TestCase ):
287- def _process (self , poll_fn , element , timestamp ):
287+ def _process (
288+ self , poll_fn , element , timestamp , restriction = None , watermark = None ):
288289 dofn = _WatchGrowthDoFn (
289290 poll_fn ,
290291 never (),
291292 Duration (1 ),
292293 StrUtf8Coder (),
293294 _identity ,
294295 StrUtf8Coder ())
295- threadsafe = ThreadsafeRestrictionTracker (
296- dofn .create_tracker (dofn .initial_restriction (element )))
297- estimator = ThreadsafeWatermarkEstimator (ManualWatermarkEstimator (None ))
296+ if restriction is None :
297+ restriction = dofn .initial_restriction (element )
298+ threadsafe = ThreadsafeRestrictionTracker (dofn .create_tracker (restriction ))
299+ estimator = ThreadsafeWatermarkEstimator (
300+ ManualWatermarkEstimator (watermark ))
298301 outputs = list (
299302 dofn .process (
300303 element ,
@@ -320,6 +323,50 @@ def test_complete_round_stops_without_residual(self):
320323 self .assertIsNone (threadsafe .deferred_status ())
321324 self .assertTrue (threadsafe .check_done ())
322325
326+ def test_replay_round_leaves_the_watermark_alone (self ):
327+ pending = PollResult ((_ts ('k:a' , 1 ), _ts ('k:b' , 2 )), MAX_TIMESTAMP )
328+ outputs , threadsafe , estimator = self ._process (
329+ _empty_poll ,
330+ 'k:' ,
331+ Timestamp (7 ),
332+ restriction = _NonPollingGrowthState (pending ))
333+ self .assertEqual ([('k:' , 'k:a' ), ('k:' , 'k:b' )],
334+ [value .value for value in outputs ])
335+ # The replay branch holds the watermark at the seed, so it never runs ahead
336+ # of the replayed outputs and never releases to MAX_TIMESTAMP itself.
337+ self .assertEqual (Timestamp (7 ), estimator .current_watermark ())
338+ self .assertIsNone (threadsafe .deferred_status ())
339+ self .assertTrue (threadsafe .check_done ())
340+
341+ def test_terminal_round_after_deferring_leaves_no_residual (self ):
342+ _POLL_CALLS .clear ()
343+ # Round one defers and parks the watermark on the new output's time.
344+ _ , threadsafe , estimator = self ._process (_growing_poll , 'd:' , Timestamp (0 ))
345+ residual , _ = threadsafe .deferred_status ()
346+ self .assertIsInstance (residual , _PollingGrowthState )
347+ self .assertEqual (Timestamp (1 ), estimator .current_watermark ())
348+ # Round two resumes from that residual, carrying the watermark forward.
349+ _ , threadsafe , estimator = self ._process (
350+ _growing_poll ,
351+ 'd:' ,
352+ Timestamp (0 ),
353+ restriction = residual ,
354+ watermark = estimator .current_watermark ())
355+ residual , _ = threadsafe .deferred_status ()
356+ self .assertEqual (Timestamp (2 ), estimator .current_watermark ())
357+ # Round three completes. The watermark stays where round two left it and
358+ # the round reports no residual, so nothing carries that hold forward.
359+ outputs , threadsafe , estimator = self ._process (
360+ _growing_poll ,
361+ 'd:' ,
362+ Timestamp (0 ),
363+ restriction = residual ,
364+ watermark = estimator .current_watermark ())
365+ self .assertEqual ([('d:' , 'd:2' )], [value .value for value in outputs ])
366+ self .assertEqual (Timestamp (2 ), estimator .current_watermark ())
367+ self .assertIsNone (threadsafe .deferred_status ())
368+ self .assertTrue (threadsafe .check_done ())
369+
323370
324371class WatchEndToEndTest (unittest .TestCase ):
325372 def _in_memory_pipeline (self ):
0 commit comments