alerting: SDK typed Event::Status + health_watch example - #485
Conversation
1a53593 to
8c23001
Compare
4a8d2b0 to
d010d05
Compare
8c23001 to
e1238a4
Compare
d010d05 to
c0ac311
Compare
e1238a4 to
d8344e7
Compare
c0ac311 to
18ca230
Compare
d8344e7 to
1c0ab06
Compare
18ca230 to
cf3b351
Compare
1c0ab06 to
29d1817
Compare
cf3b351 to
32055f9
Compare
29d1817 to
4867fcd
Compare
32055f9 to
1794109
Compare
4867fcd to
6d430ff
Compare
1794109 to
511378c
Compare
6d430ff to
12e7b8d
Compare
511378c to
8716132
Compare
12e7b8d to
1a52afa
Compare
829b586 to
880a619
Compare
1a52afa to
62e9d27
Compare
880a619 to
95cb804
Compare
90e36f9 to
ef73e3c
Compare
95cb804 to
0697556
Compare
ef73e3c to
d729fac
Compare
0697556 to
3a840e1
Compare
d729fac to
1f18a9b
Compare
3a840e1 to
39b9f66
Compare
Deep review — findings (#485 + #486, plus #487/#490)Independent review; reviewer-reported, not personally re-verified. #485 — SDK typed
|
1f18a9b to
e492b00
Compare
39b9f66 to
05be291
Compare
e492b00 to
08da44a
Compare
05be291 to
7b9cfd5
Compare
08da44a to
38b7ad1
Compare
7b9cfd5 to
5dc98aa
Compare
38b7ad1 to
d28d014
Compare
5dc98aa to
68cd422
Compare
Tier 1 + Tier 2 fixes (
|
d28d014 to
fe430c1
Compare
Round-3 review fix: the `health_watch` example subscribed to `STATUS` alone and printed "nothing printed means nothing is wrong". Unknown category bits are ignored by design, so that subscription against a pre-0.5.0 node is accepted, matches nothing, and yields an open connection that stays silent forever — indistinguishable from a healthy node, and failing open in the one direction alerting must not. It now subscribes to `STATUS | HEARTBEAT`, so silence is itself a signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AfAm3VVN8gBeYcTL5BJqH
The `details` rustdoc is what a client author reads while writing the lookup, so two errors in it are two client bugs. It described the map as "decimal strings" — a `cleared` event can carry a `reason` token (`detector_disabled`, `mempool_cap_zero`), so a uniform integer parse over the map fails on a perfectly ordinary event. And it advertised "the watched path" as one of the fields, which the node deliberately withholds: the path reaches every status subscriber and every webhook receiver, and an absolute datadir path usually names the account the node runs under. A client author would have written a lookup for a key that never arrives. `StatusKind::TipStall` said "outside IBD". The detector deliberately does not suppress during IBD and says why at length — the IBD predicate is the tip header's age, not a sync flag, so a node that was caught up and then wedged re-enters it exactly when paging matters. Also corrected a stale `resilient_watch` comment claiming both carriers map `categories: 0` to `u32::MAX`; they map it to the default mask, which excludes the explicit-only `tweaks` and `status` bits — the whole point of those bits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AfAm3VVN8gBeYcTL5BJqH
**`#[non_exhaustive]` on `StatusKind`/`StatusState`/`StatusSeverity`.** Every other open enum in this crate has it (`EvictReason`, `CursorRejectReason`, `WatchSetRejectReason`, `RescanRejectReason`, `Event` itself) and these three were missed. The `Unknown(i32)` arm is not sufficient on its own: a downstream `match` that listed every variant including `Unknown(_)` is exhaustive, so adding a kind node-side would break it. The crate is `publish = true`, so shipping without this makes an additive node change a breaking SDK release, permanently. Adding it after publication is itself breaking — this is the one finding with a deadline. That the example's own matches stopped compiling is the proof: it had exactly the exhaustive-match shape a downstream consumer would write. **The example now reconnects.** It used a plain `subscribe` with `?` on `message()`, so any transient stream error ended the process — and the node restarting is both the most common cause and the moment every standing condition is re-raised. An unsupervised copy was off for good after its first blip, with no signal. **And enforces the silence it subscribes to.** HEARTBEAT was requested with a comment about silence being a signal, then unconditionally discarded, and nothing applied a deadline. A node process that is alive but whose publisher is wedged still answers gRPC keepalives, so `next()` blocked forever, the example printed nothing, and an operator read "no output" as "healthy" — failing open in the one direction alerting must not. There is now a 90s deadline that exits non-zero. **And it no longer prints "→ all clear" while a critical condition stands.** The set was seeded only from live events, so a `disk_low` raised before connect was invisible and any unrelated clear emptied the set. The rustdoc told callers to seed from `getwarnings` on connect, which is not implementable through `ResilientSubscription`: it hides reconnects, and its only synthetic notice is cursor-anchored while status carries no cursor — so a drop that straddles a raise and a clear leaves the picture silently wrong with nothing to key off. Rather than pretend, both the example and the rustdoc now say what is true: this is a partial view labelled "observed by this client", and `getwarnings` polled on a slow timer is the authority. That also makes a missed transition self-correcting instead of permanent.
68cd422 to
7758ebd
Compare
PR 6 of the A3 alerting stack (
SATD_ALERTING_DESIGN.md§10). Stacked on #484 → #483 → #482 → #481 → #480 — merge bottom-up.Closes the loop: the node detects (#481), the wire carries (#480), webhooks push (#482/#483), and a Rust client can now consume health events with types instead of proto.
What
Categories::STATUSplus a typedEvent::Status { kind, state, severity, message, details }.Decisions worth reviewing
StatusKind,StatusState, andStatusSeverityeach carry anUnknown(i32)arm. A condition a client build predates arrives asUnknownrather than failing to decode, and itsseverityandmessagestay meaningful — so a generic "log it, page on critical" handler keeps working when the node adds a taxonomy entry. That is the whole reasondetailsis a string map andStatusKindis an open enum on the wire; the SDK would waste it by decoding strictly.StatusSeverityderivesOrd, so a severity floor isseverity >= StatusSeverity::Warningrather than a match that has to be updated. An unrecognized severity sorts aboveCriticalon purpose: a condition the client cannot name is not one to quietly filter out.examples/health_watch.rsshows the shape a real integration wants — track raise/clear pairs to hold what is wrong right now, rather than counting alerts (a number that only ever grows). It also documents the two things that surprise people: the category is explicit-request only, and status events are not replayable, so a client connecting after a raise sees nothing until the condition changes (seed fromgetwarningsif you need the full picture on connect).Acceptance (design §10 PR 6)
STATUSsubscription decodes a realdisk_lowwith itsdetailsmap and no cursor; aCHAIN-only subscription never receives one.test_address_index_backfill_*disk-guard failures on this machine.The crates.io publish dry-run stays red until the 0.5.0 proto publish — known and pre-existing on every PR in this repo right now, unrelated to this change.
🤖 Generated with Claude Code
https://claude.ai/code/session_018AfAm3VVN8gBeYcTL5BJqH