fix(teams): prevent concurrent slug collision with sequential retry (#499)#557
fix(teams): prevent concurrent slug collision with sequential retry (#499)#557Ridanshi wants to merge 4 commits into
Conversation
Signed-off-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
Replace non-deterministic random suffix generation with sequential numeric candidates (my-team → my-team-1 → my-team-2, capped at 10). Wrap team creation in a bounded retry loop (5 attempts) so P2002 constraint violations from concurrent inserts trigger re-allocation rather than surfacing as a 409. The database-level @unique constraint on Team.slug remains the authoritative guard; application logic now recovers gracefully when it fires. Adds slug utility tests (createSlug, generateUniqueSlug determinism and bounds) and team route tests for retry-on-race-condition and retry-exhaustion paths. Closes Dev-Card#499
|
@Ridanshi is attempting to deploy a commit to the Prashantkumar Khatri's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Hi @Ridanshi, Thanks for opening this pull request. This PR has been automatically classified based on the files modified. Applied Labels
Primary Review Area
Reviewer@Harxhit has been identified as the primary reviewer for this pull request. If you have any questions regarding the affected area or implementation details, feel free to reach out to the assigned reviewer. Thank you for your contribution! |
CI — All Checks PassedBackend — SKIP
Mobile — SKIP
Web — SKIP
Last updated: |
Harxhit
left a comment
There was a problem hiding this comment.
Checked this out and tested locally. The core slug-collision fix is genuinely well done — the retry loop is correct and bounded, the optimistic "let the DB unique constraint arbitrate, catch P2002, retry" strategy is the right call, and the tests prove it. My concerns are about everything around that fix.
Verified ✅
vitest run src/__tests__/slug.test.ts src/__tests__/team.test.ts→ 57/57 pass locally.- The retry loop (
team.ts:45-83) is correctly bounded —for (attempt 0..<5),continueonly onP2002, falls through to a 409 after exhaustion. No infinite-loop risk. - The exhaustion test asserts
$transactionis called exactly 5 times, and the race test confirms a P2002 on attempt 1 → regenerate slug → 201 on attempt 2. The bound is real, not just claimed. generateUniqueSlugis deterministic, capped at 10 candidates, and throws cleanly when exhausted (handled with its own 409).- Atomic creation: team + owner
TeamMembercreated inside one$transaction, with the DB@uniqueonTeam.slugas the authoritative guard. Doing the availability check outside the transaction is fine because the P2002 catch covers the TOCTOU window. - No type errors in any file this PR touches (local
tscfailures are pre-existing in unrelatedauth.ts).
Suggestions
- [major — scope] This is a feature PR wearing a bugfix title. The diff adds the entire
Team/TeamMemberPrisma model +TeamRoleenum, a 400-line teams CRUD router, and validation schemas — not a slug-collision fix. The PR body states "Schema changes: None —Team.slug @uniquealready existed," but the diff adds theTeammodel and that constraint (schema.prisma+41). Please re-title/re-describe as "feat(teams): add teams API" or split the slug logic out. - [major — correctness]
event.tswas changed but not actually fixed for the race. It was switched to the sharedgenerateUniqueSlug, but the event create path got no P2002 retry loop — only the teams route did. Worse, it replaces the oldMath.random()suffixes with deterministic-1,-2… suffixes that collide more readily under concurrency, with no recovery. Either give event create the same retry loop, or leaveevent.tsout of this PR. - [medium — lint] The reported "0 errors" excluded the files that fail. Running eslint over all changed files gives 16 errors in
event.tsand 4 inapp.ts(unused vars,import-x/order,prefer-const,object-shorthand). The PR's lint command only covered the 4 new team/slug files. Please run lint over the full changeset and fix before merge. - [minor] Validation copy + typo.
createTeamScehmais misspelled (imported under that name, so it works, but it'll spread). The team schema reuses "Event name must be at least…" messages — should say "Team name". - [minor] Owner-removal path is a known gap.
DELETE /:slug/members/:userIdblocks removing the owner with a//TODO: Assign owner role to next person. Fine to defer, but worth tracking since ownership can't currently be transferred. - [nit] Files missing trailing newlines (
team.ts,team.validation.ts,schema.prisma), andapp.tshas stray blank lines after route registrations.
Net: the slug retry mechanism itself is solid and I'd happily approve that. Holding at "needs attention" for the scope/description mismatch, the unfixed event.ts race, and the lint failures in the modified-but-unlinted files.
Reviewed locally on fix/concurrent-team-slug-collision — ran vitest (57/57), tsc (clean for PR files; pre-existing auth.ts errors unrelated), and eslint across all changed files. No changes pushed.
Summary
Math.random()suffix ingenerateUniqueSlugwith sequential numeric candidates (my-team→my-team-1→my-team-2, bounded at 10 attempts)@uniqueconstraint onTeam.slugis the authoritative guard; application layer now recovers when it fires under concurrent loadRoot cause
generateUniqueSlugchecked slug availability outside the transaction. Two concurrent requests could both observe the same slug as free, both enter$transaction, and the second would get a P2002 error that surfaced as an unrecoverable 409 — leaving one creation silently dead.Concurrency strategy
Optimistic with bounded retry (Option A): rely on the existing DB unique constraint as the true arbiter, detect the collision via P2002, and retry slug generation with the next sequential candidate. No locks, no serializable isolation, no infinite loops.
Retry strategy
generateUniqueSlug: deterministic sequential suffixes, max 10 candidates, throws if all takenFiles modified
apps/backend/src/utils/slug.ts— sequential suffix generation, bounded retries, typed signatureapps/backend/src/routes/team.ts— retry loop on P2002 in POST/, return type annotation, lint fixesapps/backend/src/__tests__/team.test.ts— concurrency retry tests, exhaustion test, import/lint fixesapps/backend/src/__tests__/slug.test.ts— new file: unit tests forcreateSlugandgenerateUniqueSlugSchema changes
None —
Team.slug @uniquealready existed inschema.prisma.Tests added
slug.test.ts: 10 tests coveringcreateSlugnormalization andgenerateUniqueSlugdeterminism, suffix increment, bounds, concurrencyteam.test.ts: retry-on-race-condition (201 after P2002 retry), retry-exhaustion (409 after 5 P2002s)Validation
eslint src/utils/slug.ts src/routes/team.ts src/__tests__/team.test.ts src/__tests__/slug.test.ts→ 0 errorsvitest run src/__tests__/slug.test.ts src/__tests__/team.test.ts→ 57/57 passFixes #499