Skip to content

Tracking: transactional rate-limit admission — charge/refund lifecycle + capacity path #218

Description

@erskingardner

Root cause

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.

Refactor design

  1. AdmissionGuard (RAII): check_and_increment returns 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 explicit commit(). Builds on the reservation-aware rollback semantics from PR fix: make rate-limit rollback reservation-aware #206.
  2. Filter before charging: the dispatcher exposes accepts(platform) / token-encoding checks so unconfigured-platform and non-UTF8 tokens are rejected before any limiter charge, with a real record_push_failed(platform, "unconfigured"/"invalid_encoding")-style drop metric ([LOW] A decrypted token dropped inside dispatch() (unconfigured platform / non-UTF8 FCM) keeps its rate-limit budget and is recorded by no metric #177).
  3. Capacity path off the global lock: shard the limiter (N × 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).
  4. Truthful gauges: the limiter publishes its real 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.
  5. Single-critical-section dedup: fold try_reserve/mark_seen into one lock acquisition or reuse the reservation entry at completion ([LOW] Successful event path acquires the dedup write lock twice (try_reserve then mark_seen), doubling lock contention and gauge updates on the hot path #197).

Members

In-flight PRs (merge first — this tracker builds on their semantics and tests)

Sequencing

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions