You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rate-limit charging is non-transactional: budget is spent at check_and_increment time, but admission is only decided later — and the refund logic only covers some failure paths. The limiter's capacity handling also serializes the hot path.
Starts after the config tracker's typed NonZeroUsize configs land (soft dependency). Overlaps the push-provider tracker only in dispatcher.rs, in different functions (dispatch() filtering loop vs send_push) — safe to run in parallel with minor merge friction.
Root cause
Rate-limit charging is non-transactional: budget is spent at
check_and_incrementtime, but admission is only decided later — and the refund logic only covers some failure paths. The limiter's capacity handling also serializes the hot path.process_inner(src/nostr/events.rs), the encrypted-token limiter is charged before the device-token limiter is checked; a device-reject justcontinues, stranding the encrypted charge with no refund and no rollback record — a later legitimate redelivery reads as a replay ([LOW] Encrypted-token rate-limit increment is never refunded when the paired device token is rate-limited (asymmetric with the dispatch-failure rollback) #170).src/push/dispatcher.rs:397-425) keep their spent budget and are counted by no metric; the drop is invisible belowtrace!([LOW] A decrypted token dropped insidedispatch()(unconfigured platform / non-UTF8 FCM) keeps its rate-limit budget and is recorded by no metric #177).check_and_incrementruns a 1000-entry eviction scan (with per-entry prunes) while holding the limiter's single global write lock — a cardinality flood converts into a server-wide stall of the very machinery meant to shed it ([MEDIUM] RateLimiter::check_and_increment runs an O(1000) eviction scan while holding the global write lock, serializing all rate-limit checks under cache-cardinality pressure #123).transponder_rate_limit_cache_sizeis only written from the 60s cleanup tick, so saturation onset is invisible for up to a minute ([LOW] transponder_rate_limit_cache_size gauge is only updated during the 60s cleanup, never on insertion — cache saturation / capacity-limit onset is invisible for up to a minute #125).try_reservethenmark_seen) ([LOW] Successful event path acquires the dedup write lock twice (try_reservethenmark_seen), doubling lock contention and gauge updates on the hot path #197).Refactor design
AdmissionGuard(RAII):check_and_incrementreturns a guard;commit()on successful hand-off to the dispatcher, drop-without-commit refunds the charge. Device-reject then automatically refunds the encrypted charge ([LOW] Encrypted-token rate-limit increment is never refunded when the paired device token is rate-limited (asymmetric with the dispatch-failure rollback) #170). The decrypt-failure path keeps its charge intentionally (documented replay defense) via explicitcommit(). Builds on the reservation-aware rollback semantics from PR fix: make rate-limit rollback reservation-aware #206.accepts(platform)/ token-encoding checks so unconfigured-platform and non-UTF8 tokens are rejected before any limiter charge, with a realrecord_push_failed(platform, "unconfigured"/"invalid_encoding")-style drop metric ([LOW] A decrypted token dropped insidedispatch()(unconfigured platform / non-UTF8 FCM) keeps its rate-limit budget and is recorded by no metric #177).RwLock<LruCache>by key hash) and/or replace the 1000-entry scan with small-K candidate sampling so a full cache costs bounded work per check ([MEDIUM] RateLimiter::check_and_increment runs an O(1000) eviction scan while holding the global write lock, serializing all rate-limit checks under cache-cardinality pressure #123).len()on insertion/eviction (throttled), not just at cleanup ([LOW] transponder_rate_limit_cache_size gauge is only updated during the 60s cleanup, never on insertion — cache saturation / capacity-limit onset is invisible for up to a minute #125) — extends the admission-eviction counters from PR fix: surface rate-limit admission evictions #210.try_reserve/mark_seeninto one lock acquisition or reuse the reservation entry at completion ([LOW] Successful event path acquires the dedup write lock twice (try_reservethenmark_seen), doubling lock contention and gauge updates on the hot path #197).Members
dispatch()(unconfigured platform / non-UTF8 FCM) keeps its rate-limit budget and is recorded by no metric #177 — pre-charge filtering + drop metrics for dispatch-level dropstry_reservethenmark_seen), doubling lock contention and gauge updates on the hot path #197 — single write-lock acquisition on the dedup success pathIn-flight PRs (merge first — this tracker builds on their semantics and tests)
AdmissionGuardbut its tests carry forward.Processed, inconsistent with the transient-shed semantics of the global limiter #195) — all-tokens-shed events stay retryable; the guard design must preserve this.Processed, inconsistent with the transient-shed semantics of the global limiter #195 — shed ≠ seen/Processed (point fix in flight)Sequencing
Starts after the config tracker's typed
NonZeroUsizeconfigs land (soft dependency). Overlaps the push-provider tracker only indispatcher.rs, in different functions (dispatch()filtering loop vssend_push) — safe to run in parallel with minor merge friction.