Fix PeriodicImpulse/PeriodicSequence watermark regression (#39026)#39465
Fix PeriodicImpulse/PeriodicSequence watermark regression (#39026)#39465uditjainstjis wants to merge 2 commits into
Conversation
The watermark reported by ImpulseSeqGenDoFn stalled at the last emitted element's timestamp when the SDF deferred to wait for the next fire time. For a side input firing every N seconds, downstream watermark age climbed to N then snapped back (saw-tooth), a regression introduced when process() was rewritten between 2.66.0 and 2.74.0. When deferring because the next scheduled output is in the future, advance the watermark to that next fire time: no element will be produced before it, so it is a valid watermark and restores the pre-2.74.0 behavior. The advance is guarded for pre-timestamped data, whose event times may be out of order, so we never declare future late events droppable. Adds unit tests that drive process() directly: one fails on the unpatched code (watermark stuck at last emitted) and passes after the fix; another asserts the watermark is not advanced past emitted timestamps for pre-timestamped data. Generated-by: Claude (Anthropic AI assistant)
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Assigning reviewers: R: @jrmccluskey for label python. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
We would need end-to-end pipeline evidence to verify this fix worked, like the screenshot in #39026 |
|
Thanks @Abacn, fair request. I don't have a Dataflow project to reproduce the exact screenshot, so instead I reproduced the SDK-reported output watermark itself — the Setup:
(freshness shown the way the runner reports it:
This is the same thing the unit test in the PR asserts (it fails on master with the watermark stuck at the last emitted element, and passes with the fix). Repro — no Dataflow needed, run against a checkout of each branch (full script): from unittest import mock
import apache_beam.transforms.periodicsequence as ps
from apache_beam.io.restriction_trackers import OffsetRange, OffsetRestrictionTracker
from apache_beam.io.watermark_estimators import ManualWatermarkEstimator
from apache_beam.runners.sdf_utils import RestrictionTrackerView, ThreadsafeRestrictionTracker
from apache_beam.transforms.periodicsequence import ImpulseSeqGenDoFn
def reported_watermark(now, start=0.0, interval=60.0):
tracker = ThreadsafeRestrictionTracker(OffsetRestrictionTracker(OffsetRange(0, 10**9)))
est = ManualWatermarkEstimator(None)
with mock.patch.object(ps.time, 'time', return_value=now):
list(ImpulseSeqGenDoFn().process(
(start, start + 10**9, interval),
restriction_tracker=RestrictionTrackerView(tracker),
watermark_estimator=est))
return float(est.current_watermark())
for now in range(0, 121, 30):
wm = reported_watermark(now)
print(now, wm, max(0.0, now - wm))
# master: 0/30/58 -> freshness 0/30/58 (saw-tooth); this PR: freshness stays 0For completeness I also ran a bounded streaming |
|
Thanks, however please refrain from using generated texts. Evidence first:
|
Bisect confirms the watermark-advance-on-defer was dropped by the process() rewrite in apache#35412 (9f431f7), first released in 2.67.0 — not 2.73.0 as the comment previously stated. Generated-by: Claude Signed-off-by: uditjainstjis <uditjainstjis@gmail.com>

Fixes #39026
Problem
After upgrading from 2.66.0 to 2.74.0, streaming jobs using a
PeriodicImpulseside input that fires at long intervals saw the job's watermark age follow a saw-tooth pattern, climbing up to the fire interval before snapping back. Users with watermark-age alerts were paged spuriously.Root cause
process()inImpulseSeqGenDoFnwas rewritten between 2.66.0 and 2.74.0. The old code, after emitting the element at indexk, recomputed the timestamp for indexk+1and set the watermark to that next fire time before deferring — so the watermark sat ~one interval ahead of the last emitted element and the reported age stayed flat.The rewritten code sets the watermark to the just-emitted element's timestamp and never advances it on the defer path. Between fires the watermark is therefore held at the last element's event time, so downstream watermark age grows to
fire_intervaland resets on each fire.Fix
When
process()defers because the next scheduled output is in the future, advance the watermark to that next fire time (monotonic-guarded). No element can be produced before it, so it is a valid watermark, and this restores the pre-2.74.0 behavior.The advance is gated on
not self._is_pre_timestamped: for pre-timestampeddata, event times may be intentionally out of order (a later element can carry an earlier timestamp, treated as a late event per thePeriodicImpulsedocstring), so we must not declare future late events droppable.Testing
Added two unit tests that drive
process()directly with aRestrictionTrackerView+ManualWatermarkEstimator:test_watermark_advances_to_next_fire_on_defer— fails on unpatchedmaster(watermark stuck at last emitted element) and passes with the fix. This is the regression reproduced as a unit test (the Dataflow-side symptom cannot be reproduced in a unit test).test_watermark_not_advanced_past_emitted_for_pre_timestamped— asserts the watermark is not advanced past emitted event times for pre-timestamped data.Full
periodicsequence_test.pypasses locally (26 passed, 1 skipped).yapf==0.43.0clean, pylint 10.00/10.CHANGES.mdwith noteworthy changes.