Repository navigation
fix(capacity): make slot admission atomic across workers - #3306
Conversation
|
Resolve by running |
# Conflicts: # tests/registry.json
|
merge-train (mechanical): merged One non-blocking note for a follow-up: |
|
merge-train: ejected after the batch gate — rides the next train once fixed. On the train (and on this branch alone, after the
Which held-id semantics is right is the author's call: either keep |
Resolve tests/registry.json: rebuilt from the merge stages, keeping every dev entry plus this PR's test_atomic_slot_admission.py entry (first). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tomic admission dev's edge-case suites (Abilityai#3327) pin that re-acquiring an already-held execution id under the cap returns True and is counted once. The atomic admission returned False, which CapacityManager reads as capacity-full (queue_persistent would re-queue a running row; reject would fail it). acquire_slot now returns True for a held id under the cap. The decision stays inside the WATCH/MULTI/EXEC step: the held path EXECs a read-only transaction, so 'held and under the cap' is validated against the same watched keys as a new admission and retried on WatchError. Nothing is written - the slot is not renewed and its metadata is not overwritten. At the cap a held id still returns False (it counts itself), as on dev. Tests: - test_r14 (Abilityai#3318) is fixed by this PR: drop the strict xfail and hook the pipeline's capacity read, so the interleaving still runs (and asserts it ran). - test_atomic_slot_admission.py: duplicate-id cases updated to the idempotent contract (every caller True, one member, metadata untouched). Refs Abilityai#3305, Abilityai#3318 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed two commits to clear the merge-train ejection:
@alex-furman this reverses a choice you made on purpose (duplicate id returns Also: r14 (#3318) no longer needs its strict Local results: the three ejected files 78 passed; |
…on retry loop Two findings from independent validation of the atomic admission. 1. A held id re-acquired under the cap now takes the same atomic write as a new admission, as dev does: ZADD re-anchors the score to now and the metadata hash + TTL are rewritten, inside the same WATCH/MULTI/EXEC. The previous read-only held path returned True on a lease dated from the original admission, so a re-dispatch could be reaped mid-run (the Abilityai#2433 class), and a held slot whose metadata had expired stayed without it. The member is still counted once; at the cap a held id still returns False and writes nothing (dev's contract, pinned by the p1 oracle). 2. The retry loop is bounded by MAX_ADMISSION_ATTEMPTS (32). redis-py reports a connection lost on a WATCHing pipeline as WatchError, so a connection that kept dying at EXEC retried forever, synchronously, on the event loop. Exhaustion raises SlotAdmissionError (a RedisError): an undecided admission never returns True or False. Tests: the connection-loss cases inject the fault from the connection's send inside the real Pipeline.execute (the old test replaced execute() wholesale and asserted a ConnectionError production cannot raise from there); new cases pin the bound, the single-loss retry, the refresh and the at-cap refusal. Refs Abilityai#3305 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train: one more commit pushed, 1. A held id now takes the same write as a new admission (corrects 2. The retry loop is bounded. redis-py reports a connection or timeout error on a watched pipeline as 3. The connection-loss test and doc sentence now say what happens. The old test replaced @alex-furman two consequences worth your eye, neither changed here:
Results: the three slot files 82 passed; 76 unit files touching |
vybe
left a comment
There was a problem hiding this comment.
merge-train: batch validated on train PR #3409. Admission is dev's logic made atomic under WATCH/MULTI/EXEC, with a bounded retry; mutation testing confirmed the atomicity. Open notes for the author are in the two merge-train comments above.
…ebuilt, both entries kept Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…son rebuilt, all entries kept Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem and change
Concurrent backend workers can each observe spare capacity and authorize dispatch past max_parallel_tasks. A repeated execution ID can also authorize another dispatch and overwrite the original metadata. This was reproduced on unmodified dev a72271d with six independent processes against Redis7.
SlotService now uses WATCH/MULTI/EXEC to serialize the capacity/membership decision and slot metadata writes. Duplicate IDs return False. Metadata type validation is inside the watched decision because Redis transactions provide isolation, not rollback on command errors. A lost EXEC response never authorizes dispatch, although an already-committed reservation may remain for reconciliation.
The existing backlog sentinel handoff is preserved: a competing admission can win the release/reacquire gap, and BacklogService returns the claimed row to its queue. Documentation and the test registry describe the boundary and how to run the real-Redis cases.
Validation and review
Reproduce the focused suite using the normal test dependencies plus a disposable Redis:
Tested with Python3.13, redis-py5.2.1, fakeredis2.39.0 and Redis7. Without TEST_SLOT_REDIS_URL, three process-contention cases skip; concurrent independent-client tests still run. No model calls or live agents are required.
Boundary
This fixes atomic admission, not complete physical-execution exclusion. It does not change stale-slot cleanup, remote termination, Redis-loss recovery or uncertain-result handling. Those lifecycle concerns remain separate; this PR must not be represented as resolving them.
Fixes #3305
Journey Impact: extends: J04
Mutation: test_independent_clients_never_over_admit, test_concurrent_processes_never_over_admit and test_duplicate_cannot_authorize_dispatch_or_replace_metadata fail against stock. The wrong-type metadata regression also caught a partial transaction in the initial candidate.