Skip to content

fix(api): make winner selection atomic and cap winners inside the transaction - #450

Open
MRTEEworld02 wants to merge 4 commits into
geevapp:mainfrom
MRTEEworld02:fix/426-select-winners-toctou
Open

MRTEEworld02 wants to merge 4 commits into
geevapp:mainfrom
MRTEEworld02:fix/426-select-winners-toctou

Conversation

@MRTEEworld02

Copy link
Copy Markdown

Summary

POST /api/posts/[id]/select-winners read the post's status, existing winners and entries, then computed eligibility and ran the "already completed" guard before opening the $transaction. Two concurrent requests could both pass that guard on stale data, each pick a winner set and each flip status to completed. skipDuplicates prevented duplicate PostWinner rows but did not stop the total from exceeding maxWinners (nor duplicate winner notifications).

This moves the read, the guards, the selection and the writes into a single Serializable interactive transaction:

  • The post (status, existing winners, entries) is re-read inside the transaction, and the "already completed" / selectable-status guards run against that read.
  • The post is claimed with an atomic conditional update (updateMany where the status is still open/active/in_progress). A concurrent request that already completed the post affects 0 rows and gets the existing 400 response.
  • remainingSlots = maxWinners - existingWinners is computed inside the transaction; every selection method is capped by it, and the committed PostWinner count is re-checked before writing, so concurrent requests can never collectively over-assign.
  • P2034 serialization failures are surfaced as 409 instead of a generic 500.

Tests

Added app/tests/api/select-winners.test.ts:

  • fires two simultaneous selections against a shared store whose reads rendezvous before either commit, asserting exactly one succeeds and maxWinners is never exceeded;
  • asserts a single selection is capped at the remaining slots;
  • asserts a post that is already completed is rejected with 400.

Notes

This is the assignee's PR for #426 (assignee @MRTEEworld02). PR #449 by macsonfleek is a separate third-party submission on the same issue.

Closes #426

@drips-wave

drips-wave Bot commented Sep 25, 2026

Copy link
Copy Markdown

@MRTEEworld02 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

select-winners reads eligibility and status outside the transaction — TOCTOU race allows over-selection

1 participant