Gate notification checks on a Redis next-due cache to let Neon suspend - #468
Merged
Conversation
_run_notification_check and run_polling's inline anchor/followup block were hitting Postgres unconditionally on every invocation, regardless of whether anything was actually due, defeating managed-Postgres (Neon) auto-suspend even when the app is fully idle. Add shared/notify_due.py: a Redis ZSET/HASH cache of each user's next anchor-boundary and next-followup-ping time. _run_notification_check and run_polling now check this cache first and skip Postgres entirely when nothing is due; check_followups and _check_anchor_transitions return their own next-due estimate (computed from data they already fetched) so the cache self-perpetuates with no separate background job. Anchor create/update/delete refresh the cache immediately. Everything fails open on any Redis error, so gating can only save a round-trip, never skip a real notification. Meeting events are push-based (in-process WS-listener queue owned by tether-premium), not time-based, so drain_meeting_events is deliberately exempt from this gate in run_polling and always called — it already self-gates for free on an empty queue.
4 tasks
jlunder00
added a commit
that referenced
this pull request
Jul 4, 2026
Add REDIS_URL to [program:bot] to un-brick Neon idle gating (#468)
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.
Summary
_run_notification_check(api/routes/internal.py) andrun_polling's inline anchor/followup block previously hit Postgres unconditionally on every invocation, regardless of whether anything was actually due — defeating managed-Postgres (Neon) auto-suspend even when the app is fully idle.shared/notify_due.py: a Redis ZSET/HASH cache of each user's next anchor-boundary and next-followup-ping time. Both entry points now check this cache first and skip Postgres entirely when nothing is due.check_followupsand_check_anchor_transitionsnow return their own next-due estimate (computed from data they already fetched — zero extra Postgres cost), so the cache self-perpetuates with no separate background refresh job.api/routes/anchors.py) refresh the cache immediately so edits take effect without waiting for the safety TTL.REDIS_URL, connection errors, malformed anchor data) — gating can only ever save a round-trip, never cause a real notification to be silently skipped.drain_meeting_eventsis deliberately exempt from this gate inrun_pollingand always called — it already self-gates for free on an empty queue. Companion premium-side PR: tether-premium#74.Design doc / context
Full design discussion in team coordination — TL;DR: gate ONCE per user (not per sub-check), since anchor-transition can create followup rows that
check_followupsneeds to see in the same pass; a per-function gate risks skipping just-created rows via a stale cache entry.Test plan
shared/notify_due.py(combined min-of-components scoring, fail-open on unconfigured Redis, fail-open on actual Redis exceptions,next_anchor_boundarypure function incl. malformed-row hardening)classify_followup_row(pure followup due-classification extracted fromcheck_followups)_run_notification_check/_check_anchor_transitionsgating (mocked pool/Redis — asserts Postgres is never touched when nothing due, fail-open fallback, per-user recompute-after-run)api/routes/anchors._refresh_anchor_due_cache(incl. critical fail-open case: a Postgres error refreshing the cache must never turn an already-committed anchor mutation into a 500)tests/api/,tests/bot/,tests/shared/suite run locally: 396 passed, 11 pre-existing failures unrelated to this change (missingclaude_agent_sdkdev dependency + one pre-existing async-cancellation flake intest_redis_pubsub.py, a file untouched by this PR)get_anchorscall that could turn a successful anchor mutation into a 500, hardenednext_anchor_boundaryagainst malformed anchor rows, bumped log severity on two fail-open paths, added Redis-exception (not just unconfigured) fail-open test coverageKnown follow-ups (out of scope for this PR)
agent_pool_manager) would make chronic Redis unavailability more visible than log-grepping._check_anchor_transitions(api/routes/internal.py) andrun_polling's inline anchor-transition block was intentionally left alone to keep this PR minimal — a separate cleanup.