Every morning at 7 AM in your household time zone, Sabine bundles up yesterday's calendar events—changes, additions, whatever you might have missed—and sends you a single digest notification. It's a tiny quality-of-life feature that keeps you from drowning in individual pings.
Except for the past few weeks, if that notification failed to deliver—if the in-app notification row was dropped for any reason—the digest would still be marked "flushed" in the database. The queue would be cleared. The next morning, Sabine would wake up with no memory that you never got yesterday's digest. The notification was just… gone.
The False Positive
The bug was in flush_daily_digest. The function calls send_notification to deliver the digest, then marks the queue rows as flushed. The problem: send_notification never raises exceptions—it returns False when an in-app notification row is dropped. But flush_daily_digest wasn't checking the return value. It had an exception handler that would have prevented the mark_flushed call, but that branch never fired. A False return just sailed right through.
So the function would call send_notification, get back False ("I dropped this"), and then immediately call mark_digest_flushed as if everything had succeeded. The queue would be emptied. The digest would be lost.
The Fix: Bind the Boolean
The fix is simple: bind the return value, check for an explicit False, and skip mark_digest_flushed when delivery failed. If send_notification returns False, log an ERROR, return an empty list, and leave the queue rows pending. The next 7 AM run will retry. Duplicate delivery is annoying; permanent loss is unacceptable.
The delivery rule now mirrors the internal _send_notification behavior: only an explicit False is a drop. None (older or mocked implementations) and True both count as delivered. The ImportError and exception branches also return empty lists now—no delivery, no flush.
Tests First
Before touching the production code, we wrote five tests in test_digest_flush_delivery.py covering all the return-value cases: None, True, False, ImportError, and exception. Three of the five failed on the old code. The None and True cases passed—those were already working. The False, ImportError, and exception cases all failed because the old code was marking rows flushed regardless. After the fix, all five pass.
What's Next
This fix closes the immediate data-loss hole, but it surfaces a deeper question: why is send_notification returning False in the first place? If in-app notification rows are being dropped at a non-trivial rate, we need to understand why. Database contention? Resource limits? A failure mode we haven't anticipated?
We're also looking at adding observability here—tracking how often the retry path fires, how many digests are successfully delivered on the first attempt versus the second or third. Right now we have an ERROR log, but no metrics. If this starts happening frequently, we need to know.
For now, the calendar digest is back to being reliable. If you don't get your 7 AM summary, it's not because we threw it away—it's because we haven't sent it yet.