Repository navigation
Keep a notice observed while deferring a fleet takeover - #1181
Conversation
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>
| upload_attempt() | ||
|
|
||
| # The watch has stopped, so a notice it observed after the check above is final. | ||
| noticed = watch.interrupted.is_set() |
There was a problem hiding this comment.
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.interruptedseparately; it is now read once after the watch stops (e8b042e). - The test's
firedevent duplicatedpolling(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
noticedis 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, notinterrupted. This only affects whatstatusshows. - Merging the two notice checks in work() would change their conditions and ordering; kept per branch.
- With
stop_eventalready 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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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":
noticedis read after thewith JobWatchblock, whose__exit__joins both threads (without a timeout unlessstop_eventis set). It also covers a notice request in flight at thewatch.interruptedcheck. - "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 theOutcome/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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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()returnsOutcome("deferred", noticed=watch.interrupted.is_set()), reading the event afterJobWatchhas exited and the claim is released, as thelostanddoneoutcomes 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 checkedwatch.interrupted.work()raisesSpotInterruptionErroron a deferred outcome ifoutcome.noticedis set or a fresh check finds a notice. Before, it relied only on the fresh check.Outcomeandwork()docstrings andbenchmarks/README.mddescribe 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 whileJobWatch.__exit__()joined the thread. The deferred outcome dropped that observation. If the fresh metadata check inwork()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 thedoneandlostoutcomes; 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. WithoutUV_PYTHON, the local default interpreter fails to buildgreenlet==3.3.2before the license-header check; this is unrelated to the change.just docs lint: 0 errors.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 thewatch.interruptedcheck; only the watch thread sees the notice, and only once. It passes 3 of 3 runs here (at 100175f). Against theorigin/mastersources (d47bd22), it fails withDID 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