Skip to content

fix(scheduler): isolate per-subscriber DB commit to prevent duplicate emails (#1118)#1134

Open
Twenitrix wants to merge 3 commits into
imDarshanGK:mainfrom
Twenitrix:fix/weekly-digest-rollback
Open

fix(scheduler): isolate per-subscriber DB commit to prevent duplicate emails (#1118)#1134
Twenitrix wants to merge 3 commits into
imDarshanGK:mainfrom
Twenitrix:fix/weekly-digest-rollback

Conversation

@Twenitrix

@Twenitrix Twenitrix commented Jun 27, 2026

Copy link
Copy Markdown

Description

This PR resolves the database transaction rollback and duplicate email risk in the weekly Sunday digest background job (_send_weekly_digests()).

Currently, the job executes the dispatch loop for all active subscribers and performs a single db.commit() at the very end. If a failure (such as an SMTP timeout, database error, or runtime exception during stats calculation) occurs mid-loop, the entire database transaction is rolled back. However, any emails already sent cannot be unsent. On the next scheduled run, those subscribers receive duplicate emails.

Fixes:

  • Moved db.commit() inside the loop to run immediately after each successfully sent email.
  • Wrapped the per-subscriber loop iteration in its own try/except block with db.rollback() on failure.
  • A failure on one subscriber now rolls back only that subscriber's dirty state, logs the exception, and safely continues to the next subscriber in the batch.
  • Added a failed counter and improved the run summary log.

Related Issue

Fixes #1118


Type of change

  • Bug fix
  • New feature / enhancement
  • Documentation update
  • Test addition
  • Refactor

Checklist

  • I have read CONTRIBUTING.md
  • My branch is up to date with main
  • I have run pytest -v and all tests pass
  • I have not introduced duplicate issues or features
  • My PR title follows the format: feat/fix/docs/test: short description
  • I have added tests for new features (Level 2 and 3 issues)
  • No hardcoded secrets or API keys in my code
  • This PR is linked to a GSSoC 2026 issue

Screenshots (if frontend change)

N/A


Test evidence

Executed the digest test suite using Python 3.12:

python -m pytest tests/test_digest.py -v

============================= test session starts =============================
platform win32 -- Python 3.12.10, pytest-9.1.1, pluggy-1.6.0 -- C:\Users\divya\AppData\Local\Programs\Python\Python312\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\divya\Desktop\gssoc\AI-dev-assistant
configfile: pyproject.toml
plugins: anyio-4.14.1
collecting ... collected 15 items

tests/test_digest.py::test_subscribe_success PASSED                      [  6%]
tests/test_digest.py::test_subscribe_duplicate_returns_409 PASSED        [ 13%]
tests/test_digest.py::test_subscribe_re_activates_after_unsubscribe PASSED [ 20%]
tests/test_digest.py::test_unsubscribe_success PASSED                    [ 26%]
tests/test_digest.py::test_unsubscribe_wrong_token PASSED                [ 33%]
tests/test_digest.py::test_unsubscribe_nonexistent PASSED                [ 40%]
tests/test_digest.py::test_get_unsubscribe_link PASSED                   [ 46%]
tests/test_digest.py::test_invalid_email PASSED                          [ 53%]
tests/test_digest.py::test_subscribe_stores_token PASSED                 [ 60%]
tests/test_digest.py::test_digest_email_uses_mounted_unsubscribe_route PASSED [ 66%]
tests/test_digest.py::test_unsubscribe_url_handles_base_url_slashes[https://qyverixai.onrender.com-https://qyverixai.onrender.com/subscribe/unsubscribe] PASSED [ 73%]
tests/test_digest.py::test_unsubscribe_url_handles_base_url_slashes[https://qyverixai.onrender.com/-https://qyverixai.onrender.com/subscribe/unsubscribe] PASSED [ 80%]
tests/test_digest.py::test_unsubscribe_url_encodes_query_parameters PASSED [ 86%]
tests/test_digest.py::test_digest_partial_smtp_failure_commits_successful_subscribers PASSED [ 93%]
tests/test_digest.py::test_digest_stats_exception_does_not_abort_other_subscribers PASSED [100%]

====================== 15 passed, 105 warnings in 2.76s =======================

… emails on failure (imDarshanGK#1118)

- Move db.commit() inside the loop immediately after each successful send
- Wrap each subscriber in its own try/except with db.rollback() on failure
- One subscriber failing no longer aborts the rest of the batch
- Added failed counter and improved run summary log line
- Add 2 regression tests: partial SMTP failure isolation and stats exception isolation (order-independent)
@Twenitrix Twenitrix requested a review from imDarshanGK as a code owner June 27, 2026 15:00
Copilot AI review requested due to automatic review settings June 27, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a reliability bug in the weekly Sunday digest scheduler by preventing a mid-batch failure from rolling back last_sent_at updates for subscribers whose emails were already sent, which previously risked duplicate digests on the next run.

Changes:

  • Move the database commit for last_sent_at updates to occur per subscriber after a successful send.
  • Add per-subscriber exception isolation with rollback + logging so one bad subscriber doesn’t abort the whole batch.
  • Add regression tests that simulate mid-loop failures (SMTP error / stats exception) and assert partial progress is committed.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
backend/app/services/scheduler.py Commits last_sent_at per subscriber and isolates failures to prevent batch-wide rollback and duplicate emails.
backend/tests/test_digest.py Adds regression tests validating partial commit behavior when one subscriber fails mid-run.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/app/services/scheduler.py Outdated
Comment on lines +38 to +42
for sub in subs:
stats = compute_subscriber_stats(db, sub.email)
if not stats:
log.debug("No stats for %s, skipping", sub.email)
continue
ok = send_digest(stats, sub.unsubscribe_token)
if ok:
sub.last_sent_at = datetime.now(UTC)
sent += 1
else:
log.warning("Failed to deliver digest to %s", sub.email)
try:
stats = compute_subscriber_stats(db, sub.email)
if not stats:
log.debug("No stats for %s, skipping", sub.email)

@imDarshanGK imDarshanGK left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

👋 This PR has had no activity for 7 days.

Please push updates or comment if you still need more time.

Inactive PRs may be closed automatically after 7 more days.

@github-actions github-actions Bot added the stale label Jul 7, 2026
@Twenitrix

Copy link
Copy Markdown
Author

I am still working on it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Weekly digest background job transaction rollback and duplicate email risk

3 participants