Skip to content

fix(teams): prevent concurrent slug collision with sequential retry (#499)#557

Open
Ridanshi wants to merge 4 commits into
Dev-Card:mainfrom
Ridanshi:fix/concurrent-team-slug-collision
Open

fix(teams): prevent concurrent slug collision with sequential retry (#499)#557
Ridanshi wants to merge 4 commits into
Dev-Card:mainfrom
Ridanshi:fix/concurrent-team-slug-collision

Conversation

@Ridanshi

Copy link
Copy Markdown
Contributor

Summary

  • Replace non-deterministic Math.random() suffix in generateUniqueSlug with sequential numeric candidates (my-teammy-team-1my-team-2, bounded at 10 attempts)
  • Wrap team creation in a 5-attempt retry loop that catches P2002 constraint violations and re-runs slug allocation instead of returning 409
  • Database @unique constraint on Team.slug is the authoritative guard; application layer now recovers when it fires under concurrent load

Root cause

generateUniqueSlug checked 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

my-team → my-team-1 → my-team-2 → … → my-team-10
  • generateUniqueSlug: deterministic sequential suffixes, max 10 candidates, throws if all taken
  • Route handler: up to 5 full create attempts; returns 409 only after all retries exhausted

Files modified

  • apps/backend/src/utils/slug.ts — sequential suffix generation, bounded retries, typed signature
  • apps/backend/src/routes/team.ts — retry loop on P2002 in POST /, return type annotation, lint fixes
  • apps/backend/src/__tests__/team.test.ts — concurrency retry tests, exhaustion test, import/lint fixes
  • apps/backend/src/__tests__/slug.test.ts — new file: unit tests for createSlug and generateUniqueSlug

Schema changes

None — Team.slug @unique already existed in schema.prisma.

Tests added

  • slug.test.ts: 10 tests covering createSlug normalization and generateUniqueSlug determinism, suffix increment, bounds, concurrency
  • team.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.ts0 errors
  • vitest run src/__tests__/slug.test.ts src/__tests__/team.test.ts57/57 pass

Fixes #499

Harxhit and others added 4 commits May 24, 2026 16:31
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
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@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.

@github-actions

Copy link
Copy Markdown

Hi @Ridanshi,

Thanks for opening this pull request.

This PR has been automatically classified based on the files modified.

Applied Labels

  • backend

Primary Review Area

  • backend

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!

@github-actions

Copy link
Copy Markdown

CI — All Checks Passed

Backend — SKIP

Check Result
Lint -
Test -
Typecheck -

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Check -
Build -

Last updated: Fri, 12 Jun 2026 13:43:07 GMT

@Harxhit Harxhit added the gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. label Jun 13, 2026

@Harxhit Harxhit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts57/57 pass locally.
  • The retry loop (team.ts:45-83) is correctly boundedfor (attempt 0..<5), continue only on P2002, falls through to a 409 after exhaustion. No infinite-loop risk.
  • The exhaustion test asserts $transaction is 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.
  • generateUniqueSlug is deterministic, capped at 10 candidates, and throws cleanly when exhausted (handled with its own 409).
  • Atomic creation: team + owner TeamMember created inside one $transaction, with the DB @unique on Team.slug as 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 tsc failures are pre-existing in unrelated auth.ts).

Suggestions

  1. [major — scope] This is a feature PR wearing a bugfix title. The diff adds the entire Team/TeamMember Prisma model + TeamRole enum, a 400-line teams CRUD router, and validation schemas — not a slug-collision fix. The PR body states "Schema changes: None — Team.slug @unique already existed," but the diff adds the Team model and that constraint (schema.prisma +41). Please re-title/re-describe as "feat(teams): add teams API" or split the slug logic out.
  2. [major — correctness] event.ts was changed but not actually fixed for the race. It was switched to the shared generateUniqueSlug, but the event create path got no P2002 retry loop — only the teams route did. Worse, it replaces the old Math.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 leave event.ts out of this PR.
  3. [medium — lint] The reported "0 errors" excluded the files that fail. Running eslint over all changed files gives 16 errors in event.ts and 4 in app.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.
  4. [minor] Validation copy + typo. createTeamScehma is 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".
  5. [minor] Owner-removal path is a known gap. DELETE /:slug/members/:userId blocks 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.
  6. [nit] Files missing trailing newlines (team.ts, team.validation.ts, schema.prisma), and app.ts has 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.

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

Labels

backend gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent team creation can generate duplicate slugs and cause team namespace collisions

3 participants