Skip to content

Keep a notice observed while deferring a fleet takeover - #1181

Merged
laughingman7743 merged 4 commits into
masterfrom
fix/1179-deferred-notice
Oct 14, 2026
Merged

laughingman7743 merged 4 commits into
masterfrom
fix/1179-deferred-notice

Conversation

@laughingman7743

@laughingman7743 laughingman7743 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

WHAT

A deferred takeover in the benchmark fleet now keeps an interruption notice that the lease watch observed (benchmarks/ only; no library change).

  • run_attempt() returns Outcome("deferred", noticed=watch.interrupted.is_set()), reading the event after JobWatch has exited and the claim is released, as the lost and done outcomes already do. The deferred outcome is returned after the watch has stopped, so it also carries a notice whose request was still in flight when the attempt checked watch.interrupted.
  • work() raises SpotInterruptionError on a deferred outcome if outcome.noticed is set or a fresh check finds a notice. Before, it relied only on the fresh check.
  • The Outcome and work() docstrings and benchmarks/README.md describe the deferred case.

WHY

Fixes #1179. When the attempt checked watch.interrupted, the watch's notice request could still be in flight, and it could return true while JobWatch.__exit__() joined the thread. The deferred outcome dropped that observation. If the fresh metadata check in work() then failed or timed out, it returned false. The worker then claimed another job, or returned normally and, with --terminate-host, retired a host that was being reclaimed. #1175 already carries the observation for the done and lost outcomes; this is the remaining case.

TEST

Tested commit: 34e02ac

  • just benchmark test (ruff, ruff format, mypy, cfn-lint, pytest): 248 passed, 1 skipped.
  • UV_PYTHON=3.13 just lint: passed. Without UV_PYTHON, the local default interpreter fails to build greenlet==3.3.2 before the license-header check; this is unrelated to the change.
  • just docs lint: 0 errors.
  • New test test_a_notice_observed_after_a_throttled_takeover_outlasts_a_failed_check. The watch's notice request blocks until the attempt uploads, which happens after the watch.interrupted check; only the watch thread sees the notice, and only once. It passes 3 of 3 runs here (at 100175f). Against the origin/master sources (d47bd22), it fails with DID NOT RAISE SpotInterruptionError: the worker runs the next job instead.

Not run: no fleet run on AWS. These changes are under benchmarks/, so the PyAthena AWS test suites do not exercise them.

🤖 Generated with Claude Code

laughingman7743 and others added 3 commits October 15, 2026 00:17
A deferred takeover now carries the interruption notice that the lease
watch observed after the attempt checked for one, for example from a
request still in flight at that check. work() raises SpotInterruptionError
on it instead of relying only on a fresh metadata check, which can fail
and let the worker claim another job or retire a host being reclaimed.

Fixes #1179

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread benchmarks/pyathena_bench/fleet.py Outdated
upload_attempt()

# The watch has stopped, so a notice it observed after the check above is final.
noticed = watch.interrupted.is_set()

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.

Self-review round one (behavior and implementation). Base d47bd22, initial head 100175f; repairs in e8b042e and 1b13ccf. Covered: run_attempt() returns after the watch, work()'s deferred branch, Outcome/work() docstrings, README, and the new test (it fails on d47bd22 with DID NOT RAISE). Includes /code-review and /simplify, each run on a separate detached worktree.

FINDINGS (repaired):

  • Every return after the watch read watch.interrupted separately; it is now read once after the watch stops (e8b042e).
  • The test's fired event duplicated polling (e8b042e).
  • The test's 0.5 s lease could be given up after about 0.33 s without a renewal on a loaded runner, before the notice request; widened to 2 s (1b13ccf).

Reasoned deferrals:

  • A lost claim with unconfirmed queries still raises RuntimeError before noticed is consulted. Either way the host is kept; Close the remaining gaps in the benchmark fleet's quiescing #1175 deliberately kept that precedence.
  • A deferred takeover with an observed notice ends the claim as released, not interrupted. This only affects what status shows.
  • Merging the two notice checks in work() would change their conditions and ordering; kept per branch.
  • With stop_event already set, __exit__ joins for at most 10 s. A notice check makes two metadata requests with 2 s timeouts each (instance_metadata()), so an in-flight check finishes before that.

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.

