-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix race condition preventing duplicate commissions in bounty workflow #2945
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReworked the bounty-award flow into a single database transaction that finds or creates the partner submission, updates performanceCount, short-circuits if already approved, evaluates trigger, creates and links a partner commission if needed, updates submission to Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Caller as Workflow Invoker
participant WF as executeAwardBountyWorkflow
participant DB as Database
participant CS as Commission Service
participant ES as Email Service
Caller->>WF: invoke(bounty, partner, payload)
WF->>DB: BEGIN TRANSACTION
WF->>DB: SELECT submission FOR UPDATE (bountyId, partnerId)
alt submission exists
WF->>DB: if status != "approved" -> UPDATE performanceCount (+1)
else no submission
WF->>DB: INSERT submission (performanceCount = 1)
end
WF->>WF: Evaluate trigger using updated performanceCount
alt trigger not met
WF->>DB: COMMIT
WF-->>Caller: return (no-op)
else trigger met
WF->>DB: Check submission.commissionId / status
alt already approved or commission exists
WF->>DB: COMMIT
WF-->>Caller: return (already processed)
else need commission
WF->>CS: Create partner commission
alt commission created
WF->>DB: UPDATE submission { commissionId, status: "approved" }
WF->>DB: COMMIT
WF->>ES: Send approval email
ES-->>WF: Ack
WF-->>Caller: return (approved)
else commission creation fails
WF->>DB: ROLLBACK
WF-->>Caller: return (error)
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (2)
147-153: Evaluate condition against the locked, up-to-date countIf you apply the lock-and-refetch, ensure you evaluate with the latest performanceCount:
-const shouldExecute = evaluateWorkflowCondition({ - condition, - attributes: { - [condition.attribute]: bountySubmission.performanceCount, - }, -}); +const shouldExecute = evaluateWorkflowCondition({ + condition, + attributes: { + [condition.attribute]: lockedSubmission.performanceCount, + }, +});
107-107: Use Serializable isolation to strengthen correctness under concurrencyAdd isolationLevel: Serializable to the interactive transaction to reduce write skew and phantom reads:
// Add at top of file: import { Prisma } from "@dub/prisma/client"; // Change the transaction invocation: const updatedSubmission = await prisma.$transaction( async (tx) => { // ... }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, );This complements the row-level lock and atomic increment.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)
apps/web/lib/api/create-id.ts (1)
createId(66-71)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(93-121)apps/web/lib/partners/create-partner-commission.ts (1)
createPartnerCommission(57-368)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)
73-74: Group eligibility check looks goodUsing bounty.groups.map(groupId) is clear and correct given the include.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)
167-175: Commission creation is outside the DB transaction; risk of orphan/duplicate commissions on rollback/failureIf
createPartnerCommissionsucceeds but the subsequentbountySubmission.updatefails/rolls back, you’ll orphan a commission. Under retries, you can create duplicates. Make the helper transaction‑aware and passdb: txso “create commission + link + approve” is atomic.Apply in this file:
- const { commission } = await createPartnerCommission({ + const { commission } = await createPartnerCommission({ event: "custom", partnerId, programId: bounty.programId, amount: bounty.rewardAmount ?? 0, quantity: 1, description: `Commission for successfully completed "${bounty.name}" bounty.`, skipWorkflow: true, + db: tx, });And update the helper (apps/web/lib/partners/create-partner-commission.ts):
// imports import { prisma } from "@dub/prisma"; import type { Prisma, PrismaClient } from "@dub/prisma"; // signature export const createPartnerCommission = async ( props: CreatePartnerCommissionProps & { db?: Prisma.TransactionClient | PrismaClient }, ) => { const db = props.db ?? prisma; // use `db` for all DB writes // e.g., // const commission = await db.commission.create({ data: { ... }, include: { customer: true } }); // keep side effects (webhooks/notifications) out of the tx if possible, // or gate them to run after commit from the caller. };Optional hardening: add an idempotency guard (e.g., unique key on commission metadata by bountySubmissionId) to prevent duplicate commissions even if retried post‑failure.
🧹 Nitpick comments (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (1)
107-116: Remove redundant pre-read; rely on the upsert result under lock
findUniquehere doesn’t lock and is immediately followed by anupsertthat does lock/update. Drop the pre-read and re-check status/commissionId using thebountySubmissionreturned byupsert.- const existingSubmission = await tx.bountySubmission.findUnique({ - where: { - bountyId_partnerId: { - bountyId, - partnerId, - }, - }, - }); - - if (existingSubmission?.status === "approved") { - console.log( - `Partner ${partnerId} has already been awarded this bounty (bountyId: ${bounty.id}, submissionId: ${existingSubmission.id}).`, - ); - return; - }Then right after the upsert:
const bountySubmission = await tx.bountySubmission.upsert({ /* ... */ }); + if (bountySubmission.status === "approved" || bountySubmission.commissionId) { + console.log( + `Bounty submission ${bountySubmission.id} already approved or has commission ${bountySubmission.commissionId}.`, + ); + return; + }Also applies to: 117-123
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)
apps/web/lib/api/create-id.ts (1)
createId(66-71)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(93-121)apps/web/lib/partners/create-partner-commission.ts (1)
createPartnerCommission(57-368)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/lib/api/workflows/execute-award-bounty-workflow.ts (3)
140-142: Good: atomic increment avoids lost updatesUsing
{ increment: performanceCount }fixes the lost‑update race onperformanceCount.
107-107: Optional: enforce Serializable isolation on the transaction
Further reduces concurrency anomalies (e.g., write skew). If your Prisma client version supports it, update the call to:import { Prisma } from "@prisma/client"; const updatedSubmission = await prisma.$transaction( async (tx) => { /* ... */ }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, );Confirm your installed Prisma version supports
TransactionIsolationLevel.Serializablebefore applying.
105-106: No change needed for performanceCount Δ handling
performanceCountis sourced fromcontext.current, which producers set as per-event deltas (e.g.leads: 1,saleAmount: amount), so using{ increment: performanceCount }is correct.
Summary by CodeRabbit
Bug Fixes
Refactor
Notifications