fix(net): bound every request with a timeout
A server that stops answering without closing the socket left a running sync waiting forever. Requests now fail with TimeoutException after 30 s (10 s for the handshake, both configurable in ConnectAsync) and the connection is marked dead so the app reconnects. See ADR 0034.
This commit is contained in:
1 parent
cb551efce4
commit
3be761d97e
5 files changed
+147
-6
No files matched your search
@@ -0,0 +1,28 @@
|
||||
# 0034. Bound every network request with a timeout
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Area:** Network
|
||||
- **Recorded:** 2026-11-03
|
||||
|
||||
## Context
|
||||
|
||||
`ElectrumClient.RequestAsync` waited until the server answered or the caller cancelled. The only timeout in the stack was the 8 s ping of the ViewModel keep-alive, which does not run while a sync is in progress. A server that stops answering without closing the socket (half-open TCP, Android Doze, a stalled or hostile server) left a running sync waiting forever, with no error and no automatic recovery.
|
||||
|
||||
## Decision
|
||||
|
||||
- Every request has a timeout, `ElectrumClient.DefaultRequestTimeout` (30 s). The `server.version` handshake has its own, shorter `DefaultHandshakeTimeout` (10 s). Both are parameters of `ConnectAsync` (`requestTimeout`, `handshakeTimeout`), so tests use a few hundred milliseconds.
|
||||
- The timer starts when the request obtains its in-flight slot: waiting behind the cap (ADR 0009) does not count.
|
||||
- On expiry the request fails with `TimeoutException` (naming the method and server) and the **whole connection is marked dead**: the other pending requests fail, `IsConnected` becomes false, `Disconnected` is raised and later requests fail fast with `IOException`. After a missed answer the socket cannot be trusted, and the ViewModel reconnects through the existing path. A late reply is dropped because the request is no longer pending.
|
||||
- The keep-alive is left as it is (it skips ticks while syncing): the request timeout now bounds a stalled sync, so a second mechanism is not needed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A stalled server makes a sync fail within the timeout instead of hanging; the existing retry and reconnect logic takes over.
|
||||
- A legitimately slow reply above 30 s is treated as a dead connection. The largest realistic replies (header batches, large transactions, busy-address histories) are far below that on a working link; the value is a constructor parameter if a deployment needs more.
|
||||
- One slow request tears down the shared connection and the requests in flight with it. This is deliberate: it is cheaper than a half-open socket that keeps accepting writes.
|
||||
|
||||
## References
|
||||
|
||||
- `src/Core/Net/ElectrumClient.cs`
|
||||
- `tests/PalladiumWallet.Tests/Net/ElectrumClientTests.cs`
|
||||
- Related: 0009
|
||||
@@ -37,6 +37,7 @@ One ADR per feature or significant design decision. Format: `template.md`. Workf
|
||||
| 0031 | [Explicitly defer multisig, Lightning, Tor and hardware wallets](0031-defer-multisig-lightning-and-hardware-wallets.md) | Scope | Accepted |
|
||||
| 0032 | [Run tests, fuzz smoke and docs checks in GitHub Actions](0032-github-actions-ci.md) | Testing | Accepted |
|
||||
| 0033 | [Test the UI layer and CLI headlessly, gate coverage](0033-test-strategy-beyond-core.md) | Testing | Accepted |
|
||||
| 0034 | [Bound every network request with a timeout](0034-bound-every-network-request.md) | Network | Accepted |
|
||||
|
||||
ADRs `0002` onwards were written retroactively on 2026-11-03 from the code, comments and changelog.
|
||||
If one misstates the original reasoning, fix it in a pull request.
|
||||
Reference in new issue
Block a user