Skip to content

fix(escrow): circuit-break retrieves against a wedged holder - #245

Merged
arzafran merged 3 commits into
mainfrom
fix/escrow-circuit-breaker
Aug 4, 2026
Merged

fix(escrow): circuit-break retrieves against a wedged holder#245
arzafran merged 3 commits into
mainfrom
fix/escrow-circuit-breaker

Conversation

@arzafran

@arzafran arzafran commented Aug 4, 2026

Copy link
Copy Markdown
Member

What this does

Follow-up to #244, from its review: a holder that accepts connections but never answers cost the launch restore 5s of main-thread stall per escrowed panel (they nearly all share one holder socket). Now the first receive timeout opens a 60s circuit for that socket path and every other retrieve against it skips straight to the fallback without connecting — one stall per launch, not per panel. Fast failures (missing socket, refused, eof) never open the circuit; the 60s window is the only reset, so a holder that recovers gets retried on the next real request.

Review order

  1. Commit 1: the regression test + breaker state/seam (CI red on the test — second call must be instant and the listener must see exactly one connection)
  2. Commit 2: the skip + record logic (green)

Test plan

  • CI: commit 1 red on testEscrowRetrieveCircuitBreakerSkipsSecondCallToWedgedHolder, commit 2 green
  • TerminalControllerSocketSecurityTests 11/11 locally

…er session

A holder that accepts but never answers previously stalled the serial
main-thread restore for retrieveRecvTimeout per escrowed panel. The new
test drives retrieve against a bind+accept-but-never-respond listener
with an injectable sub-second timeout and asserts the SECOND call to the
same socket path returns instantly without connecting (the listener must
see exactly one connection), then that resetting the breaker allows a
fresh attempt.

Test-only commit by design: the breaker state and reset seam land here
so the test compiles; the skip and record behavior land next, so CI
shows this failing without them.
One wedged holder (accepts, never answers) now costs one
retrieveRecvTimeout stall per launch instead of one per escrowed panel:
the first post-connect receive timeout records the socket path, and
every retrieve against that same path within 60s returns nil without
connecting. Fast-fail paths (missing socket, connection refused, eof,
protocol error) never open the circuit — they are already cheap and may
be transient. The 60s window is the only reset: long enough to cover a
full serial restore sweep, short enough that a recovered holder is
retried on the next real request.

Turns the previous commit's regression test green.
…ession test

The retrieve() circuit breaker keyed recentRetrieveTimeoutsByPath by wall-clock
Date, so a backward clock jump (NTP sync, sleep/wake) could keep a path's
circuit open past the intended 60s window. Switched to DispatchTime (monotonic
CLOCK_UPTIME_RAW) and compare elapsed nanoseconds instead; the
path_failed_ago_ms dilog value is unchanged.

testEscrowRetrieveCircuitBreakerSkipsSecondCallToWedgedHolder asserted the
open-breaker skip completes in < 0.05s (tight against scheduling jitter) and
read the accept thread's connection count without waiting for the async
accept() to actually land. Loosened the skip bound to < 0.15s (still well
under the 0.2s injected timeout) and added AcceptedConnections.waitForCount,
a bounded poll used wherever the test expects a new connection to have been
accepted, instead of reading count immediately after retrieve() returns.
@arzafran
arzafran merged commit 151e516 into main Aug 4, 2026
10 checks passed
@arzafran
arzafran deleted the fix/escrow-circuit-breaker branch August 4, 2026 17:17
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