Skip to content

Commit aeafcb2

Browse files
committed
[Python] Add Watch tests for terminal and replay round watermarks
The replay round leaves the watermark at the seed and reports no residual. A run that defers twice before completing keeps the watermark where the last poll left it and ends without a residual.
1 parent c559963 commit aeafcb2

1 file changed

Lines changed: 51 additions & 4 deletions

File tree

sdks/python/apache_beam/io/watch_test.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,17 +284,20 @@ def _windowed_group(kv, window=beam.DoFn.WindowParam):
284284

285285

286286
class 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

324371
class WatchEndToEndTest(unittest.TestCase):
325372
def _in_memory_pipeline(self):

0 commit comments

Comments
 (0)