Skip to content

feat(llm,rooms): model-affinity scheduler and full join-queue wiring (Phase 16) - #25

Merged
withrvr merged 2 commits into
masterfrom
phase-16-scale-and-polish
Sep 16, 2026
Merged

withrvr merged 2 commits into
masterfrom
phase-16-scale-and-polish

Conversation

@withrvr

@withrvr withrvr commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Two pieces of Phase 16 ("Scale and polish"), both scoped with the owner before building:

  • Model-affinity scheduler (DECISIONS.md §13): ModelAffinityScheduler, bounded per-base_url admission control so LLM calls sharing one Ollama backend don't all fire at once and thrash model swaps. Wired transparently into ModelRegistry.client_for() via a new _ScheduledClient wrapper.
  • Join queue, fully wired (docs/rooms.md and docs/capacity-and-queueing.md had both documented this as unbuilt since Phase 7): room-seat offers with anti-steal protection (a new "offered" seat status), global concurrent-game queueing with self-promotion when a slot frees, and frontend UI for queue position/ETA and accepting an offered seat.

Two real bugs found and fixed by the tests written for this feature (not assumed correct):

  1. A pre-existing Redis TTL race in JoinQueue's offer reclaim (the offer key's own automatic expiry raced sweep()'s explicit reclaim for the same deadline) — never caught before because no existing test exercised a real wall-clock wait.
  2. RoomService.start() left a Postgres advisory lock held after raising, which deadlocked a second session trying to acquire the same lock — a genuine ~90s hang under a real timeout guard, not just a slow test.

Left for later, explicitly (not silently): no standalone leader-elected sweeper task, no seat_offer/queue_update push events — both were Phase 7's original plan. Built instead: opportunistic reclaim (a join retry or a queue-status poll both sweep as a side effect) and REST polling. Documented as a real, bounded gap in docs/capacity-and-queueing.md.

Test plan

  • Backend: uv run pytest tests — full suite green (new tests for the scheduler, the queue wiring, and both bugs above)
  • uv run prek run --all-files clean (ruff, mypy, ty, biome, zizmor, SDK generation)
  • Frontend: bun run build and bun run lint clean
  • Full Playwright E2E suite (60/60) against the real Docker stack, including a new three-browser-context test exercising the full queue → offer → accept flow end to end

…(Phase 16)

Two pieces: the model-swap-aware LLM scheduler DECISIONS.md #13 had
flagged for this phase (ModelAffinityScheduler, bounded per-base_url
admission control), and the join queue's real seat/room wiring that
docs/rooms.md and docs/capacity-and-queueing.md had documented as
unbuilt since Phase 7 -- room-seat offers with anti-steal protection,
global concurrent-game queueing with self-promotion, and frontend UI
for queue position/ETA and accepting an offered seat.

Found and fixed two real bugs along the way: a pre-existing Redis TTL
race in JoinQueue's offer reclaim (never caught before because no test
exercised a real wall-clock wait), and an advisory-lock leak in
RoomService.start() that deadlocked a second session trying to acquire
the same lock after the first raised without releasing it.

Claude-Session: https://claude.ai/code/session_01RdYr89Z6wPoS79MWe7VAkC
python-reviewer (2 HIGH, confirmed with a live repro):
- ModelAffinityScheduler.release() woke a waiter unconditionally, even
  when the released model still had other in-flight calls occupying
  its slot -- the woken waiter then self-admitted with no capacity
  re-check, letting the active set exceed max_loaded. Now only wakes
  when a model's count actually reaches zero, and acquire() re-checks
  capacity under the lock after being woken instead of blindly
  admitting.
- force_start()'s human->agent seat backfill was staged, not
  committed, alongside start()'s own final commit -- but start() can
  now enqueue-and-roll-back on CapacityExceededError instead of only
  ever raising outright, and _promote_next_queued_room retries that
  exact room from a different session later. An uncommitted backfill
  was invisible to that retry, silently and permanently orphaning the
  room in the global queue. Now committed immediately in force_start().
  _promote_next_queued_room also logs a suppressed retry failure
  instead of swallowing it silently.

Also fixed: a real fairness window where leave()/_reclaim_expired_offers()
wrote a freed seat to "open" before offering it to the queue, letting a
concurrent non-queued joiner win it out from under the queue (MEDIUM);
a `status` local shadowing the imported `fastapi.status` module (LOW).

react-reviewer: missing aria-live on the queue/offer status panels in
JoinByCode and the room detail page -- a screen reader user got no
announcement when a seat became available (HIGH, accessibility).

Claude-Session: https://claude.ai/code/session_01RdYr89Z6wPoS79MWe7VAkC
@withrvr
withrvr merged commit 7604230 into master Sep 16, 2026
26 of 32 checks passed
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.

1 participant