-
Notifications
You must be signed in to change notification settings - Fork 0
SMS workflow reminder retry count tracking #3
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
base: sms-retry-base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,10 +28,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { | |||||||||||||||||||||||
| //delete all scheduled sms reminders where scheduled date is past current date | ||||||||||||||||||||||||
| await prisma.workflowReminder.deleteMany({ | ||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||
| method: WorkflowMethods.SMS, | ||||||||||||||||||||||||
| scheduledDate: { | ||||||||||||||||||||||||
| lte: dayjs().toISOString(), | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| OR: [ | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| method: WorkflowMethods.SMS, | ||||||||||||||||||||||||
| scheduledDate: { | ||||||||||||||||||||||||
| lte: dayjs().toISOString(), | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| retryCount: { | ||||||||||||||||||||||||
| gt: 1, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 98% |
||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — Intent alignment (confidence: 84%) The deletion threshold is Evidence:
Agent: logic |
||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business Logic Correctness (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code organization (confidence: 100%) The deleteMany OR clause deletes reminders with retryCount > 1 regardless of method type. Non-SMS reminders could be unintentionally deleted if they accumulate retryCount > 1, since the second OR branch lacks a method filter. Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business logic correctness (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Off-by-one / Retry threshold (confidence: 95%) The deletion threshold Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 95% |
||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business logic correctness (confidence: 100%) The second OR branch in deleteMany has no Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — Intent Alignment (confidence: 91%) The PR is linked to Issue #9 'feat: 2fa backup codes', but the code changes are entirely about SMS workflow reminder retry tracking. The code changes are correctly scoped to SMS workflow reminders and contain nothing related to 2FA. The linked issue appears to be incorrect. Evidence:
Agent: logic |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -44,8 +53,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { | |||||||||||||||||||||||
| lte: dayjs().add(7, "day").toISOString(), | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| select, | ||||||||||||||||||||||||
| })) as PartialWorkflowReminder[]; | ||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||
| ...select, | ||||||||||||||||||||||||
| retryCount: true, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| })) as (PartialWorkflowReminder & { retryCount: number })[]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!unscheduledReminders.length) { | ||||||||||||||||||||||||
| res.json({ ok: true }); | ||||||||||||||||||||||||
|
|
@@ -163,9 +175,26 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { | |||||||||||||||||||||||
| referenceId: scheduledSMS.sid, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| await prisma.workflowReminder.update({ | ||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||
| id: reminder.id, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Data Integrity (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 100% |
||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Edge Cases (confidence: 100%) Both the Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -163,19 +163,28 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
- if (scheduledSMS) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- scheduled: true,
- referenceId: scheduledSMS.sid,
- },
- });
- } else {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
- }
- }
- } catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
- console.log(`Error scheduling SMS with error ${error}`);
- }
+ let schedulingFailed = false;
+
+ try {
+ if (scheduledSMS) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ scheduled: true,
+ referenceId: scheduledSMS.sid,
+ },
+ });
+ } else {
+ // Twilio call succeeded but returned no sid — treat as a failed attempt
+ schedulingFailed = true;
+ }
+ } catch (twilioCallError) {
+ // Twilio call itself threw — treat as a failed attempt
+ schedulingFailed = true;
+ console.log(`Error scheduling SMS with error ${twilioCallError}`);
+ }
+
+ // Increment retryCount exactly once if this attempt did not produce a valid sid,
+ // regardless of whether the failure came from Twilio or from the DB update above.
+ if (schedulingFailed) {
+ try {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ } catch (retryUpdateError) {
+ console.log(`Error incrementing retryCount for reminder ${reminder.id}: ${retryUpdateError}`);
+ }
+ }
+ }
+ }
}🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||
| retryCount: reminder.retryCount + 1, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Error handling (confidence: 79%) The retryCount increment in the else branch (no SID returned — soft failure) is outside the try/catch block's protection. If this Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 79% |
||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — Code patterns (confidence: 88%) Duplicate retry logic: both the else branch (no sid) and catch block independently increment retryCount. If an exception is thrown after the sid check passes but before the update, retryCount could be incremented twice. Evidence:
Agent: style |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Data flow / Type safety (confidence: 89%) The code uses Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -179,7 +179,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
- retryCount: reminder.retryCount + 1,
+ retryCount: { increment: 1 },
},
});
}
@@ -190,7 +190,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
- retryCount: reminder.retryCount + 1,
+ retryCount: { increment: 1 },
},
});
console.log(`Error scheduling SMS with error ${error}`);🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||
| await prisma.workflowReminder.update({ | ||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||
| id: reminder.id, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Error Handling (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -189,9 +189,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
- console.log(`Error scheduling SMS with error ${error}`);
+ console.log(`Error scheduling SMS with error ${error}`);
+ // The reminder may have been concurrently deleted by the cleanup sweep
+ // (deleteMany removes reminders with retryCount > 1). Wrap this update
+ // in its own try-catch so a "record not found" error here doesn't crash
+ // the entire cron loop and skip remaining unscheduled reminders.
+ try {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ } catch (updateError) {
+ console.log(
+ `Error incrementing retryCount for reminder ${reminder.id} (may have been deleted): ${updateError}`
+ );
+ }
}
}🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||
| retryCount: reminder.retryCount + 1, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Error handling (confidence: 100%) The retryCount increment in the catch block has no error handling of its own. If the database is unavailable (which is a plausible cause of the original exception), this Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Business logic correctness (confidence: 99%) If the else-branch (no SID) retryCount update at line 176-184 itself throws an exception, execution falls to the catch block at line 186, which ALSO increments retryCount using the same stale in-memory value ( Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Race conditions (confidence: 95%) Both retryCount increments use the in-memory value Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -189,8 +189,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
console.log(`Error scheduling SMS with error ${error}`);
+ try {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ } catch (retryUpdateError) {
+ console.log(
+ `Error incrementing retryCount for reminder ${reminder.id} after scheduling failure: ${retryUpdateError}`
+ );
+ }
}
}🤖 Grapple PR auto-fix • major • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -163,6 +163,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
for (const reminder of unscheduledReminders) {
+ let retryCountUpdated = false;
try {
// ... existing scheduling logic ...
@@ -175,10 +176,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
referenceId: scheduledSMS.sid,
},
});
- } else {
+ } else {
+ // No SID returned — soft failure, increment retry count
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
retryCount: reminder.retryCount + 1,
},
});
+ retryCountUpdated = true;
}
} catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
+ // Only increment retryCount if the else-branch didn't already succeed.
+ // Both paths use `reminder.retryCount + 1` (stale in-memory value); if the
+ // else-branch already persisted N+1 to the DB and then an unrelated error
+ // fires here, we must NOT overwrite with N+1 again — that would silently
+ // swallow one failure increment.
+ if (!retryCountUpdated) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ }
console.log(`Error scheduling SMS with error ${error}`);
}
}🤖 Grapple PR auto-fix • major • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -179,7 +179,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} else {
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
- retryCount: reminder.retryCount + 1,
+ retryCount: { increment: 1 },
},
});
}
@@ -190,7 +190,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
- retryCount: reminder.retryCount + 1,
+ retryCount: { increment: 1 },
},
});
console.log(`Error scheduling SMS with error ${error}`);🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| console.log(`Error scheduling SMS with error ${error}`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Error handling / Double increment (confidence: 98%) When Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Error handling resilience (confidence: 89%) The catch block's Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -163,22 +163,34 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
+ let scheduledSuccessfully = false;
+ let incrementRetryCount = false;
+
if (scheduledSMS) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- scheduled: true,
- referenceId: scheduledSMS.sid,
- },
- });
- } else {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
+ if (scheduledSMS.sid) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ scheduled: true,
+ referenceId: scheduledSMS.sid,
+ },
+ });
+ scheduledSuccessfully = true;
+ } else {
+ // scheduledSMS returned but without a sid — treat as a failure
+ incrementRetryCount = true;
+ }
+ } else {
+ // Twilio returned no scheduledSMS object — treat as a failure
+ incrementRetryCount = true;
}
+
+ if (incrementRetryCount) {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ }
}
} catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
console.log(`Error scheduling SMS with error ${error}`);
+ // Only increment retryCount if we haven't already done so inside the try block.
+ // Using a DB-level increment here avoids the stale in-memory value race:
+ // if the else-branch update succeeded before some later statement threw,
+ // we must not overwrite it back to the stale value.
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: {
+ increment: 1,
+ },
+ },
+ });
}
}🤖 Grapple PR auto-fix • critical • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts
@@ -189,10 +189,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} catch (error) {
- await prisma.workflowReminder.update({
- where: {
- id: reminder.id,
- },
- data: {
- retryCount: reminder.retryCount + 1,
- },
- });
- console.log(`Error scheduling SMS with error ${error}`);
+ console.log(`Error scheduling SMS with error ${error}`);
+ try {
+ await prisma.workflowReminder.update({
+ where: {
+ id: reminder.id,
+ },
+ data: {
+ retryCount: reminder.retryCount + 1,
+ },
+ });
+ } catch (updateError) {
+ // If the DB is also unreachable, log and continue so remaining reminders are still processed
+ console.log(`Failed to update retryCount for reminder ${reminder.id} with error ${updateError}`);
+ }
}
}🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "WorkflowReminder" ADD COLUMN "retryCount" INTEGER NOT NULL DEFAULT 0; |
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.
🟡 MINOR — Intent Alignment (confidence: 98%)
The deletion threshold
retryCount > 1(i.e.,gt: 1, meaning retryCount >= 2) means a reminder must fail at least twice before being cleaned up. The intent says 'retryCount greater than 1' which matches the code, but this means a reminder that failed once (retryCount === 1) will be retried. Confirm this is the desired threshold — if the intent is 'retry once then delete',gt: 1(delete at 2+) is correct. If the intent is 'delete after first failure', it should begt: 0orgte: 1.Evidence:
gt: 1which translates to retryCount >= 2Agent: logic