If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.
A relay that fails is retried on an exponential backoff, then abandoned
for the lifetime of the process. Nothing brings it back: the relay layer
registers no connectivity callback, the periodic subscription validator
only repairs subscriptions on sockets that are already open (and returns
immediately when connectedRelayCount is 0, which is exactly the state
after an outage), and connect() runs once from NostrClient.initialize().
The remaining paths that reset reconnectAttempts are a manual retry, a
Tor state change, and a successful open.
Two ways a relay died permanently:
- Any error whose message mentioned DNS returned before scheduling
anything at all. "Unable to resolve host" is what this device reports
when it simply has no network, so a moment in a tunnel killed every
relay at once, with no retry ever.
- Otherwise the schedule stopped at MAX_RECONNECT_ATTEMPTS. With
INITIAL=1s and MULTIPLIER=2 that is nine waits totalling about eight
and a half minutes, after which the relay was dead. MAX_BACKOFF_INTERVAL
was unreachable: attempt 9 asks for 256s and attempt 10 gave up, so the
five-minute ceiling the constant defines never applied to anything.
Let the backoff saturate at MAX_BACKOFF_INTERVAL and keep retrying there.
A name-resolution failure now backs off like any other error. Steady state
costs one connection attempt per relay per five minutes; the previous
behaviour cost the user every internet DM, delivery receipt and geohash
channel until they noticed and restarted the app.
The schedule moves into RelayReconnectPolicy so it is unit-testable
without OkHttp or a Context.
Two problems Codex found in the first push.
"SOCKS connection error" is not a circuit failure. arti-build logs that
wrapper for every error out of handle_socks_connection, including local
protocol faults raised before a circuit is attempted — "Invalid SOCKS
handshake", "Unsupported SOCKS version", "Unsupported SOCKS command".
Anything on the device that speaks SOCKS badly to the local port would
have read as a dead Tor. Drop it. "Failed to connect through Tor" is
logged only on client.connect() failure, and "Failed to obtain exit
circuit" is arti's own detail for that error, which is what #610 shows —
so the reported lines are still matched.
The restart path cancels itself. restartArti stops Arti first, and
stopArtiInternal cancels retryJob and inactivityJob. Both existing
recovery paths call restartArti from inside one of those jobs, so the
coroutine is cancelled between the stop and the start and Arti never
comes back. That is already true on main for the start-failure and
bootstrap-inactivity paths; routing a post-bootstrap outage into it would
have turned a wrong indicator into a stopped Tor, which is worse.
Run restarts from a separate restartJob that the stop path leaves alone,
and cancel it explicitly when the user turns Tor off so a pending
recovery cannot resurrect it.
Uses the log lines from the report verbatim. Pins both directions: a
same-instant burst and occasional failures far apart leave the indicator
alone, while four failures still going after twenty seconds downgrade it.
Also covers reset and a backwards clock jump.
Arti's bootstrap milestones only fire on the way up. "We have found that
guard [scrubbed] is usable." latches the session to RUNNING at 100% and
calls stopInactivityMonitoring(), and from there nothing downgrades the
status except a deliberate stop or a state-file lock conflict.
So when the network goes away after bootstrap, Arti logs a continuous
stream of "Failed to connect through Tor" / "SOCKS connection error"
while the header keeps showing the connected colour, which is gated on
running && bootstrapPercent >= 100. isProxyEnabled() agrees, so requests
keep being routed into a SOCKS port that cannot reach an exit.
Track circuit failures after bootstrap and drop the published status
below 100% once they both accumulate and persist. Individual failures are
a normal part of Tor, so a burst inside one instant — which is what the
report shows, three threads failing in the same millisecond — is ignored;
it takes four failures still going twenty seconds later.
Downgrading to BOOTSTRAPPING rather than ERROR keeps running = true, so
callers fail closed through awaitSelectedRoute instead of leaking to
clearnet, the inactivity watchdog re-arms now that bootstrapPercent is
below 100, and the existing backoff retry gets to re-establish the
session. A successful re-bootstrap clears the tally and restores RUNNING.
The policy is kept free of Android and coroutine dependencies so the
thresholds are directly testable.
Refs #610