[Hardening] Remove duplicate reminder delivery on session reschedule #90
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Session reminders have two delivery mechanisms that coexist:
poll_session_remindersbeat task, deduplicated bySessionReminderSentrows in Postgres (unique per session/time/offset)._schedule_reminders(webapp/backend/app/services/session_service.py:67) enqueuessend_session_reminderCelery tasks with an ETA (apply_asyncat:120), tracked viasession.celery_task_ids(:145-151), deduplicated by a Redis keyqb:reminder-sent:{session_id}:{confirmed_time}:{hours}(webapp/backend/app/tasks/reminder_tasks.py:35).confirm_session(session_service.py:561) correctly relies on the poller alone — see the comment at:605("Reminders are delivered by the poll_session_reminders beat task"), and the same note at:226.Current behavior
The reschedule path (
session_service.py:397-433) does both:SessionReminderSentrows (:398-414) so the poller will re-fire for the new time, and_schedule_reminders(:423), storing the returned task IDs insession.celery_task_ids(:432), which enqueues legacy ETA tasks.The two paths have independent dedup stores (Postgres row vs Redis key), so neither suppresses the other: every reminder for a rescheduled session is delivered twice (two Discord posts / bot notifications per offset).
Fix / Spec
_schedule_reminders(...)call (and thecelery_task_idsbookkeeping) from the reschedule branch — clearing theSessionReminderSentrows is sufficient; the poller is the system of record, matchingconfirm_session._schedule_remindersandsend_session_reminderand remove them if the poller covers every case (confirm before deleting). Also remove the now-dead task-revocation helper that iteratessession.celery_task_ids(session_service.py:145-151) and the Redis dedup key logic atreminder_tasks.py:35if the task itself is deleted. Leave thecelery_task_idsDB column for a later migration if dropping it is noisy — but stop writing to it.Acceptance criteria
_schedule_reminders/ no ETA-basedsend_session_reminderenqueues (or a documented follow-up if step 3 applied).confirm_sessionbehavior unchanged.References
webapp/backend/app/services/session_service.py:67-151(legacy scheduler + revocation),:397-433(reschedule double-path),:561/:605(confirm path, poller-only)webapp/backend/app/tasks/reminder_tasks.py:35(Redis dedup key),poll_session_remindersat:1027+Filed from the July 2026 full-project review.
Picking this up as part of a v3.3.0 push. Landing on branch
hardening/backendtogether with #88, #97, #106, and #109 (grouped by component to keep the diffs reviewable).Fixed on
main(commit13c2a33, merged via1c9c19f). Legacy ETA path fully retired — the poller (deduped bySessionReminderSentrows) is now the sole delivery mechanism. Removed_schedule_reminders/_revoke_remindersand their call sites, and deletedsend_session_reminder,_claim_reminder_delivery, and the Redis dedup key. Thecelery_task_idscolumn is kept but no longer written.Two things the issue description didn't anticipate:
_schedule_reminderswas also firing the immediate "session confirmed" notice. Deleting the call outright would have silently dropped that announcement on every reschedule. Replaced with_send_confirmation_notice(...), the same pathconfirm_sessionuses.confirm_sessionitself is unchanged.update_sessionallows reschedule fromin_progressand leaves the status asin_progresswhileconfirmed_timemoves into the future, but the poller only selectedstatus == confirmed. Full retirement would have silently dropped reminders for that (frontend-supported) flow, so the poller now selectsconfirmed + in_progress. This is a no-op for genuinely running sessions — theirconfirmed_timeis already older than the 5-minute lookback window, so the ETA-window gate means no offset can fire. (Verified this independently before merging.)Tests: a rescheduled session delivers each offset exactly once, and the legacy ETA task is retired. Deploy note: ETA messages already sitting in the Redis broker at upgrade will be discarded by workers as unregistered tasks (logged, non-fatal) for up to 7 days — which is the desired no-duplicate outcome. Backend suite green (358 passed), ruff clean.