fix(allocator): retry assign_vm when SKIP LOCKED hides a transiently locked seat - #499
Merged
Merged
Conversation
…locked seat `assign_vm` claimed a seat with `FOR UPDATE SKIP LOCKED`, which skips any eligible row another transaction holds a lock on — a client heartbeat or other short update on the final free VM. When the claim returned no row the code raised "no available VMs" and the caller returned 503 no_seats, even though an eligible VM was still running, healthy, and unassigned. Observed live at N=30: VM 9 stayed free and Healthy while seat request 12 got a 503. Keep the atomic claim and SKIP LOCKED. On an empty claim, run an EXISTS check with the identical eligibility clause: raise ValueError (-> 503) only when the pool is genuinely empty, otherwise retry with bounded exponential backoff (10 attempts, 10ms -> 100ms, ~0.65s worst case) so a transient lock resolves instead of turning a student away. Exhausted retries raise RuntimeError rather than a false empty-pool. Add a real-Postgres regression test that locks the only free VM, releases the lock mid-retry, and asserts the assignment then succeeds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 14, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 16, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 16, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 16, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 17, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7174Andy
added a commit
that referenced
this pull request
Sep 17, 2026
data/quarantine/ held the pre-fix N=30 diagnostic run that exposed the #499 bug. Nothing plots it and the paper does not discuss it; the story is recorded in PR #499 and its regression test. data/ is now exactly the four figure runs plus allocator-deploy.json. The README's "Excluded runs" note now points at #499 instead of a directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
assign_vmcould return a false "no seats available" (HTTP 503) while a VM was still running, healthy, and unassigned.FOR UPDATE SKIP LOCKED, which skips any eligible row another transaction is briefly holding a lock on (a client heartbeat or other short update on the last free VM). An empty claim was treated as an empty pool.The bug, observed live
During a 30-VM workshop-scaling run, client VM 9 remained
running,Healthy, and unassigned while seat request 12 received a 503.SKIP LOCKEDhad skipped VM 9's momentarily-locked row and the handler reported the pool empty.Changes Made
packages/allocator/src/lablink_allocator_service/db/vms.py—assign_vm:SELECT EXISTS (... eligibility ...). If a free VM still exists, retry with bounded exponential backoff (10 attempts, 10 ms → 100 ms, ~0.65 s worst case); only raiseValueError(→ 503no_seats) when the pool is confirmed empty.RuntimeErrorrather than a false empty-pool.packages/allocator/tests/db/test_vms.py— real-Postgres regression testtest_assign_vm_retries_temporarily_locked_eligible_vm: locks the only free VM from a second connection, releases the lock mid-retry, and asserts the assignment then succeeds on that exact host.Testing
postgres:16).pytest --ignore=tests/terraform, real Postgres); existing concurrency guarantees (no-double-assignment, oversubscribed) still hold.ruff checkclean on both changed files.g4dn.xlarge, us-west-2), fixed allocator, concurrent claim bursts against a full pool:Design Decisions
RuntimeError, not a silent empty-pool.Related Issues
None filed; discovered during workshop-scaling measurement.