[FLINK-40101][runtime] Emit intermediate watermarks while firing timers#28752
[FLINK-40101][runtime] Emit intermediate watermarks while firing timers#28752pnowojski wants to merge 2 commits into
Conversation
With unaligned checkpoints + interruptible timers, an operator's output watermark could stall for hours (surviving restarts) because it only advances once an entire due-timer backlog drains in one uninterrupted pass — a large backlog (e.g. after a rescale) can outlast every single attempt. InternalTimerServiceImpl/InternalTimeServiceManagerImpl now track the highest watermark known to be fully fired even when interrupted partway, and MailboxWatermarkProcessor emits that as an intermediate watermark instead of withholding all progress. This progress lives in a new field, not currentWatermark, since currentWatermark's eager semantics are relied on elsewhere (WindowOperator cleanup timers, user ProcessFunctions). Emission is paced by a configurable interval (default 5s, 0 disables) via an internal no-op processing-time nudge, avoiding per-timer clock checks.
| ConfigOptions.key( | ||
| "execution.checkpointing.unaligned.interruptible-timers.intermediate-watermark-interval") | ||
| .durationType() | ||
| .defaultValue(Duration.ofSeconds(5)) |
There was a problem hiding this comment.
Isn't 5s a long wait for WM progress?
being X25 pipeline.auto-watermark-interval default
There was a problem hiding this comment.
🤔 Maybe... maybe this should be changed to a boolean flag, and the interval should taken directly from pipeline.auto-watermark-interval?
I've picked 5s default here, because anything > 1s doesn't matter for performance. 200ms would have marginal effect if any, hard to say without benchmarking. However indeed it seems as if re-using here pipeline.auto-watermark-interval makes sense, as it has almsot the same performance implications vs trying to keep up with real time?
Previously I've thought this features matter only if there is a huge backpressure, and huge watermark stall. However if we have a lot of fine grained timers to fire, smaller intervals here can smooth out workload for downstream operators. For example if job is generally speaking keeping up with the pending records, but there is an occasional couple of seconds hiccup when a lot of timers fire.
There was a problem hiding this comment.
I've implemented the change to use auto-watermark-interval - please check fixup commit.
There was a problem hiding this comment.
I'd keep the .intermediate-watermark-interval and only make it equal to pipeline.auto-watermark-interval by default
But either way works
There was a problem hiding this comment.
I think reducing the amount of knobs is a good idea here.
There was a problem hiding this comment.
Future us would appreciate this
| ConfigOptions.key( | ||
| "execution.checkpointing.unaligned.interruptible-timers.emit-intermediate-watermarks") | ||
| .booleanType() | ||
| .defaultValue(true) |
There was a problem hiding this comment.
I think this should be disabled by default for 1 release to minimize potential impact.
| for (InternalTimerServiceImpl<?, ?> service : timerServices.values()) { | ||
| if (!service.tryAdvanceWatermark(watermark.getTimestamp(), shouldStopAdvancingFn)) { | ||
| return false; | ||
| // Once one service is interrupted, stop attempting to fire on the remaining ones this | ||
| // round, but still fold their (possibly stale, from an earlier round) reachedWatermark | ||
| // into the min below: a service we don't retry this round may be even further behind. | ||
| if (fullyAdvanced) { | ||
| fullyAdvanced = | ||
| service.tryAdvanceWatermark( | ||
| watermark.getTimestamp(), shouldStopAdvancingFn); | ||
| } | ||
| } |
There was a problem hiding this comment.
Sorry but I don't understand this loop :)
First, once fullyAdvance is false, we still iterate, but just do nothing? Why not just break?
Second, if the iteration order is the same next round (likely) then we won't update min wm if there are more services with lower wm after the interrupted one.
I think in practice this renders the feature as no-op with high probability.
| if (getContainingTask() | ||
| .getJobConfiguration() | ||
| .get( | ||
| CheckpointingOptions | ||
| .UNALIGNED_INTERRUPTIBLE_TIMERS_EMIT_INTERMEDIATE_WATERMARKS)) { | ||
| timeServiceManager.configureIntermediateWatermarkInterval( | ||
| Duration.ofMillis(getExecutionConfig().getAutoWatermarkInterval())); | ||
| } |
| if (reachedWatermark > lastEmittedIntermediateWatermark) { | ||
| // Firing was interrupted before completing; surface the progress made so far instead | ||
| // of leaving watermark advancement stalled until the whole drain finishes. | ||
| lastEmittedIntermediateWatermark = reachedWatermark; |
There was a problem hiding this comment.
Should we also update lastEmittedIntermediateWatermark in the short-cut branch above?
| // Firing was interrupted before completing; surface the progress made so far instead | ||
| // of leaving watermark advancement stalled until the whole drain finishes. | ||
| lastEmittedIntermediateWatermark = reachedWatermark; | ||
| output.emitWatermark(new Watermark(reachedWatermark)); |
There was a problem hiding this comment.
Can we honor the config option here?
With unaligned checkpoints + interruptible timers, an operator's output watermark could stall for hours (surviving restarts) because it only advances once an entire due-timer backlog drains in one uninterrupted pass — a large backlog (e.g. after a rescale) can outlast every single attempt.
InternalTimerServiceImpl/InternalTimeServiceManagerImpl now track the highest watermark known to be fully fired even when interrupted partway, and MailboxWatermarkProcessor emits that as an intermediate watermark instead of withholding all progress. This progress lives in a new field, not currentWatermark, since currentWatermark's eager semantics are relied on elsewhere (WindowOperator cleanup timers, user ProcessFunctions). Emission is paced by a configurable interval (default 5s, 0 disables) via an internal no-op processing-time nudge, avoiding per-timer clock checks.
Verifying this change
Added new unit tests and is covered by various existing unit/IT tests.
Does this pull request potentially affect one of the following parts:
@Public(Evolving): (yes / no)Documentation
Was generative AI tooling used to co-author this PR?
Yes, claude 5.0