From 8236c31fcdda0eb50338167c2f90ce14ad62ab6a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 18 Sep 2026 02:49:18 -0600 Subject: [PATCH] Revert "fix(billing): one reminder at 1 day, guarded by an advisory lock" Revert "fix(billing): stop the reminder claim from rewriting stale metadata" This reverts commits 037230496 and 14e059784. --- .../server/utils/trial-notifications.ts | 42 +++---------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/apps/dokploy/server/utils/trial-notifications.ts b/apps/dokploy/server/utils/trial-notifications.ts index 090be2748..2a7b9c90e 100644 --- a/apps/dokploy/server/utils/trial-notifications.ts +++ b/apps/dokploy/server/utils/trial-notifications.ts @@ -1,42 +1,21 @@ import { IS_CLOUD } from "@dokploy/server"; -import { db, dbUrl } from "@dokploy/server/db"; +import { db } from "@dokploy/server/db"; import TrialExpiringEmail from "@dokploy/server/emails/emails/trial-expiring"; import { sendEmail } from "@dokploy/server/verification/send-verification-email"; import { render } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { scheduleJob } from "node-schedule"; -import postgres from "postgres"; import type Stripe from "stripe"; import { user } from "@/server/db/schema"; import { getStripeClient, planFromPriceIds } from "@/server/utils/billing"; import { WEBSITE_URL } from "@/server/utils/stripe"; const REMINDER_THRESHOLDS = [ + { days: 3, metadataKey: "trialReminder3Sent" }, { days: 1, metadataKey: "trialReminder1Sent" }, ] as const; -// Cloud runs several replicas and node-schedule fires on each one, so without -// this every trial user would get one email per replica. Advisory locks live -// in the connection rather than the schema, so no migration is needed. -const ADVISORY_LOCK_KEY = 4820260918; - -const withAdvisoryLock = async (run: () => Promise) => { - const sql = postgres(dbUrl, { max: 1 }); - try { - const [row] = - await sql`select pg_try_advisory_lock(${ADVISORY_LOCK_KEY}) as locked`; - if (!row?.locked) return null; - try { - return await run(); - } finally { - await sql`select pg_advisory_unlock(${ADVISORY_LOCK_KEY})`; - } - } finally { - await sql.end({ timeout: 5 }); - } -}; - const PLAN_LABELS: Record = { hobby: "Hobby", startup: "Startup", @@ -115,11 +94,9 @@ const processSubscription = async ( ); const plan = planFromPriceIds(priceIds); - // Marked before sending so a crash mid-send cannot resend on the next run. - // Stripe merges metadata, so only the claimed key travels; spreading the - // snapshot read at list time would rewrite concurrent changes. + // Claim the reminder before sending so concurrent replicas don't double-send. await stripe.subscriptions.update(subscription.id, { - metadata: { [threshold.metadataKey]: "sent" }, + metadata: { ...subscription.metadata, [threshold.metadataKey]: "sent" }, }); try { @@ -132,7 +109,7 @@ const processSubscription = async ( }); } catch (error) { await stripe.subscriptions.update(subscription.id, { - metadata: { [threshold.metadataKey]: "" }, + metadata: { ...subscription.metadata, [threshold.metadataKey]: "" }, }); throw error; } @@ -169,14 +146,7 @@ export const initTrialNotificationsCronJob = () => { scheduleJob("trial-expiration-reminders", "0 14 * * *", async () => { try { - const result = await withAdvisoryLock(processTrialExpirations); - if (!result) { - console.log("Trial reminders: another replica holds the lock"); - return; - } - console.log( - `Trial reminders: scanned ${result.scanned}, sent ${result.sent}`, - ); + await processTrialExpirations(); } catch (error) { console.error( "Trial reminder job failed:",