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
Retry policy decisions are scattered across retry.rs constants, header parsing, and permit mechanics, and two of them break the system's own stated invariants under provider stress:
Wave 2 — hard-blocked behind the push-provider tracker (both rewrite with_retry/BackoffPermit in retry.rs). The optional dispatcher InFlightTracker → TaskTracker unification (from the lifecycle tracker) also waits for this.
Root cause
Retry policy decisions are scattered across
retry.rsconstants, header parsing, and permit mechanics, and two of them break the system's own stated invariants under provider stress:Retry-Afteris honored verbatim with no ceiling (retry_wait_duration,src/push/retry.rs:219-224) — an untrusted header can pin a send task (holding a decrypted device token, and anInFlightGuardthat blocks drain) for an arbitrary duration ([LOW] Provider-supplied Retry-After is honored verbatim with no upper bound — an untrusted header can pin a send task (and its decrypted token) for an arbitrary duration #162).BackoffPermit::release()frees the concurrency slot during backoff sleeps while the task stays alive, so under a provider 429/5xx storm the "max 100 concurrent sends" bound silently becomes "up to the full 10k queue of live tasks, each holding a decrypted token, thundering-herd re-competing on reacquire" ([MEDIUM] Backoff permit release lets live send-task (and decrypted-token) count exceed the 100 concurrency bound up to the full 10k queue #160).Refactor design
On top of the push-provider tracker's rewritten retry engine (hard dependency — same
retry.rsmachinery):Retry-Afterat a small policy ceiling (e.g. 60–120s):server_delay.min(MAX_RETRY_AFTER)inretry_wait_duration/parse_retry_after([LOW] Provider-supplied Retry-After is honored verbatim with no upper bound — an untrusted header can pin a send task (and its decrypted token) for an arbitrary duration #162).RetryPolicystruct owning floor, cap, jitter, budget), so the lower bound ([LOW] Retry backoff has no minimum floor and no jitter:Retry-After: 0yields immediate back-to-back retries and concurrent sends retry in synchronized waves #96), upper bound ([LOW] Provider-supplied Retry-After is honored verbatim with no upper bound — an untrusted header can pin a send task (and its decrypted token) for an arbitrary duration #162), and concurrency interaction ([MEDIUM] Backoff permit release lets live send-task (and decrypted-token) count exceed the 100 concurrency bound up to the full 10k queue #160) live in one reviewed place.Members
Retry-AfterIn-flight PRs
Retry-After: 0yields immediate back-to-back retries and concurrent sends retry in synchronized waves #96, backoff floor + jitter): merge first; this tracker absorbs its policy into the unifiedRetryPolicy.Retry-After: 0yields immediate back-to-back retries and concurrent sends retry in synchronized waves #96 — backoff floor + jitter (point fix in flight)Sequencing
Wave 2 — hard-blocked behind the push-provider tracker (both rewrite
with_retry/BackoffPermitinretry.rs). The optional dispatcherInFlightTracker→TaskTrackerunification (from the lifecycle tracker) also waits for this.