Commit b143f5e
feat(retry): M18.1 — RetryPolicy + classifier + run loop + CAT_RETRY (FR-81, FR-82, FR-83 lib side) (#28)
Adds the library-side scaffold M18 needs to honour ConnectTimeout +
ConnectionAttempts from `~/.ssh/config` (FR-80, M18.2), classify
transient vs fatal errors (FR-82), drive an exponential-backoff
retry loop with jitter (FR-81), and capture per-attempt history for
FR-83's `gitway --test --json` envelope.
src/retry.rs (new, ~430 lines + ~190 lines of tests):
- pub struct RetryPolicy { attempts, base, factor, cap, max_window,
connect_timeout } with builder-style setters. Default values
per PRD: 3 attempts, 250 ms base, x2 factor, 8 s cap, 30 s max
window, no connect_timeout.
- pub enum Disposition { Retry, Fatal }
pub fn classify(err: &AnvilError) -> Disposition (FR-82):
* AuthenticationFailed / HostKeyMismatch / NoKeyFound /
KeyEncrypted -> Fatal
* Io kind in {ConnectionRefused, TimedOut, HostUnreachable,
NetworkUnreachable, NotFound (DNS NXDOMAIN), AddrNotAvailable}
-> Retry
* Everything else (russh protocol, signing, signature-invalid,
other Io kinds) -> Fatal
HTTP 429/503 detection from FR-82's defensive wording is out of
scope: Anvil speaks raw SSH; HTTP statuses only surface in
ProxyCommand subprocess output that Anvil doesn't parse.
- pub struct RetryAttempt { attempt: u32, reason: String,
elapsed: Duration } captures per-failure history for FR-83.
- pub async fn run<F, Fut, T>(policy, op) -> Result<(T, Vec<RetryAttempt>), AnvilError>
drives the loop with jittered exponential backoff:
delay_n = min(base * factor^(n-1), cap) + uniform_jitter([0, base/2])
Jitter sourced from rand_core::OsRng (already in deps from M19's
prepend_revoked).
Bails on max_window before starting another attempt.
Emits tracing::warn! at CAT_RETRY per failed attempt with
attempt / reason / elapsed_ms / disposition fields.
- run is timeout-agnostic: the per-attempt tokio::time::timeout
wrap lives at the call site (M18.2's session.rs::connect) so
the same loop driver can be reused for non-network operations.
- 15 unit tests covering: default-policy values, builder
chainability, classifier matrix (auth-fatal / host-key-fatal /
no-key-fatal / io-connection-refused-retry / io-timed-out-retry /
io-not-found-retry / io-permission-denied-fatal), run loop
(success-first-try / bail-on-fatal / retry-and-record-history /
exhaust-attempt-count), backoff curve (exponential growth, cap
enforcement, 1000-draw jitter window).
src/log.rs:
- New pub const CAT_RETRY = "anvil_ssh::retry"; appended to
CATEGORIES.
- categories_slice_matches_individual_constants test updated to
include the new constant.
src/error.rs:
- New pub fn AnvilError::io_kind(&self) -> Option<std::io::ErrorKind>
returns the underlying io::Error::kind() for the Io variant.
Used by retry::classify; also useful for downstream consumers
inspecting failure categories.
- New pub fn AnvilError::is_transient(&self) -> bool returning
matches!(retry::classify(self), Disposition::Retry). Surfaces
the classifier as a single-call predicate for log-aggregation
pipelines and CLI error paths.
src/lib.rs:
- pub mod retry;
Public API: pure additive. Version bump to 0.9.0 lands in M18.3.
Plan: M18.1 of anvil-gitway-milestone-plan.md.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 6f20788 commit b143f5e
4 files changed
Lines changed: 627 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
194 | 224 | | |
195 | 225 | | |
196 | 226 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
72 | 78 | | |
73 | 79 | | |
74 | 80 | | |
75 | 81 | | |
76 | 82 | | |
77 | 83 | | |
78 | | - | |
| 84 | + | |
79 | 85 | | |
80 | 86 | | |
81 | 87 | | |
| |||
125 | 131 | | |
126 | 132 | | |
127 | 133 | | |
128 | | - | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
129 | 138 | | |
130 | 139 | | |
131 | 140 | | |
| |||
0 commit comments