fix(copi): prevent duplicate votes via unique constraint and safe upsert - #3332
Open
prajakta128 wants to merge 1 commit into
Open
fix(copi): prevent duplicate votes via unique constraint and safe upsert#3332prajakta128 wants to merge 1 commit into
prajakta128 wants to merge 1 commit into
Conversation
Signed-off-by: Prajakta <prajuu2812@gmail.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a race condition in the Copi voting system by enforcing uniqueness at the database layer and switching vote toggles to safe, conflict-tolerant writes, preventing duplicate votes under concurrent requests.
Changes:
- Adds a unique index on
votes(player_id, dealt_card_id)to enforce one vote per player per dealt card. - Updates
toggle_continue_voteto delete via key-basedRepo.delete_all/2and insert withon_conflict: :nothing+conflict_target. - Updates card voting (
handle_toggle_vote) to use the samedelete_all+ safe insert pattern with the new unique index as the conflict target.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| copi.owasp.org/priv/repo/migrations/20260807120000_add_unique_constraint_to_votes.exs | Adds a DB-level unique index to prevent duplicate card votes. |
| copi.owasp.org/lib/copi_web/live/player_live/show.ex | Reworks vote toggling to use atomic delete-by-keys and safe upserts under concurrency. |
Suppressed comments (1)
copi.owasp.org/lib/copi_web/live/player_live/show.ex:244
- With
on_conflict: :nothing,Repo.insert/2can return{:ok, struct}even when no row was inserted (conflict/no-op). Logging "Vote added successfully" in the{:ok, _vote}branch is therefore potentially misleading and makes debugging race conditions harder. Consider explicitly distinguishing the no-op conflict case (e.g.,id == nil) from a true insert.
{:ok, _vote} ->
Logger.debug("Vote added successfully for player_id: #{player.id}, dealt_card_id: #{card_id}, game_id: #{game.id}")
{:error, changeset} ->
Logger.warning("Voting failed for player_id: #{inspect(player.id)}, dealt_card_id: #{inspect(card_id)}, game_id: #{inspect(game.id)}, errors: #{inspect(changeset.errors)}")
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1
to
+7
| defmodule Copi.Repo.Migrations.AddUniqueConstraintToVotes do | ||
| use Ecto.Migration | ||
|
|
||
| def change do | ||
| create unique_index(:votes, [:player_id, :dealt_card_id], name: :votes_player_id_dealt_card_id_index) | ||
| end | ||
| end No newline at end of file |
Comment on lines
171
to
+175
| Logger.debug("Adding continue vote for player_id: #{player.id}, game_id: #{game.id}") | ||
| Copi.Repo.insert(%Copi.Cornucopia.ContinueVote{player_id: player.id, game_id: game.id}) | ||
|
|
||
| Copi.Repo.insert( | ||
| %Copi.Cornucopia.ContinueVote{player_id: player.id, game_id: game.id}, | ||
| on_conflict: :nothing, |
Comment on lines
+235
to
+239
| case Copi.Repo.insert( | ||
| %Copi.Cornucopia.Vote{dealt_card_id: card_id, player_id: player.id}, | ||
| on_conflict: :nothing, | ||
| conflict_target: [:player_id, :dealt_card_id] | ||
| ) do |
Collaborator
|
@prajakta128 Please have a look at the comments |
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.
Resolves #2288
Summary
Both
toggle_voteandtoggle_continue_voteused a check-then-act pattern (check if a vote exists, then separately insert or delete) with no database constraint backing it up. Concurrent requests could both pass the check and create duplicate votes.continue_votesalready had a unique constraint on(player_id, game_id), butvoteshad none, and both handlers still relied on the racy in-memory check rather than the database.Changes
Scoped to two files:
votes(player_id, dealt_card_id)toggle_continue_vote: replacedEnum.find+delete!withRepo.delete_allmatching on the actual keys (handling{n, _} when n > 0, not just{1, _}), and replaced the plaininsertwithon_conflict: :nothing, conflict_target: [:player_id, :game_id]handle_toggle_vote: same pattern applied to card voting, using the newvotesunique constraint asconflict_targetTesting
Existing tests for both handlers continue to pass unchanged — their assertions check the resulting vote count/state, not the specific error path, so they remain valid under the new atomic-upsert behavior. Opening as a draft PR to let CI confirm before requesting review.
Related
Two prior attempts (#2289, #2291) addressed this correctly in principle but were closed after months due to scope creep (rate limiting, an unrelated game-deletion handler, Python test file changes) and unresolved review comments (
coveralls.jsonedits,delete_allmatching only{1, _}). This PR is scoped strictly to the migration and the two handlers.@sydseter — you mentioned a migration script was already finished;
I wasn't able to locate it, so I've written one myself here. Happy to align with yours if it differs, just let me know.