fix: race on POST /api/games (UNIQUE constraint 500)#61
Merged
Conversation
…lation A double-fire on POST /api/games from the same device (front mount effect re-running, double-click, retry under flaky network) was racing the SELECT-then-INSERT pattern and 500-ing with: duplicate key value violates unique constraint "uniq_games_grid_device" When INSERT collides with the UNIQUE (grid_id, device_id) index, we now re-read the winning row and return resume_existing on it — same end-state as the pre-check branch, just resistant to the SELECT-vs-INSERT window. A finished game on the same (grid, device) still surfaces as 409 Conflict instead of resuming; the race fallback only resumes active games. New helper is_unique_violation() pattern-matches the SeaORM error on SQLSTATE 23505 or the index name.
The Svelte mount $effect was firing twice on duel links: loadGrid reads store.state which is itself reactive, so the effect re-subscribes each time startGame mutates the store. Without a guard, two POST /games requests went out in the same tick, the second 500ing on the UNIQUE (grid_id, device_id) DB constraint. - loadStarted flag stops the $effect from re-running once it has kicked off loadGrid the first time. - startGame returns early if it's already in flight (`starting`), defending against any other concurrent caller (retry click, abandon retry, etc.). Backend is now race-tolerant too (resumes on UNIQUE violation), but that's a safety net — the right fix is to not double-fire in the first place.
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.
What
Hitting "Play this grid" on a duel link sometimes returned 500 Internal Server Error:
```
duplicate key value violates unique constraint "uniq_games_grid_device"
```
Two POST `/api/games` requests went out in the same tick, the second collided with the UNIQUE (grid_id, device_id) DB constraint.
Why
Two layers contributed:
How
Backend (`server/src/routes/games.rs`)
Race-tolerant insert: when the INSERT errors with a Postgres unique_violation on the games (grid_id, device_id) index, we re-read the winning row and return `resume_existing` on it — same end-state as the pre-check branch, just resistant to the SELECT/INSERT window. A finished game still surfaces as 409 Conflict; the race fallback only resumes active games.
New helper `is_unique_violation()` pattern-matches the SeaORM error on SQLSTATE `23505` or the index name (string match — SeaORM stringifies the underlying sqlx error).
Frontend (`web/src/lib/components/Grid.svelte`)
Both layers are needed: the front fix removes the actual double-fire, the backend fix is a safety net for any future caller that double-tap-races.
Checklist
Test plan