Skip to content

Keep progress renewal heartbeats active through bundle teardown - #40020

Draft
kennknowles wants to merge 4 commits into
apache:masterfrom
kennknowles:LeaseRenewal
Draft

Keep progress renewal heartbeats active through bundle teardown#40020
kennknowles wants to merge 4 commits into
apache:masterfrom
kennknowles:LeaseRenewal

Conversation

@kennknowles

Copy link
Copy Markdown
Member

BatchDataflowWorker shut down WorkProgressUpdater in the finally block of executeWork() before calling workItemStatusClient.reportSuccess(). If the worker thread was delayed in closing connections, flushing shuffle state, or serializing counters and metrics, no lease renewals could be sent, allowing the 180s work item lease to expire on the Dataflow service.

Furthermore, reportSuccess() and reportUpdate() in WorkItemStatusClient both synchronized on the same monitor lock, blocking any concurrent lease renewals while the worker thread was assembling final metrics.

To fix this:

  1. Extend progress reporting in BatchDataflowWorker so progressUpdater stays active until reportSuccess() completes, while reporting any unreported dynamic split before success.
  2. Decouple monitor locking in WorkItemStatusClient so counter and metric extraction run outside the RPC lock, using a dedicated metricsLock to serialize metric extraction and commits.
  3. Support lightweight lease renewal pings without heavyweight metric serialization to keep leases alive under high CPU/memory utilization.
  4. Gracefully handle in-flight non-final status updates in execute() if a final completion state was already sent.

Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

GitHub Actions Tests Status (on master branch)

Build python source distribution and wheels
Python tests
Java tests
Go tests

See CI.md for more information about GitHub Actions CI or the workflows README to see a list of phrases to trigger workflows.

BatchDataflowWorker shut down WorkProgressUpdater in the finally block
of executeWork() before calling workItemStatusClient.reportSuccess().
If the worker thread was delayed in closing connections, flushing
shuffle state, or serializing counters and metrics, no lease renewals
could be sent, allowing the 180s work item lease to expire on the
Dataflow service.

Furthermore, reportSuccess() and reportUpdate() in WorkItemStatusClient
both synchronized on the same monitor lock, blocking any concurrent
lease renewals while the worker thread was assembling final metrics.

To fix this:
1. Extend progress reporting in BatchDataflowWorker so progressUpdater
   stays active until reportSuccess() completes, while reporting any
   unreported dynamic split before success.
2. Decouple monitor locking in WorkItemStatusClient so counter and
   metric extraction run outside the RPC lock, using a dedicated
   metricsLock to serialize metric extraction and commits.
3. Support lightweight lease renewal pings without heavyweight
   metric serialization to keep leases alive under high CPU/memory
   utilization.
4. Gracefully handle in-flight non-final status updates in execute() if
   a final completion state was already sent.
* WorkItemStatusClient#reportSuccess} completes, lease renewal heartbeats continue even if bundle
* completion or status reporting encounters delays, preventing lease expiration.
*/
void executeWork(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use method overrides. They are a human-oriented stylistic choice that obscures readability. Give the method a meaningful name that indicates why it takes more parameters.

*/
public @Nullable WorkItemServiceState reportLeasePing(Duration requestedLeaseDuration)
throws Exception {
checkState(worker != null, "setWorker should be called before reportLeasePing");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use checkStateNotNull

if (finalStateSent) {
return null;
}
checkArgument(requestedLeaseDuration != null, "requestLeaseDuration must be non-null");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use checkArgumentNotNull


private synchronized @Nullable WorkItemServiceState execute(WorkItemStatus status)
throws IOException {
if (finalStateSent && !Boolean.TRUE.equals(status.getCompleted())) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use Boolean.TRUE.equals. getCompleted() is already a boolean.

- Rename executeWork overload taking WorkItemStatusClient to
  executeWorkAndReportSuccess in BatchDataflowWorker, clarifying that it
  executes work and completes reporting while progress heartbeats are
  active.
- Use Preconditions.checkStateNotNull and checkArgumentNotNull in
  WorkItemStatusClient#reportLeasePing.
- Avoid Boolean.TRUE.equals when checking status.getCompleted() in
  WorkItemStatusClient#execute.
- Add unit tests for reportLeasePing precondition validations.
The previous change attempted to split synchronization in
WorkItemStatusClient via a dedicated metricsLock to allow progress
heartbeats during success reporting. However, splitting the lock
violated the single-reader invariant of MetricsContainerImpl and
BatchModeExecutionContext, introduced a data race on lastReportedMillis,
dropped destructively-drained metric updates if progress updates raced
reportSuccess(), and caused TOCTOU IllegalStateExceptions. Furthermore,
reportLeasePing was dead code never invoked by the worker lifecycle.

To address this while fulfilling the requirement to keep leases alive
until reportSuccess completes:

1. Remove metricsLock and restore synchronization on
   WorkItemStatusClient's intrinsic monitor across extract, RPC, and
   commit. Because the Dataflow service strictly sequences reportIndex
   per WorkItem, concurrent status RPCs are invalid and monitor
   serialization is required.
2. Hold the monitor across the extract-RPC-commit sequence so at most
   one thread extracts or commits metrics, preserving single-reader
   invariants and preventing dropped counter and metric updates.
3. Wire reportLeasePing into the worker lifecycle: once
   BatchDataflowWorker completes compute and dynamic splitting, it sets
   leaseRenewalOnly(true) on the progress updater. Any heartbeat
   occurring during final bundle teardown and reportSuccess preparation
   sends a lightweight lease ping without metric extraction overhead.
4. Treat finalStateSent gracefully in reportUpdate, reportLeasePing, and
   execute, returning null without throwing IllegalStateException or
   logging spurious errors when a heartbeat races reportSuccess.
5. Add multi-threaded concurrency tests in LeaseRenewalRaceTest to
   verify that races between reportSuccess and progress updates or lease
   pings do not throw exceptions or lose metrics.
Revert redundant internal synchronization in DataflowWorkProgressUpdater
and restore executor visibility to private in WorkProgressUpdater.
Because WorkProgressUpdater's caller methods already hold the monitor
on executor, internal synchronized blocks were reentrant no-ops that
caused SpotBugs warnings.

Additionally:
- Use a volatile boolean for leaseRenewalOnly so status updates can be
  switched to lightweight lease renewal pings after execute() finishes.
- In reportProgressHelper(), do not bypass reporting an uncommitted
  dynamic split if dynamicSplitResultToReport is present when in lease
  renewal mode.
- Reduce reportLeasePing visibility to package-private.
- Rename concurrency test to
  drainedMetricUpdatesSurviveConcurrentReportSuccess and remove the
  vestigial latch wait that added artificial test suite delay.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant