fix: [SDK-4995] harden background notification ingress against ANRs - #2712
fix: [SDK-4995] harden background notification ingress against ANRs#2712fadi-george wants to merge 4 commits into
Conversation
📊 Diff Coverage ReportDiff Coverage Report (Changed Lines Only)Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff). Changed Files Coverage
Overall (aggregate gate)537/619 touched executable lines covered (86.8% — requires ≥ 80%) Per-file detail (informational; gate is aggregate above):
|
There was a problem hiding this comment.
Multi-model adversarial review (interrogate)
Verdict: request changes. The durable-ingress direction matches the ANR/lost-work intent, but several paths can still strand or drop work, and SyncJobService stop ownership looks broken in production.
Intent
Harden cold-start notification receivers and background sync against ANRs and lost work via gated dispatcher init, a durable FCM/dismiss journal + WorkManager drain, exact-once broadcast completion, and race-safe
SyncJobServiceownership — without changing customer-facing notification APIs.
Reviewers
- A: claude-fable-5-thinking-xhigh — 14 findings (2 critical)
- B: gpt-5.6-sol-xhigh — 8 findings (5 critical)
- C: cursor-grok-4.5-high-fast — 11 findings (3 critical)
- D: claude-opus-5-thinking-xhigh — 22 findings (6 critical)
Act On
ExistingWorkPolicy.KEEP+ one-shotstore.list()strands mid-drain inserts (A/B/C/D) — drain can finish while newer journal rows have no successor work.SyncJobService.onStopJobusesJobParametersidentity (===) (B/D) — Binder-delivered params are distinct instances; stop likely never cancels/reschedules. Tests reuse one mock and miss this.- No poison-record / attempt isolation in drain (A/D) — one bad row or
IngressKind.valueOffailure head-of-line blocks the queue with unboundedResult.retry(). - FCM/dismiss handoffs have no
timeoutMs(A/B/C/D) — ANR-sensitive receivers rely only oninvokeOnCompletion; Boot/Upgrade uniquely get the 8s deadline. - Broadcast completes before drain enqueue is durable (B/C) —
scheduleDrainBestEffortfire-and-forgets on IO; process death afterfinish()can leave journal rows unscheduled until a later startup/ingress.
Consider
- Delete-after-process without durable downstream ack → possible double-display on crash (A/C).
- Journal
putfailure is swallowed → silent drop with no inline fallback (A/D). - Bootstrap thread has no
finallyaroundinitialize(); fallback throw can wedgerunning=true(A/B/D). goAsync()makessetAbort()/setSuccessfulResultCode()dead; abort criterion also changed to “persisted” (A/C/D).- Missing drain/KEEP/poison/JobParameters-distinct tests for the headline guarantees (A/C/D).
Noted
Extra WM hop latency; SerialIO/Ingress now share a 200-cap queue; restore not actually journaled; BundleCodec lossiness; companion restored static; production @ForTest seams.
Dismissed
- “Rewrite dispatchers simpler” as a ship-blocker — complexity is high, but the gated warm path is the stated ANR fix; treat as follow-up hardening, not a revert.
- HMS/ADM not on the journal path — out of this PR’s stated FCM/dismiss scope.
- Dismiss
hashCodeid collisions — low practical risk given idempotent dismiss processing.
Agreement map
Strongest consensus: KEEP/drain backlog race, missing FCM/dismiss deadlines, and weak verification of drain semantics. Independent high-severity hits from B+D on JobParameters ===. Divergence mostly on how hard to push structural simplification vs. surgical durability fixes.
Inline comments cover the Act On items.
Sent by Cursor Automation: Untitled
Ensure queued notification work cannot be stranded or blocked by poison records, and make job and dispatcher ownership resilient across Android callback and bootstrap races. Co-authored-by: Cursor <cursoragent@cursor.com>


Description
One Line Summary
Harden cold-start notification receivers and background sync jobs against ANRs, stranded work, and queue-blocking failures.
Details
Motivation
Chronic background ANRs continued around
FCMBroadcastReceiverdespite best-effort dispatcher prewarming. Cold dispatcher initialization, unbounded receiver work, incompletePendingResult.finish()paths, andSyncJobServicelifecycle races could still block Android component deadlines or strand notification work.Scope
APPEND_OR_REPLACEso records arriving during an active drain always receive successor work.SyncJobServicecallbacks by stable job ID and make completion and cancellation ownership race-safe.OneSignalDispatchers.launchOnIngressas the shared ingress dispatch entry point. There are no customer-facing API changes.Testing
Unit testing
Added coverage for:
SyncJobServicecallbacks using distinctJobParametersinstances with matching or different job IDs.Validated with:
./gradlew spotlessCheck detekt --console=plain./coverage/checkCoverage.shChanged-line coverage is 86.9% against an 80% requirement.
Manual testing
Installed the local GMS demo on
emulator-5556, queued offline background work, and forced JobScheduler job2071862118. The sync ran onOneSignal-IO-1, completed throughjobFinished, and produced no ANR or crash.Affected code checklist
Checklist
Overview
Testing
Final pass