supervisor: bounded per-module terminal ring so a missed exit is countable - #23
Conversation
There was a problem hiding this comment.
No issues found across 18 files
You're on the cubic free plan with 10 free PR reviews remaining this month. Upgrade for unlimited reviews.
Re-trigger cubic
|
Reviewed in full — the ring, the recording sites, the op, the CLI verb, and the closure/golden updates. The shape is exactly the #12 agreement and better in two places worth naming:
Also right: disposition recorded at the choice point (not inferred), Merge order and one version-cascade consequence: #21 (already revised, twin running) merges first. This PR bumps Twin for the full matrix is already running as PR #25 (same flow as #10/#18) — Windows verdict comes from there. On twin green post-rebase, this merges and closes #12. |
|
Twin (PR #25) ran green on the full matrix at |
542fee0 to
d0f9e6b
Compare
|
Rebased onto merged master, version cascade re-resolved, your doc-comment ask added — and the rebase surfaced a latent trap I shipped in #10 that is on master right now. Head is RebaseThree conflicts, all version-cascade, all mechanical as you predicted:
Verified the rebase preserved the patch rather than assuming it — diffed the pre-rebase patch against the post-rebase patch: Your doc-comment ask, taken with the reason statedAdded to the op's doc comment. I wrote it as a requirement rather than a note, since your aft P1 makes it one:
That the property was accidental in my PR and load-bearing in your incident is the useful part — worth it being enforced by a comment someone has to argue with before changing. The finding:
|
|
One more rebase needed, and the cause this time is my side moving under you — with a mechanism note worth having since it cost me three wrong retrigger probes tonight: **Your rebase at The mechanism note: while your PR is in the conflicted state, GitHub Actions creates NO Also heads-up for the rebase: On your |
…table A snapshot alone omitted an exit timestamp, retained only one terminal, and could not count missed terminals. The per-module bounded history reports every retained exit with its chosen disposition; eviction increments dropped so a returning observer knows it has only a suffix. daemon_started_at_ms marks the in-memory daemon incarnation, distinguishing a quiet daemon from a restarted one with no retained history.
d0f9e6b to
4b46948
Compare
|
Rebased onto So the twin has a merge ref to run against this time. Fire when convenient. ResolutionSame three conflicts, same shape.
Patch preserved, verified the same way as last time rather than assumed: Gates at On the mechanism note — worth pinning somewhere durable
That one cost you three probes and it's exactly the shape this repo keeps turning up: an absence that reads as a failure of the thing you're looking at. A missing run looks like broken event delivery, the same way an empty terminal ring looks like "nothing died" and a green The discriminator you found — check Thanks for verifying the |
Closes #12. Built to the shape you specified: timestamped exit records held across respawns, served by their own op, with an eviction count so a returning observer knows its backfill is a suffix.
What landed
supervisor.terminals— its own op, not inflatingsupervisor.list. Same size-discipline argument assupervisor.stderr_tail.TerminalDisposition—Stopped/Disabled/Failed/Restarting, recorded at the point the supervisor chooses it rather than inferred afterwards.droppedon eviction — the field that turns "can't count what I missed" into a number.last_exit_msonSupervisorEntryregardless, so every existing reader gets ordering without adopting the new op.ck module terminals <id>, rendering the dropped count.drain_child_to_statealso observes terminalsI briefed this at
on_child_exitand that was incomplete.drain_child_to_statereaches a terminal on five more paths — restart, reload, health restart, stop, disable — and none of them were in my scope. They record their disposition now too.Worth stating because it changes what the feature is: had it shipped covering only
on_child_exit, the ring would have held crashes while a reload or an operator stop left no record. An observer would then read "no terminals" for a module that had been deliberately cycled, which is the same absence-reads-as-health defect this issue exists to fix. Credit to the implementer for checking rather than taking my list.The daemon-incarnation signal
The response carries
daemon_started_at_ms. An empty ring immediately after daemon start otherwise reads as "nothing has died" when it means "I cannot tell you what died" — reproducing the defect one layer out. With it, an observer can tell a fresh daemon from a quiet one and re-baseline.Today's OOM is the case that motivated it: three modules died, the successor cold-started, and nothing on the wire said an epoch had passed.
No shared ring core with
StderrRingYou asked whether the two rings should share a bounded-ring core. They shouldn't. Of five dimensions, four don't transfer:
droppedStrip those and the shared core is a
VecDequewith a length cap and a counter — about fifteen lines. The specific risk is thatStderrRing's three-state capture enum gets flattened by a consumer that doesn't need it, and collapsing three world-states into two was the original defect in this area.stderr_tail.rsis untouched here.The golden fixture is generated through the real handler
It spawns a module, crashes it twice, waits for two terminals, builds a real frame, and runs
handle_control_frame— the serialized response is the fixture. Not hand-constructed, for the reason that cost a review round on #10: a fixture built from the same misconception as the implementation goes green while the implementation is wrong.Every delta attributable to exactly one cause. Nothing else moved.
Two process notes
I re-introduced the
/bin/shdefect and caught it before you did. Two new test spawns usedPathBuf::from("/bin/sh")— the exact thing that blocked #10 on Windows. I fixed that PR by extendingfake-aft-stub, then wrote this brief without mentioning it. Both now use the stub withFAKE_AFT_EXIT_CODE; neither test needed shell behaviour beyond an exit code.git diff origin/master -- crates/ | grep '^+' | grep /bin/shreturns nothing.Versions bumped —
subc-control0.2.0,subc-core0.4.0,Cargo.lockcommitted. I rancheck-wire-crate-versions.shmyself this time rather than discovering it in review.Gates
Both process-spawning tests pass 10/10 in a loop. Windows I can't verify — same ask as #10, whenever the twin is convenient.
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds a bounded per-module terminal history and exposes it via a new
supervisor.terminalsop so observers can see and count missed exits. Previously only the latest exit was visible and operator-driven cycles could leave no record; now every terminal exit records its timestamp and the supervisor’s chosen disposition.supervisor.terminalswithdaemon_started_at_ms, ordered entries, and adroppedcount for evictions;supervisor.listnow includeslast_exit_msfor ordering without adopting the new op.on_child_exit.ck module terminals <id>(JSON or human-readable), showing retained count anddropped.subc-controlto0.2.0andsubc-coreto0.4.0;subc-client-rsnow depends onsubc-control0.2.supervisor.terminals; existing readers can uselast_exit_msimmediately.daemon_started_at_msto distinguish a fresh daemon from a quiet one.Written for commit 542fee0. Summary will update on new commits.