You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: Add idempotency guard to notification scheduler
Add last_notified_date column to notification_channels table and update
scheduler pseudo-code with atomic check-and-set to prevent duplicate
digests when multiple backend instances run or when the job re-fires
within the same minute. Failed deliveries leave last_notified_date
unchanged so the channel is retried on the next tick.
Addresses PR review comment about missing idempotency protection.
https://claude.ai/code/session_01VP4wi9RRFsCDgBbbY5xkCL
logger.error({ err, channelExternalId: channel.external_id }, 'Failed to dispatch notification');
355
370
Sentry.captureException(err);
371
+
// last_notified_date is NOT updated on failure, so the channel will be retried next minute
356
372
}
357
373
}
358
374
});
359
375
```
360
376
361
-
A new PgTyped query `getEnabledChannelsDueAt` is needed that selects from `system.notification_channels` where `is_enabled = true` and `notify_time = :notifyTime`, joining to `auth.users` to retrieve `user_external_id` and the user's `language` preference (extracted from `preferences->'language'`, defaulting to `'en'`).
377
+
**Idempotency:** The `last_notified_date` column ensures each channel receives at most one digest per calendar day (UTC). The `getEnabledChannelsDueAt` query filters out channels where `last_notified_date` already matches today's date, making the scheduler safe to run across multiple backend instances. A separate `markChannelNotified` PgTyped query updates the column:
378
+
379
+
```sql
380
+
/* @name MarkChannelNotified */
381
+
UPDATEsystem.notification_channels
382
+
SET last_notified_date = :today
383
+
WHERE id = :channelId
384
+
AND (last_notified_date IS NULLOR last_notified_date < :today::date);
385
+
```
386
+
387
+
The `WHERE` guard in the UPDATE makes the mark itself idempotent — concurrent instances racing to mark the same channel will not conflict. If dispatch fails, `last_notified_date` is left unchanged so the scheduler retries on the next minute tick until either delivery succeeds or the day rolls over.
388
+
389
+
A new PgTyped query `getEnabledChannelsDueAt` is needed that selects from `system.notification_channels` where `is_enabled = true`, `notify_time = :notifyTime`, and `(last_notified_date IS NULL OR last_notified_date < :today::date)`, joining to `auth.users` to retrieve `user_external_id` and the user's `language` preference (extracted from `preferences->'language'`, defaulting to `'en'`). The `last_notified_date` filter is the idempotency guard that prevents duplicate notifications.
362
390
363
391
**Important:** The scheduler pseudo-code above calls `getUpcomingDates` with `maxDays` and `limitCount` parameters. The existing `GetUpcomingDates` query in `friend-dates.sql` already accepts these exact parameters (`maxDays` for the lookahead window, `limitCount` for the result limit), so no modification to the existing query is needed. The scheduler reuses it as-is — the only difference from the frontend dashboard usage is that different values are passed at call time (per-channel `lookahead_days` rather than a hardcoded default). A new query is **not** required; the existing one is fully compatible.
0 commit comments