Fix email/notification toggle not persisting on unsubscribe - #6
Open
rtb-12 wants to merge 2 commits into
Open
Conversation
Subscription.save()/delete() returned all(res) over the SADD/SREM return codes. Redis returns 0 for the idempotent case (member already present on SADD, already absent on SREM), which is not a failure, but all() treated any 0 as failure. As a result UserSubscription.unsubscribe() saw a falsy result from delete() and returned early, never deleting the DB row. Redis and the DB then drifted: a person who toggled emails off was removed from the Redis send-set but kept their DB subscription row, and any resync that rebuilds Redis from the DB re-added them, so they kept receiving mail after opting out (mirror bug on subscribe()). pipe.execute() raises on a real broker/transaction error, so reaching the return means every command ran. Return True unconditionally and let exceptions signal genuine failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There is no mechanism that rebuilds the Redis subscription sets from the UserSubscription table, so any drift between the two stores is permanent. This command flushes the 'categories:subscription:<action>:*' keys and replays save() over the DB rows, making Redis a pure function of the DB. Run it once after deploying the unsubscribe fix to clear the drift that already accumulated. Supports --action to scope to one action and --dry-run to preview.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Users who toggled email (or notification) subscriptions off kept receiving them.
Subscription.save()andSubscription.delete()returnedall(res)over the RedisSADD/SREMreturn codes. Redis returns0for the idempotent case - member already present onSADD, already absent onSREM- which is not a failure, butall()treats any0as failure.Downstream,
UserSubscription.subscribe()/unsubscribe()do:So whenever the person wasn't in (or already out of) every leaf set - common with partial or collapsed subscriptions, or when subscribing to a category whose leaves the person already partly holds -
save()/delete()returnedFalseand the DB row was left un-created / un-deleted. Redis and the DB then drifted. Because a category shows as subscribed in the UI when the person has a row on it or any ancestor, this could also leave a person receiving mail with no DB row to toggle off.Fix
pipe.execute()already raises on a real broker/transaction error, so reaching thereturnmeans every command ran. ReturnTrueunconditionally and let exceptions signal genuine failures. (redisdb/subscription.py, bothsave()anddelete().)Also included: drift reconciliation
There is no mechanism that rebuilds the Redis sets from the
UserSubscriptiontable, so drift that already accumulated is permanent. New management commandreconcilesubscriptionsflushes thecategories:subscription:<action>:*keys and replayssave()over the DB rows, making Redis a pure function of the DB. Run it once after deploy:Verification
Run against the assembled app (needs Redis + DB + swapper, so not a unit test in this isolated service repo):
Asserts subscribe -> in both stores, unsubscribe -> empty in both, and the idempotent repeat stays empty. Note: I have not yet run this - containers were down. Please run before merge.
Out of scope (separate work)
send_only_to_subscribed_usersdefaults toFalseinomniport-service-emails/omniport-service-notifications(different repos). The non-targeted send path already always respects subscriptions; the flag only affects explicitly-targeted sends where reaching exactly those recipients may be intended. Flipping the default is a behavior decision for those services, not a silent change here.utils/get_subscription.py. On review it is not a defect: the frontend posts the full tree state, so the parent slug is present and is correctly unsubscribed when a child is unchecked.🤖 Generated with Claude Code