Skip to content

Trip the DoH circuit breaker per attempt and drop the dead delayed-screen-off alarm - #768

Merged
kasnder merged 2 commits into
masterfrom
fix/battery-followups
Aug 22, 2026
Merged

Trip the DoH circuit breaker per attempt and drop the dead delayed-screen-off alarm#768
kasnder merged 2 commits into
masterfrom
fix/battery-followups

Conversation

@kasnder

@kasnder kasnder commented Aug 22, 2026

Copy link
Copy Markdown
Member

Two battery follow-ups from the battery audit (companions to #747/#748/#749). Rebased onto current master (f520eba), which already contains #747 (DoH retries/keep-alives gated on screen state), #752 (ServiceSinkhole teardown) and #753 (DNS proxy hardening).

1. DoH circuit breaker counts attempts, not queries

Updated rationale post-#747: #747 already sets maxRetries = 0 while the screen is off, so a failing query in doze now costs one ~5s attempt, not three — this PR's per-attempt counting degenerates to the old per-query behaviour there, and #747 has already captured that battery win. The remaining value of this PR is screen-on: a dead endpoint hit by #753's larger DoH thread pool now trips the breaker in ~3–4 bad queries (3 wasted attempts each) instead of needing 10 full queries, so screen-on doesn't keep hammering (and logging around) a known-dead endpoint as long.

  • DnsOverHttpsClient.resolve(query) gains an optional onFailedAttempt callback fired once per retryable failure (network error, server 5xx, unusable body) — exactly the attempts that waste their full budget. Non-retryable client errors are not reported there; the caller still counts those once via its null handling.
  • Both proxy paths (UDP and TCP) now aggregate per-query attempts into dohFailures with addAndGet(max(1, attempts)), so the breaker trips after ~3–4 bad queries instead of ten, on screen-on.
  • Threshold value unchanged; semantics documented at the constant.
  • Verified against Harden DNS proxy pool, TCP framing, and fallback validation #753: its new isPlausibleDnsQuery gate returns before DoH is ever called, so garbage/malformed queries still never reach the counter.

#760 (per-attempt counting worsens the endpoint-switch false-trip) — assessed and fixed. #760 already flags that DnsOverHttpsClient.getInstance(context, endpoint) cancels in-flight calls on the outgoing client via shutdown()'s dispatcher().cancelAll(), and DnsProxyServer counts every null resolve() as a failure. With straight per-attempt counting that's worse than before: a query whose in-flight call gets canceled and then keeps retrying against the same doomed client can rack up all MAX_RETRIES+1 attempts as "failures," so as few as ~4 in-flight queries at switch time (4×3=12) could trip the threshold-of-10 breaker against the new, healthy endpoint, versus 10 queries needed pre-#768. Fixed in this rebase: resolve() now checks Call#isCanceled() in the IOException handler and returns without calling onFailedAttempt when the failure was our own cancellation rather than a real network error. This directly implements the "don't count cancellations caused by our own shutdown()" direction from #760's own suggested fix (the other two #760 issues — reset-on-any-success, and the shared UDP/TCP counter — are pre-existing and out of scope here).

2. Remove the half-dead ACTION_SCREEN_OFF_DELAYED machinery

The action had a receiver registration and a cancel, but nothing ever scheduled it — except an error fallback in the screen-state receiver that armed a setAndAllowWhileIdle RTC_WAKEUP every 15 seconds if handling a screen event threw, looping indefinitely in doze while the underlying failure persisted. Removed: the action constant, its registration line, and the fallback loop (the catch now just logs). Unaffected by #752's ServiceSinkhole teardown changes (different code path).

Testing

…reen-off alarm

Two battery follow-ups:

The DoH circuit breaker counted failed queries, but each failing query
burns up to three full timeout budgets inside resolve(). A dead endpoint
could keep ~30s of hung executor work alive per query for ten straight
queries before the 60s cooldown tripped. Count every wasted network
attempt instead (via a new optional resolve() listener), so the breaker
trips after a few bad queries. Non-retryable client errors are still
counted once, by the caller.

ACTION_SCREEN_OFF_DELAYED was registered and cancelled but never
scheduled — except by an error fallback that armed a doze-allowed
RTC_WAKEUP every 15 seconds if handling a screen event threw, looping
indefinitely while the underlying failure persisted. Remove the action,
its receiver registration, and that fallback loop.
Rebasing onto master surfaced issue #760: getInstance()'s endpoint swap
cancels in-flight calls on the outgoing client via shutdown()'s
dispatcher().cancelAll(), and with per-attempt counting each canceled
attempt now adds to the circuit-breaker total. That turns a routine
endpoint switch into several instant "failures" against the brand-new,
healthy endpoint. Skip reportFailedAttempt when the IOException came
from our own cancellation (Call#isCanceled()) rather than a real
network failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant