fix(webapp): two quick votes on the same slot no longer race into a 500 (#844) #847

Merged
claude-bot merged 4 commits from fix/vote-race-and-start-commit into main 2026-09-26 16:58:02 +00:00
Collaborator

Closes #844.

The bug: vote_service.upsert_vote read the existing vote, then inserted or updated. Two PUT …/timeslots/{id}/vote requests for the same slot and user could both see no row. The second insert then hit uq_votes_slot_user as an IntegrityError, which came back as a 500.

The fix: one postgresql.insert(Vote).on_conflict_do_update(constraint="uq_votes_slot_user", …), the same pattern as notification_settings_service and the transcript-feedback endpoint. It re-reads with populate_existing, and the router's side effects are unchanged.

Tests:

  • test_votes.py::test_two_concurrent_votes_for_the_same_slot_and_user_both_succeed runs two real sessions on separate connections. It failed before the fix with the exact IntegrityError.
  • 133 targeted tests pass, and ruff is clean.

Also (#845, investigated, no code change): start_by_recording already commits before the response. A new two-connection regression test proves a fresh reader sees in_progress straight away. #845 stays open, since the one J4 observation has another cause.

🤖 Generated with Claude Code

Closes #844. **The bug:** `vote_service.upsert_vote` read the existing vote, then inserted or updated. Two `PUT …/timeslots/{id}/vote` requests for the same slot and user could both see no row. The second insert then hit `uq_votes_slot_user` as an IntegrityError, which came back as a 500. **The fix:** one `postgresql.insert(Vote).on_conflict_do_update(constraint="uq_votes_slot_user", …)`, the same pattern as `notification_settings_service` and the transcript-feedback endpoint. It re-reads with `populate_existing`, and the router's side effects are unchanged. **Tests:** - `test_votes.py::test_two_concurrent_votes_for_the_same_slot_and_user_both_succeed` runs two real sessions on separate connections. It failed before the fix with the exact IntegrityError. - 133 targeted tests pass, and ruff is clean. **Also (#845, investigated, no code change):** `start_by_recording` already commits before the response. A new two-connection regression test proves a fresh reader sees `in_progress` straight away. #845 stays open, since the one J4 observation has another cause. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
vote_service.upsert_vote raced a SELECT-then-INSERT: two PUTs for the same
slot and user could both miss the existing row and both try to insert,
so the loser hit uq_votes_slot_user as a real IntegrityError (a 500 at the
router). Replaced it with one INSERT ... ON CONFLICT DO UPDATE on that
constraint via SQLAlchemy's postgresql.insert(...).on_conflict_do_update,
keeping the same return shape and side effects.

Investigated #845 (a GET right after record/start allegedly still reading
"confirmed"): session_service.start_by_recording already calls db.commit()
itself before the router returns, rather than relying on get_db's
post-yield commit. No code change there; added a regression test with two
independent connections proving the commit lands before a fresh read.

Tests: two-connection concurrency test for the vote race (confirmed it
fails with IntegrityError against the old code) and for the start-commit
timing (passes unmodified, as evidence). ruff format/check clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: restore the Fixed heading the previous commit took out
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Has been cancelled
CI / Bot tests and audit (pull_request) Has been cancelled
CI / Backend lint (ruff) (pull_request) Has been cancelled
CI / Feedback site tests (pull_request) Has been cancelled
CI / Bot/backend version sync (pull_request) Has been cancelled
CI / Docker image build (pull_request) Has been cancelled
CI / Does this change touch the recording path? (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
a1647325c4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/vote-race-and-start-commit
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Bot tests and audit (pull_request) Successful in 2m0s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Feedback site tests (pull_request) Successful in 43s
CI / Docker image build (pull_request) Successful in 18s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Does this change touch the recording path? (pull_request) Successful in 25s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Frontend tests, audit, and build (pull_request) Successful in 18m44s
CI / Backend migration, tests, and audit (pull_request) Successful in 35m9s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 15m31s
ff4f105463
# Conflicts:
#	CHANGELOG.md
Sign in to join this conversation.
No description provided.