fix(recommendations): replace TOCTOU count+update with atomic RPC claim#224
Open
anshul23102 wants to merge 1 commit into
Open
fix(recommendations): replace TOCTOU count+update with atomic RPC claim#224anshul23102 wants to merge 1 commit into
anshul23102 wants to merge 1 commit into
Conversation
claimRecommendation enforced the 3-active-claim limit using a SELECT COUNT
followed by a separate UPDATE. Because these were two independent database
round-trips, concurrent requests from the same user could both pass the
count check before either write committed, allowing a user to hold more
than 3 simultaneous claims.
Fix:
- Adds supabase/migrations/0012_claim_recommendation_atomic.sql, which
creates the PostgreSQL function claim_recommendation_atomic. The function
runs the count check and the status flip as a single UPDATE ... WHERE
(SELECT count(*) ...) < 3 statement. PostgreSQL row-level locking ensures
concurrent calls serialise correctly with no application-level race window.
- Updates claimRecommendation to call service.rpc('claim_recommendation_atomic',
...) instead of the two-step COUNT + UPDATE. When the RPC returns zero
rows the action falls back to a single count query to return the correct
claim_limit vs. already_claimed error code to the caller.
- Updates recommendations.test.ts: replaces the old service.from chain mocks
with mockServiceRpc mocks and adds tests for the zero-row claim_limit path,
the zero-row already_claimed path, and the RPC error path.
Closes Coder-s-OG-s#205
Contributor
|
@anshul23102 is attempting to deploy a commit to the codersogs-3057's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Hey @anshul23102 You have 6 open PRs right now. The limit is 3 at a time. Please get your existing PRs merged or closed before opening new ones:
This PR will remain open but won't be reviewed until you're under the limit. See our Contributing Guidelines for details. |
Author
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.
Summary
Fixes #205.
`claimRecommendation` enforced the 3-active-claim limit with a SELECT COUNT followed by a separate UPDATE. Because these were two independent database round-trips, concurrent requests from the same user could both read a count below 3 before either write committed, allowing a user to hold more than 3 simultaneous claims.
Root cause
```typescript
// Step 1: read — concurrent requests both see count = 2
const { count: claimedCount } = await service.from('recommendations')
.select('id', { count: 'exact', head: true })
.eq('user_id', user.id)
.eq('status', 'claimed');
// Race window: another request sneaks in here
// Step 2: write — both proceed because they both passed Step 1
const { data } = await service.from('recommendations')
.update({ status: 'claimed', ... })
...
```
Fix
Migration (
supabase/migrations/0012_claim_recommendation_atomic.sql)A new PostgreSQL function
claim_recommendation_atomicruns the count check and the status flip in a singleUPDATE ... WHERE (SELECT count(*) ...) < 3statement. PostgreSQL row-level locking ensures concurrent invocations serialise correctly.Action (
src/app/actions/recommendations.ts)claimRecommendationnow callsservice.rpc('claim_recommendation_atomic', { p_rec_id, p_user_id }). When the RPC returns zero rows, a single count query distinguishes betweenclaim_limit(at cap) andalready_claimed(rec not open) for correct error codes.Tests (
src/app/actions/recommendations.test.ts)mockServiceRpcadded to the service mock.claimRecommendationtests updated to use the RPC mock.claim_limitpath, zero-rowalready_claimedpath, and RPC error path.Test plan
npx vitest run src/app/actions/recommendations.test.ts- 21/21 passnpx tsc --noEmit- no errorsnpx eslint src/app/actions/recommendations.ts src/app/actions/recommendations.test.ts- no errors