From 2743721e932c62423a80e6d90ada6a183946b89c Mon Sep 17 00:00:00 2001 From: Swapnil Bhattacharya Date: Sun, 23 Feb 2025 20:07:26 +0530 Subject: [PATCH] Fix QStash reminder scheduling logic --- controllers/workflow.controller.js | 69 ++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/controllers/workflow.controller.js b/controllers/workflow.controller.js index a70b5dc..762e819 100644 --- a/controllers/workflow.controller.js +++ b/controllers/workflow.controller.js @@ -1,48 +1,73 @@ -import dayjs from 'dayjs' -import { createRequire } from 'module'; +import dayjs from "dayjs"; +import { createRequire } from "module"; const require = createRequire(import.meta.url); const { serve } = require("@upstash/workflow/express"); -import Subscription from '../models/subscription.model.js'; -import { sendReminderEmail } from '../utils/send-email.js' +import Subscription from "../models/subscription.model.js"; +import { sendReminderEmail } from "../utils/send-email.js"; -const REMINDERS = [7, 5, 2, 1] +const REMINDERS = [7, 5, 2, 1]; // IN DAYS export const sendReminders = serve(async (context) => { const { subscriptionId } = context.requestPayload; const subscription = await fetchSubscription(context, subscriptionId); - if(!subscription || subscription.status !== 'active') return; + if (!subscription || subscription.status !== "active") return; // STOP WORKFLOW IF SUBSCRIPTION IS NOT ACTIVE const renewalDate = dayjs(subscription.renewalDate); - if(renewalDate.isBefore(dayjs())) { - console.log(`Renewal date has passed for subscription ${subscriptionId}. Stopping workflow.`); + // STOP WORKFLOW IF RENEWAL DATE HAS PASSED + if (renewalDate.isBefore(dayjs())) { + console.log( + `Renewal date has passed for subscription ${subscriptionId}. Stopping workflow...` + ); return; } for (const daysBefore of REMINDERS) { - const reminderDate = renewalDate.subtract(daysBefore, 'day'); + const reminderDate = renewalDate.subtract(daysBefore, "day"); - if(reminderDate.isAfter(dayjs())) { - await sleepUntilReminder(context, `Reminder ${daysBefore} days before`, reminderDate); + // SEND REMINDERS UNTIL RENEWAL DATE + if (reminderDate.isAfter(dayjs())) { + await sleepUntilReminder( + context, + `Reminder ${daysBefore} days before`, + reminderDate + ); } - if (dayjs().isSame(reminderDate, 'day')) { - await triggerReminder(context, `${daysBefore} days before reminder`, subscription); + // TRIGGER REMINDER ON RENEWAL DATE + if (dayjs().isSame(reminderDate, "day")) { + await triggerReminder( + context, + `${daysBefore} days before reminder`, + subscription + ); } } }); const fetchSubscription = async (context, subscriptionId) => { - return await context.run('get subscription', async () => { - return Subscription.findById(subscriptionId).populate('user', 'name email'); - }) -} + return await context.run("get subscription", async () => { + return Subscription.findById(subscriptionId).populate("user", "name email"); + }); +}; const sleepUntilReminder = async (context, label, date) => { + const now = dayjs(); + const delayInSeconds = date.diff(now, "second"); + + // QStash MAX_DELAY is 11.5 days + if (delayInSeconds > 1_000_000) { + // IF REMINDER IS MORE THAN 11.5 DAYS, SKIP SLEEP + console.warn( + `Skipping sleep for ${label} because it's beyond QStash max delay (11.5 days).` + ); + return; + } + console.log(`Sleeping until ${label} reminder at ${date}`); - await context.sleepUntil(label, date.toDate()); -} + await context.sleepUntil(label, date.toDate()); // SLEEP UNTIL RENEWAL DATE +}; const triggerReminder = async (context, label, subscription) => { return await context.run(label, async () => { @@ -52,6 +77,6 @@ const triggerReminder = async (context, label, subscription) => { to: subscription.user.email, type: label, subscription, - }) - }) -} \ No newline at end of file + }); + }); +};