test(sync): take the lock by retrying, not by assuming it is free - #972
Merged
Conversation
fujibee
force-pushed
the
fix/934-lock-race
branch
from
August 22, 2026 23:09
44b046b to
3cec142
Compare
The suite's most frequent failure. It fails in `setup`, on `mkdir "$lock"`, which reads as a broken fixture; it is a race. `sync start` holds this team's lock across the launch: remote.sh:2801 the starter acquires it remote.sh:2005 the SHELL writes the pidfile, right after `nohup ... &` remote.sh:2838 the starter releases it, once the engine is running remote.sh:2904 the cleanup tries to retake it The test waited for the pidfile and then took the lock. But the pidfile appears at the second line, and the lock is still the starter's until the third -- so a single `mkdir` there raced the starter's release and lost often enough to be the most common red in the suite. Retrying takes it the instant the starter drops it. Waiting for the lock to disappear and then taking it would be the same race one step later; retrying has no gap to lose. The window it then owns is `cmd_sync_start`'s readiness wait, which begins the line after the release: 1600 turns at 0.01s, a floor of 16 seconds and longer in practice because each turn also spawns a status probe, a tail and an awk (remote.sh:2873-2886). That is what the test's own comment asks for -- a helper holding the lock "for the whole window" -- and the retry ceiling, 400 x 0.05s, is sized to reach into it. (An earlier revision of this message cited 50 x 0.1s at remote.sh:1660. That loop is real but belongs to a different function and is not this window; the number was wrong and review caught it.) Failing to take it now says so, rather than surfacing as EEXIST in setup. Measured, same harness both arms, unloaded: before 4 / 10 failed after 0 / 10 failed Load was not the condition -- it reproduces on an idle machine. Control, because 0/10 does not distinguish "fixed" from "no longer measuring": with the retry removed and everything else identical, 1 / 6 failed. The harness still produces red for the shape this fixes. Two other approaches were tried and are recorded because each looked right: take the lock BEFORE starting the engine -- the pidfile then never appears, because the starter needs that same lock to launch; pass `_REMOTE_ENGINE_CALLER_HOLDS_LOCK=1` -- remote.sh:49 resets it at startup on purpose, so that an exported variable cannot make the engine skip its own locking (#762). That door is closed by design and a test should not pry it. Closes #934
fujibee
force-pushed
the
fix/934-lock-race
branch
from
August 25, 2026 05:59
3cec142 to
b07b5e7
Compare
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.
Closes #934.
The suite's most frequent failure. It fails in
setup, onmkdir "$lock", which reads as a broken fixture — it is a race.What races what
sync startholds this team's lock across the launch:remote.sh:2801remote.sh:2005nohup … &remote.sh:2838remote.sh:2904The test waited for the pidfile and then took the lock. But the pidfile appears at the second line and the lock is still the starter's until the third — so a single
mkdirthere raced the starter's release, and lost often enough to be the most common red in the suite.The pidfile means "the engine was launched", not "nothing holds the lock".
The fix
Retry the
mkdiruntil it succeeds. It takes the lock the instant the starter drops it, so there is no gap to lose.Waiting for the lock to disappear and then taking it would be the same race one step later. Retrying has no such gap.
The window it then owns is
cmd_sync_start's readiness wait, which begins the line after the release: 1600 turns at 0.01s — a floor of 16 seconds, and longer in practice because each turn also spawns a status probe, atailand anawk(remote.sh:2873-2886). That is what the test's own comment already asks for: a helper holding the lock "for the whole window". The retry ceiling, 400 × 0.05s, is sized to reach into it.Failing to take it now says so, instead of surfacing as
EEXISTin setup.Measured
Same harness both arms, unloaded:
Load is not the condition — it reproduces on an idle machine, which is worth knowing because "run it under load" was the obvious next step and would have been a detour.
Control, because
0/10does not distinguish fixed from no longer measuring: with the retry removed and everything else identical, 1 / 6 failed. The harness still produces red for the shape this fixes.Whole file: 9/9.
Two approaches that looked right and are not
Recorded because each one costs an hour to rediscover.
Take the lock before starting the engine. The pidfile then never appears — the starter needs that same lock to launch, so it blocks.
[ -f "$pidfile" ]fails instead, and the test still cannot run.Pass
_REMOTE_ENGINE_CALLER_HOLDS_LOCK=1.remote.sh:49resets it at startup on purpose: an exported variable of that name would let anything upstream make the engine skip its own locking and run the check-then-act unserialised (#762). That door is closed by design, and a test should not pry it open.What this does not do
#828 is not addressed, and I could not establish that it is the same shape. It looks like the same family —
sleep 4standing in for "the watcher stopped", where the watcher writes a line to stderr before it exits (watch.sh:640,:661) — but on this machine it is 0 / 20, so the condition is environmental. Fixing it here would be a change I cannot tell had any effect. Better to catch it red in CI and read the log first.