fix(watch): claim the pidfile before displacing the previous holder (#595) - #803
Open
fujibee wants to merge 4 commits into
Open
fix(watch): claim the pidfile before displacing the previous holder (#595)#803fujibee wants to merge 4 commits into
fujibee wants to merge 4 commits into
Conversation
…ch event came from
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.
Declared reviewers: 1
Refs #595, #758, #769, #797.
Landing on
integration/remote. Head17b8cc81cdd1d2e6647bb8267bb3fe9bbfea9c60.What was wrong
scripts/watch.shsignalled the previous holder of a pidfile before claiming the slot:The guard at 333 exists so a departing watcher does not erase a successor's record. It is sound only if the successor's write cannot land between the read at 323 and the remove at 333. Signalling first is precisely what schedules it there: the predecessor is woken, reads its own pid, and the successor writes before the remove executes. The successor never writes again, so the slot stays empty for the rest of that watcher's life.
The fix is an order, not a check
Decide who to displace where that is already decided, and send the signal after the claim. Then the predecessor's own read cannot see its own pid, so the guard's condition is false and it removes nothing.
The guard's comment already described this order as the one in force — "a successor watcher overwrites
$PIDFILEwith its own pid before killing us". The code did the opposite. That comment is the reason the ordering was never questioned, so it is corrected here rather than left to mislead the next reader.Tightening the check instead would not close it: read-check-remove is three steps whatever the comparison is. The order removes the interleaving; a better comparison only narrows it.
Measured, both directions
The predecessor is stopped between its read and its remove, and the successor's write is pushed into that window — the interleaving pinned rather than raced for. Only the test environment's copy of the script is patched;
scripts/watch.shin the repository was verified unmodified afterwards.<no-file>The first attempt at this control did not reproduce it, and that is the useful part. With a 5s poll interval the predecessor is asleep when the signal arrives, so its cleanup read happens after the successor's write,
pidfile_pid != $$, and the guard correctly deletes nothing. The guard is not "usually right": it is right or wrong according to an ordering that nothing established.The test asserts the order, not a timing
A timing test passes on every machine that wins the race — which is how this survived:
bats tests/test_watch.batsis green on a developer machine, and the failure only ever appeared on CI runners. So the new case reads the two statements' positions, with both anchors required to exist so it cannot pass by finding nothing.kill "$prev_pid"restored inside the takeover blockrefutereddensThe first mutation was green against the first version of this test: its
refutewas anchored to the start of a line, and the restoredkillsits after acasepattern on the same line. The mutation found the hole before CI could; the pattern is unanchored now, and the reason is written beside it.What this does not claim
That the CI failures on
ubuntu-latest 4/4were this race. The evidence is strong and it is circumstantial: at the timeout the run directory holds.watch-start.<sid>.<successor-pid>(written at 389) and the filter file (251), and not the pidfile (253) written between them, while the successor is alive and still the same process. The filter file is the natural control — same writer, one line apart, but removed by an owner recorded inside the file (#766) rather than by read-check-remove. Only the deletable-by-a-departing-predecessor one was deleted.Nobody observed the removal itself, and the write at 253 has no error handling. See #595 for the full account.
Two other explanations were tested and dropped: the poll window (widened 3s → 10s in #797, still red) and load (still red on an idle queue).
Measurements
Drift
The destination moved while this branch existed — #792 landed. Drift is measured per file, and the files this touches did not move:
So this is measured empty, not structural — the destination has advanced and did not touch the surface under review. Re-measured immediately before landing.
Not in this PR
The instrument that produced the diagnosis is #797, which stays open and is not landed on the strength of this. If this change is right, the next
ubuntu-latest 4/4on a branch carrying it is green — and that is the only thing that closes #595.