feat(doctor): name the registry locks, and which directory to remove (#865) - #867
Closed
fujibee wants to merge 3 commits into
Closed
feat(doctor): name the registry locks, and which directory to remove (#865)#867fujibee wants to merge 3 commits into
fujibee wants to merge 3 commits into
Conversation
…865) A lock left behind by a killed process is never broken by anything: nothing expires, nothing sweeps, and acquire waits out its budget and reports 'timed out acquiring registry lock', which describes contention. The operator goes looking for the process holding it; there is none, and no message anywhere names the directory to remove. A machine in that state cannot get itself out. doctor now lists each team's lock with what it records, whether that process is running, and the removal as a line to paste. Three answers, not two. 'held' and 'the holder is gone' are what an operator acts on; 'no holder recorded' is neither - a lock this cannot ask about, written either by a library version that recorded nothing or by a process killed between creating the lock and writing its record. Calling that gone would be a guess, and the guess that costs is the one that says a live lock is dead. It removes nothing, and it does not touch the exit code. A team being locked is not a failed prerequisite - reporting it as one would fail doctor every time somebody is joining - and sweeping automatically is a separate decision with a worse failure mode. Reported for the named team when doctor is given one, matching what its header already says.
#865 review) Three findings, all from static review. _agmsg_pid_alive_local returns false for a value it never put to the process table - non-numeric, leading zero, past the POSIX ceiling - and folding that into 'not running' turned 'pid not-a-pid' into a stale verdict with a rm -r beside it. The number is validated with the same ceiling the helper uses before it is asked about, and an unusable one joins 'no holder recorded' under cannot-tell. The sweep used *, which does not match a leading dot, and team names may begin with one - the validator rejects empty, . , .. , a leading -, / , \ and control characters, and nothing else. A named team is now looked at directly, and the sweep adds the two dot patterns. All checks passed. printed under a stale lock and its removal command reads as withdrawing them. Narrowed to All prerequisite checks passed.; the exit code deliberately does not move, so the wording is what carries it. Five existing assertions updated with it. Reporting one lock is its own function now. Both callers share it, so neither has to join paths into a string and read them back - which would mean an unquoted heredoc, running command substitution on a team name, or unquoted word splitting, globbing one. The first attempt at the sweep did build such a string and fed a while-read loop that had no redirection at all, so it read stdin and reported nothing.
…eview) cmd_doctor takes an optional team and, until the direct lookup landed, only ever put it in a header sentence - so nothing had ever validated it. Building TEAMS_DIR/$team/.config.lock from it turned 'doctor ../outside' into a read of a directory outside the store, reported with its records and a rm -r to paste. The sweep it replaced never reached one only because no team was named that. Validated with agmsg_validate_team_name, the same one every other team-taking path uses, and a rejection ends the command rather than falling through to a prerequisites verdict for a question nobody answered. Three cases: a traversal argument reads nothing (with a sentinel .config .lock outside the store to prove the refusal is about reach), a slash is refused, and an ordinary and a dot-leading name still resolve - a refusal that took the legitimate names with it would be the cheapest way to pass the first two.
Owner
Author
|
Shipped in v1.2.1. This landed through Verified present in
Closing. The behaviour is live. |
Owner
Author
|
Shipped in v1.2.1 via integration/1.2.1 (squash-merged as #868), so GitHub could not close this by SHA. Verified in main: the doctor lock report is present. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #865 — the first of the three pieces it describes, and the one that removes nothing.
Describes head
3d3b0b784d49cbc1926bf07c617bc05049fa1318.Affects existing users, and only by printing more:
doctorgains a section. No behaviour changes, no exit code changes, nothing is deleted.What is missing today
A registry lock left behind by a killed process is never broken. Nothing expires, nothing sweeps, and
agmsg_lock_releaseis only ever reached from a trap — whichSIGKILL, an OOM kill and the machine going down do not run. Acquire then waits out its budget and says:That describes contention, so the operator goes looking for the process holding it. There is no process. And no message anywhere names the directory to remove, so a machine in this state cannot get itself out. Observed: two locks, six seconds apart, still there nearly three hours later.
What this adds
Three answers, not two.
heldandthe holder is goneare what an operator acts on.cannot tellis neither: it names what could not be established rather than asserting the lock is dead.What the three states change is the sentence, not whether a removal is offered.
staleandcannot tellboth print the removal, because the operator on a wedged machine needs it in exactly those two cases — a legacy lock with no holder record is the one koit's machine is sitting on. What carries the safety is that the line is conditional (if no agmsg command is running for this team) and that the state above it does not claim more than was measured. Onlyheldwithholds it, and says so. Callingcannot tellstale would put a measurement's weight behind a guess.Two things reach
cannot tell, and the second was a defect found in review. A lock with no holder record at all — written either by a library version that recorded nothing or by a process killed between creating the lock and writing its record. And a holder whose pid was never put to the process table:_agmsg_pid_alive_localreturns false for a non-numeric value, a leading zero, or a number past the POSIX ceiling, and folding that into "not running" turnedpid not-a-pidinto a stale verdict with arm -rbeside it. The number is validated with the same ceiling the helper uses before it is asked about.It removes nothing, and it does not touch the exit code. A team being locked is not a failed prerequisite — reporting it as one would fail
doctorevery time somebody is joining. Sweeping automatically is a separate decision with a worse failure mode, and it is not made here.The named team is validated before it becomes a path.
cmd_doctortakes an optional team and, until the direct lookup landed, only ever put it in a header sentence — so nothing had ever checked it, anddoctor ../outsideread a directory outside the store and offered to remove it. Raised in review. It goes throughagmsg_validate_team_namenow, the same one every other team-taking path uses, and a rejection ends the command rather than falling through to a prerequisites verdict for a question nobody answered.Reporting one lock is its own function, shared by both callers, so neither has to join paths into a string and read them back — which would mean either an unquoted heredoc (running command substitution on a team name) or unquoted word splitting (globbing one). The first attempt did build such a string, and fed it to a
while readloop with no redirection at all: it read stdin and reported nothing. Caught by reading the structure back, not by a test — the cases were written against the behaviour, and a loop that reports nothing fails them, but the run that would have said so was still going when the code was re-read.A named team is looked at directly, and the sweep does not stop at
*. Team names may begin with a dot — the validator rejects empty,.,.., a leading-,/,\and control characters, and nothing else — and*does not match a leading dot, so.foowas invisible to it. Raised in review.The summary line is narrowed to
All prerequisite checks passed.All checks passed.printed under a stale lock and its removal command reads as withdrawing them, and the exit code deliberately does not move.Liveness is
_agmsg_pid_alive_local, already sourced byremote.sh: it readsEPERMas alive, a zombie as gone, and cross-checks withps. A barekill -0readsEPERMas dead, which in a sandbox would report a live lock as stale.The removal line is quoted (
lib/shquote.sh's scheme), because the store root and a team name can both contain a space and an unquoted path becomes several arguments torm -r.Order, and why this piece is first
The issue names three fixes. This is the one with no way to make things worse:
doctorfinds them and says which directory to removedoctor --fixsweepsThat third one exists as a draft (#866) and has already earned the caution: its first version bound the removal to a path rather than to the holder it had judged, so a lock that changed hands between the verdict and the rename could be taken from a live process. Review caught it. A wrong verdict there costs a lock somebody is using; a wrong verdict here costs a line of output.
Mutations
Each reverted alone, and each reddens its own case:
doctor <team>reports that team and not anotherpid not-a-pidand a pid past the ceiling saycannot tell*aloneAll checks passed.Both liveness controls are asserted, not assumed. The stale case spawns a process,
waits for it, and checks_agmsg_pid_alive_localsays gone before writing that number into the holder — a pid that merely happened to be free would pass for the wrong reason. The live case checks the holder is alive at the momentdoctorruns.Before this lands: one rebase step, written down here so it is not re-derived
This branch renames the summary line, and #862 — already on the 1.2.1 integration branch — adds and edits assertions on the old wording. Landing order is #862 first, so when this rebases onto
mainafter 1.2.1 the following will be present and must move toAll prerequisite checks passed.(measured on #862's head15d0067e, four assertions intests/test_remote.bats):remote doctor: passes when age is installedremote doctor: passes with the full toolchain installedremote doctor: age is optional — its absence does not fail the runremote doctor: reports a SHA-256 tool, and its absence does not fail the run— this one is fix(hash): SHA-256 without shasum, and no fingerprint it could not compute (#861) #862's own edit, and the reason the collision exists at allThe five on this branch are already updated. The check after rebasing is
grep -c 'All checks passed' tests/test_remote.batsreturning 0 for assertions (one comment mentions the phrase and is not one).Suites
tests/test_remote.bats -f 86513/13,-f doctor22/22 (13 new). Not the full suite locally; CI is the gate.