Skip to main content

Module notification_service

Module notification_service 

Source
Expand description

Persisted per-user notification feed.

Postgres is the source of truth. Redis (Upstash, globally distributed) holds an edge-cached has_unread:{wallet} bit so SSR / SSR-like reads don’t have to cross the Pacific to Tokyo-pg for the bell dot.

Web Push delivery (handled by PushNotificationService) is a separate display channel on top of this log: if the user is subscribed they get an OS notification too, but the feed stays complete either way.

Graceful degradation: the service stays fully functional when Redis is unavailable – pg writes/reads proceed normally; the bell cache just doesn’t update and SSR readers fall back to no-cached-bit behavior. Publishing a fill should never fail because of Redis.

Backpressure: callers on the fill hot path use [enqueue_publish], which try_sends onto a bounded mpsc. A single background drainer awaits inserts with bounded concurrency. Burst loads that can’t keep up drop the tail (counted via a warn! log) rather than spawning an unbounded pile of detached tokio::spawn tasks.

Has-unread bit race: mark_*_read clears the Redis bit after checking pg is empty, and then re-checks pg one more time to catch a concurrent publish that raced in during the clear. Not strictly atomic (would need a Lua script with an out-of-band pg check), but closes the common race window where a publisher lands between our pg check and our DEL.

No in-process dedupe cache on the has_unread bit. An earlier revision kept a DashSet<wallet> to skip re-SETs. That cache isn’t invalidated across instances after a mark-read (instance B clears Redis, instance A keeps suppressing), so correctness beats the optimization: SET k 1 is cheap and idempotent.

Payloads are stored as msgpack bytes: we never query into them, and the encoder is cheaper + more compact than JSON for the volumes we expect.

Structs§

NotificationService
PublishJob 🔒

Constants§

DEFAULT_LIST_LIMIT 🔒
HAS_UNREAD_KEY_PREFIX 🔒
MAX_LIST_LIMIT 🔒
PUBLISH_QUEUE_CAPACITY 🔒
Bounded channel capacity for fire-and-forget publishes. If the drainer can’t keep up, new publishes are dropped with a warn! (best-effort delivery, matching PushNotificationService).
REDIS_COMMAND_TIMEOUT 🔒
REDIS_FAILURE_BACKOFF 🔒
After a Redis connect failure we short-circuit subsequent calls for this long so a transient outage doesn’t burn 2s per publish waiting on the timeout. One block per wallet-side call is cheap; cascading retries back the drainer into a queue-full state.
SHUTDOWN_DRAIN_TIMEOUT 🔒
Max time we give the drainer to flush pending publishes once the shutdown signal fires. After this the drainer exits even if the queue still has items.

Functions§

drain_one 🔒
drain_publish_queue 🔒
has_unread_key 🔒
now_ms 🔒