🚨 Race condition in game with actual video
- Dominant language
- Python
- Stars
- 146
- Forks
- 97
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 104
Description
- **Describe the bug**
A race condition occurs when multiple users (or the same user with rapid clicks) trigger the "Start Game" action at nearly the same time. Because card dealing and the `started_at` timestamp update are not wrapped in a transaction — and there is no unique constraint on `DealtCard` — both requests pass the `game.started_at == nil` check and then both execute the full card-dealing loop. This results in every card from the deck being inserted twice.
**Observed behavior**
- Normal (single click): each player receives exactly 26 unique cards (correct for a 3-player game)
- Race condition triggered (two near-simultaneous "Start Game" clicks): each player receives 52 cards (the entire deck is dealt twice)
**Before**
https://github.com/user-attachments/assets/5f3977f9-92fd-47d9-9d57-685b8ede64d0
**After** (triggered the start game same time with mobile and laptop , mobile is not shown in the video because this is a screen recording)
https://github.com/user-attachments/assets/e84302a9-07bb-42e0-9654-ba7d9b644735
**Expected behavior**
Clicking "Start Game" should deal each card exactly once, mark the game as started (`started_at` set), and prevent any concurrent dealing process from duplicating cards.
**Steps to reproduce**
1. Create/join a game with >2 players
2. Ensure game is ready to start (both players present, deck loaded)
3. Have two browser tabs (or two users) click "Start Game" within ~100–300 ms of each other
4. Check the dealt cards — each player will have 52 cards instead of 26
**Desktop environment**
OS: Windows
Browser: Chrome
Version: 130+ (also reproducible in Edge/Firefox)
**Additional context**
- Root cause: `Repo.insert!` calls in `show.ex` (lines ~77–84) run outside a transaction
- No unique index exists on `dealt_cards (card_id)` or `(card_id, player_id)`
- The `started_at` nil check is not atomic with the inserts
- Fix suggestion: wrap both the card inserts and `update_game` call in `Repo.transaction/1` (as previously shown)
Contributor guide
Assessment
This issue has not been assessed yet.