Skip to content

[2.x] fix: Key the notification email dedup lock on the notification, not its type - #4989

Merged
imorland merged 1 commit into
2.xfrom
im/notification-email-dedup-identity
Aug 26, 2026
Merged

imorland merged 1 commit into
2.xfrom
im/notification-email-dedup-identity

Conversation

@imorland

Copy link
Copy Markdown
Member

Changes proposed in this pull request:

NotificationSyncer::sync() queues a SendEmailNotificationJob per recipient on every call, so when sync() fires twice in quick succession for the same notification (Posted then Revised, say) two identical jobs land in the queue for the same recipient. SendEmailNotificationJob guards against that with a short-lived atomic lock — the first job sends, the rest no-op.

That lock was keyed on the notification's type and recipient alone. So a genuinely different notification of the same type to the same user, within the lock's 600s lifetime, was also dropped. The clearest case: a user requests account erasure, cancels it, then requests again — the second request reuses gdpr_erasure_confirm for the same user, hits the still-held lock, and its confirmation email is silently discarded. The user is left waiting for a link that never arrives, with no way to complete the request.

This keys the lock on the notification's identity — type, sender, subject and data, the same attributes Notification::matchingBlueprint() uses to decide whether two blueprints are the same notification:

  • Two jobs for the same notification (the race the guard exists for) still share a key and still dedupe to one email.
  • Two distinct notifications of the same type — a re-request, a second mention, anything carrying different subject/data — now get distinct keys and each sends.

This also brings the email guard into line with the notification-row guard: SendNotificationsJob already dedupes on matchingBlueprint identity, so the row and the email now agree on what counts as the same notification, rather than the email half being coarser.

Reviewers should focus on:

  • That the identity key still collapses the original race — two jobs built from the same event (same subject, same data) send once. There's a test pinning exactly this with two separate blueprint instances.
  • That the identity is derived the same way matchingBlueprint derives it, so the email guard and the row guard can't disagree.

Necessity

  • Has the problem that is being solved here been clearly explained? — a legitimate second notification of the same type to a user was being silently dropped for 600s.
  • If applicable, have various options for solving this problem been considered? — the fix reuses the existing blueprint-identity notion rather than inventing a new discriminator.
  • For core PRs, does this need to be in core, or could it be in an extension? — the dedup lock lives in core's SendEmailNotificationJob.
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation. — no frontend changes.
  • Frontend changes: tests are green — n/a.
  • Frontend changes: tests have been added — n/a.
  • Backend changes: tests are green (run composer test).
  • Backend changes: tests have been added, or are not appropriate here. — a core test that two distinct notifications of one type both email; one that two separate jobs for the same notification still email once; and a gdpr test covering request, cancel, re-request.
  • Where applicable, changes are suitable for all supported database drivers (MySQL, MariaDB, PostgreSQL, SQLite). — the lock is a cache lock; no database involvement.
  • The description above is written by me and describes what this pull request actually does.

Required changes:

  • Related documentation PR: (Remove if irrelevant)

…ts type

SendEmailNotificationJob takes a short-lived lock so that two identical
jobs racing through the queue only send one email. The lock was keyed on
the notification type and recipient alone, so a genuinely different
notification of the same type to the same user within the lock's lifetime
was dropped too - a GDPR erasure request made, cancelled, then made again
never sent its second confirmation email, leaving the user with no link.

Key the lock on the notification's identity instead - type, sender,
subject and data, the same attributes matchingBlueprint uses - so the
notification-row guard and the email guard now agree on what counts as
the same notification. A repeat of the same event is still deduplicated;
a distinct notification of the same type sends as it should.

Adds a core test that two distinct notifications of one type both email,
alongside one proving two separate jobs for the same notification still
email once, and a gdpr test covering request, cancel, re-request.
@imorland
imorland requested a review from a team as a code owner August 26, 2026 10:09
@imorland imorland changed the title Key the notification email dedup lock on the notification, not its type [2.x] fix: Key the notification email dedup lock on the notification, not its type Aug 26, 2026
@imorland imorland added this to the 2.0.0-rc.8 milestone Aug 26, 2026
@imorland
imorland merged commit ad5658d into 2.x Aug 26, 2026
30 checks passed
@imorland
imorland deleted the im/notification-email-dedup-identity branch August 26, 2026 14:57
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.

1 participant