Skip to content

Fix permanent badge inflation from read receipts before first rotation#19785

Open
stefanceriu wants to merge 1 commit into
element-hq:developfrom
stefanceriu:stefan/fixBadgeInflation
Open

Fix permanent badge inflation from read receipts before first rotation#19785
stefanceriu wants to merge 1 commit into
element-hq:developfrom
stefanceriu:stefan/fixBadgeInflation

Conversation

@stefanceriu
Copy link
Copy Markdown
Member

@stefanceriu stefanceriu commented May 15, 2026

We've had problems with the iOS badge numbers for the longest time, with element-hq/element-x-ios#3151 being our most commented on issue. We were never quite sure what the root cause was nor had time to look into it but so I decided to let Claude have a go. I'm too far away from Synapse to have enough context here but the problem it found (and subsequent regression tests) do make a lot of sense.

If a user reads a room before its first _rotate_notifs cycle, the receipt position is dropped: simple_update_txn no-ops because no event_push_summary row exists yet, then rotation INSERTs the row with last_receipt_stream_ordering=NULL. The badge query treats NULL rows as up-to-date, so already-read highlights (which survive the receipt DELETE) get re-counted, and inflation accumulates on every rotation.

_handle_new_receipts_for_notifs_txn used simple_update_txn to record last_receipt_stream_ordering, which silently no-ops if no summary row exists yet. _rotate_notifs_before_txn then INSERTed the row with last_receipt_stream_ordering=NULL. The badge query treats NULL as "trust the counts", and since highlights survive the receipt DELETE, they get re-counted on every rotation, growing the badge unboundedly.

Pre-populate event_push_summary rows with the receipt position for every thread with pending push actions, and switch the threaded path to upsert. A SQL migration backfills existing NULL rows from receipts_linearized.

_handle_new_receipts_for_notifs_txn used simple_update_txn to record
last_receipt_stream_ordering, which silently no-ops if no summary row
exists yet. _rotate_notifs_before_txn then INSERTed the row with
last_receipt_stream_ordering=NULL. The badge query treats NULL as
"trust the counts", and since highlights survive the receipt DELETE,
they get re-counted on every rotation, growing the badge unboundedly.

Pre-populate event_push_summary rows with the receipt position for every thread
with pending push actions, and switch the threaded path to upsert. A SQL migration
backfills existing NULL rows from receipts_linearized.
@stefanceriu stefanceriu force-pushed the stefan/fixBadgeInflation branch from d30a049 to edbba7f Compare May 15, 2026 11:43
@stefanceriu stefanceriu marked this pull request as ready for review May 15, 2026 12:37
@stefanceriu stefanceriu requested a review from a team as a code owner May 15, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant