Two verified defects where a single user action can 500 or stall the whole site. Both live in the app container, which runs a single uvicorn worker — so a blocked event loop is a full outage, not a slow request.
Line numbers current as of origin/main @ b7edcbc (2026-07-30).
Suggested order: V5 → C2.
PR V5 — Concurrent first-action IntegrityError handling (small)
waitlist_submit (public.py:485-503, unique email) and review_proposal (agent_page.py:440-466, unique uq_proposal_reviews_decision_agent) do SELECT-then-INSERT with no IntegrityError catch → two concurrent first-time actions race and one 500s.
The codebase already has the correct pattern twice: the vote endpoint (public.py:1041) and the PI web-message writer (agent_page.py:804-812, which rolls back and retries once). Fix: mirror the vote endpoint's catch-and-recover in both handlers.
PR C2 — Move blocking provisioning I/O off the web loop (medium)
admin_provision_slack / admin_provision_slack_callback (admin.py:907/934) are async def and await start_provisioning/complete_provisioning, which internally call synchronous httpx.post plus time.sleep(wait) with up to 5 retries (slack_provisioning.py:107/128/144). There is no to_thread anywhere in src/ (verified 2026-07-30). A rate-limited Slack manifest create therefore freezes every web request for up to ~5 minutes on the single worker, and the get_db session is held open across the whole blocking call.
Fix: offload the blocking calls to asyncio.to_thread; commit/close the session before external I/O.
Definition of done: each PR ships a test that fails against the pre-fix code — for V5, a concurrent-insert test; for C2, an assertion that the handler does not block the loop.
Two verified defects where a single user action can 500 or stall the whole site. Both live in the
appcontainer, which runs a single uvicorn worker — so a blocked event loop is a full outage, not a slow request.Line numbers current as of
origin/main@b7edcbc(2026-07-30).Suggested order: V5 → C2.
PR V5 — Concurrent first-action IntegrityError handling (small)
waitlist_submit(public.py:485-503, uniqueemail) andreview_proposal(agent_page.py:440-466, uniqueuq_proposal_reviews_decision_agent) do SELECT-then-INSERT with noIntegrityErrorcatch → two concurrent first-time actions race and one 500s.The codebase already has the correct pattern twice: the vote endpoint (
public.py:1041) and the PI web-message writer (agent_page.py:804-812, which rolls back and retries once). Fix: mirror the vote endpoint's catch-and-recover in both handlers.PR C2 — Move blocking provisioning I/O off the web loop (medium)
admin_provision_slack/admin_provision_slack_callback(admin.py:907/934) areasync defandawaitstart_provisioning/complete_provisioning, which internally call synchronoushttpx.postplustime.sleep(wait)with up to 5 retries (slack_provisioning.py:107/128/144). There is noto_threadanywhere insrc/(verified 2026-07-30). A rate-limited Slack manifest create therefore freezes every web request for up to ~5 minutes on the single worker, and theget_dbsession is held open across the whole blocking call.Fix: offload the blocking calls to
asyncio.to_thread; commit/close the session before external I/O.nginx.conf'sproxy_read_timeout 120sconverts this freeze into a 504 for the admin doing the provisioning — see PR I5 in the issue Deploy, CI & coverage gate: no GitHub Actions, fake healthcheck, unpinned deps, stale nginx (5 PRs) #27. Fixing either alone leaves the other symptom.Definition of done: each PR ships a test that fails against the pre-fix code — for V5, a concurrent-insert test; for C2, an assertion that the handler does not block the loop.