Correction to this round-one record: the single read of watch.interrupted (e8b042e) was reverted in 34e02ac. The independent review showed that after a lost claim, the 10 s bounded join can leave a notice check running past that read. Each outcome now reads the event at its return again. The test simplification (fired removed) and the 2 s lease remain.

f"Deferred {job['id']}: Athena throttled quiescing its earlier attempts"
)
if check_notice(notice):
if outcome.noticed or check_notice(notice):

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.

Self-review round two (claims, callers, operators). Base d47bd22, head 1b13ccf. CLEAN for this scope.

Claims checked:

  • "the fresh metadata check ... failed or timed out, it returned false": check_notice() returns False on any exception (fleet.py:1356-1358).
  • "the deferred outcome is returned after the watch has stopped": noticed is read after the with JobWatch block, whose __exit__ joins both threads (without a timeout unless stop_event is set). It also covers a notice request in flight at the watch.interrupted check.
  • "Close the remaining gaps in the benchmark fleet's quiescing #1175 already carries the observation for done and lost": both returns use the same noticed.
  • README (... or exits after an interruption notice, including one observed while it quiesced) and the Outcome/work() docstrings match work()'s deferred branch.

Callers and operators: Outcome gets no new field, and its only consumer is work(). No new AWS or metadata requests; when noticed is set, the fresh check short-circuits. The worker CLI already handles SpotInterruptionError with exit 0 and no retirement.

Validation at 1b13ccf (local): just benchmark test 248 passed, 1 skipped; UV_PYTHON=3.13 just lint passed; just docs lint 0 errors. No fleet run on AWS.

After a lost claim, JobWatch joins its threads for only 10 seconds, so a
notice check can finish after a single early read; each outcome reads the
event last, after releasing its lease.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
break
upload_attempt()

# Each outcome reads the watch's notice last: after a lost claim, the watch

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.

Independent review (relayed): Codex CLI 0.162.1, model gpt-6-astra, codex exec -s read-only, session 01a13b00-f337-7962-a3e8-4929041c85d0. Static review of d47bd22..1b13ccf on an unchanged detached snapshot; no PR number, description, commit messages or prior findings were given. Result: FINDINGS (1).

P2, introduced: "notice snapshot can become stale. When the renewal thread detects claim loss, JobWatch.__exit__ joins each thread for only 10 seconds. An in-flight notice callback can therefore remain running when noticed=False is captured. If it subsequently reports an interruption while abandon() performs its S3 lease release, the returned lost outcome still carries False. ... Preserve the event reads at each outcome return, after lease release."

Verified: the single read added in e8b042e (a /simplify suggestion) happens before abandon() releases the lease. The default notice check needs at most two 2 s metadata requests, which makes the window narrow, but a custom notice has no such bound. Repaired in 34e02ac: each outcome reads watch.interrupted at its return again, after releasing the lease, as at 100175f, with a comment stating why. The reviewer also said the new test "deterministically holds notice completion until after the attempt's interruption check" and exercises the deferred regression; it does not cover the bounded-join case.

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.

Independent follow-up (relayed): Codex gpt-6-astra, codex exec -s read-only, session 01a13b05-afc5-79f1-bc02-5363eddb17cd. Static review of the repair 1b13ccf..34e02ac on an unchanged detached snapshot. Result: CLEAN. "The reported read-before-release race is fixed. ... abandon() now reads watch.interrupted after end_lease() ... Since the event is never cleared, these later reads preserve previously observed notices. Existing work() error precedence remains unchanged." Limit stated by the reviewer: the tests do not deterministically exercise the timed-join and lease-release interleaving.

Validation at 34e02ac (local): just benchmark test 248 passed, 1 skipped.

@laughingman7743
laughingman7743 marked this pull request as ready for review October 14, 2026 15:30
@laughingman7743
laughingman7743 merged commit c0cc145 into master Oct 14, 2026
14 checks passed
@laughingman7743
laughingman7743 deleted the fix/1179-deferred-notice branch October 14, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Benchmark fleet: a deferred takeover discards an observed interruption notice

1 participant