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§
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, matchingPushNotificationService). - 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.