Repository navigation
Conversation
…k in _common.sh A dead holder's line X is now reaped under tickets <lock>.reap.<cksum X>.<N>, written aside and ln'ed (exclusive, never empty). Ticket N+1 is allowed only when ticket N's pid is dead, never by age. Only the highest ticket's holder may rm X, and only while the lock still holds exactly X. A new holder clears other keys' tickets. Lock lines gain a nonce= field. exec.sh and test-scratch.sh call mxtk_lock_take/mxtk_lock_release; both stop with the sync remedy on an older _common.sh. bench-lock-race.sh, 200 rounds x 10 contenders, Linux: old block (master d16f578): 13 overlaps in 6 rounds, 12 stolen locks reap tickets: 0 overlaps, 0 stolen, 0 starved, 0 probe failures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…me a blocking reaper 1. An empty X is not unique: the old code (and the no-hard-link fallback) creates its lock empty and writes after, so a fresh empty lock could replace the stale one while a reaper sat between compare and rm. Before the rm, an empty X must still be over a minute old (find -mmin +1). test-lock-reap.sh swaps one in at the compare anchor; the case fails on a677671 and passes here. 2. When mxtk_lock_take gives up because a live reaper's ticket (or a live old-style .takeover) never finished, MXTK_LOCK_BLOCKER names the pid and file, and mxtk_lock_blocked_text says to resume (kill -CONT) or end it. exec.sh and test-scratch.sh print it on rc 2. Bench, 60 rounds x 10 (Linux): default anchors 0 overlaps / 0 stolen / 0 starved / 0 probe failures; all four anchors the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
This was referenced Oct 14, 2026
Merged
Merged
_common.sh: end-of-file conflict with the mxtest helpers (#370), both sections kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Review nit on #379: it described a blocker from an earlier attempt when the last attempt ended for another reason (a fresh empty lock being written). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
MendixMau
added a commit
that referenced
this pull request
Oct 15, 2026
sync-project.sh --only-bin <name> installs or refreshes just the named project-bin scripts, without running a full sync. It fixes the #390 field finding that --upgrade-bin ran a whole sync. Review: independent review MERGE after one fix round. - --only-bin scope probed on both layouts: only bin/<name> changes. A dry run, a path-like or unknown name, a NOINSTALL name, a glob and an empty name all write nothing. - The full sync is byte-identical to master over 18 runs. - All five fix-round findings verified: the refresh hold when _common.sh is edited, manifest validation, set -f, the empty-name usage error and the dry-run backup note. - test-sync-only-bin 40/0 and test-bug12-sync 61/0. Portability, pr-discipline, leak guard and render-routing are clean. - Follow-up: #379 switches its two plain --upgrade-bin _common.sh remedies to --only-bin.
Brings in #396 (sync-project.sh --only-bin). No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
exec.sh and test-scratch.sh told users to run sync-project.sh with --upgrade-bin _common.sh alone, which runs a full sync. They now give --only-bin _common.sh --upgrade-bin _common.sh, matching test-scratch.sh's mxtest guard on master. app-restart.sh is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
MendixMau
added a commit
that referenced
this pull request
Oct 19, 2026
Promote release/v1.0 to master for the 1.0 release: 17 squash-merged PRs plus the release notes (#428). Each was independently reviewed and green against release/v1.0. Review: independent review MERGE-AFTER-FIX on the PR body only; the branch content was ready. - The commits match the PR table one for one. - Context cost reproduces with render-routing --check: baseline 73,558 -> 73,682 of 80,000. - The added-line client-name scan finds 0 hits; leak guard and docs numbering are clean. - The body fixes (tag name, count, Behaviour row, decomposition range) were applied before merge. - Known: #384 and #379 will conflict in project-bin/exec.sh and need a master merge before they land.
Owner
Author
|
After 1.0: master still uses the takeover-file lock from #304, so the race this closes is still there. This needs a rebase onto the current exec.sh. |
4 of 5 tasks
exec.sh: keep master's watcher pause (#451) on the new shared lock: trap 'mxtk_resume_watchers; mxtk_lock_release "$LOCK"' EXIT, then mxtk_resume_leftover. CHANGELOG: both sides kept; the lock line now cites the macOS bench PASS from #390 and says the Git Bash run is still owed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
MendixMau
pushed a commit
that referenced
this pull request
Oct 26, 2026
…-changing git pull/checkout The restatement guard's ledger only saw exec.sh writes, so a pull that brought newer model work in left it saying "05 wrote last" and a replay of 05 over that work stayed quiet. install-project-hooks.sh now installs post-merge + post-checkout hooks that append a RESTORE row (as restore-mpr.sh does) when the operation changed the .mpr or mprcontents/. Squash merges and file checkouts give the hook no range, so they always mark: the cost is one capped DESCRIBE round. Fixture test-restatement-git-hooks.sh: real git, both layouts, chained foreign hook, idempotent re-install; 26/26, and the #504 repro fails on master. test-model-stamp 35/35, test-mx-metadata-note 40/40, test-restatement-guard 58/58. Not done: restore-mpr.sh taking the exec lock (optional ask) — draft PR #379 moves the lock into _common.sh; do it on top of that. Fixes #504 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
This was referenced Oct 26, 2026
Conflicts: - project-bin/exec.sh: master (#590) moved the old exec_lock_* block to ${EPOCHSECONDS:-$(date +%s)} and added the MXTK_PROFILE report to the EXIT trap. Kept the PR's shared mxtk_lock_take call sites; the held-for age uses EPOCHSECONDS, and the trap keeps `_prof_report`. - CHANGELOG.md: both sides' Unreleased lines kept; the PR's line notes the merge. Also: the shared lock in _common.sh reads the clock as ${EPOCHSECONDS:-$(date +%s)} (bash 3.2 falls back to date), and bench-lock-race.sh absolutises a relative --common (contenders run from $WORK, so a relative path made every probe fail). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LeNWZV2sVH62vkgfYh5u5
…age, bench liveness on Git Bash - bench-lock-race.sh: the contender's alive() now counts empty `ps -o args=` output as alive (the lock's own rule), so on Git Bash --old can show overlaps. With a fake ps that prints nothing, --old 40x10 went from overlaps=0 to overlaps=10 (and 5, 2 on reruns). - mxtk_lock_blocked_text: a pid that is NOT an exec.sh / test-scratch.sh (reused) is never told to be killed; its remedy is `rm -- "<ticket>"`. Only a stopped/hung reaper is resumed or ended. - An empty lock file under a minute old sets MXTK_LOCK_BLOCKER="empty <lock>"; exec.sh and test-scratch.sh then say the lock file is empty (a crashed take) and to re-run in a minute or delete it, not "another session keeps re-taking it". - _mxtk_lock_gc: glob test first; cksum/awk only when a ticket exists. - Fixture cases in test-lock-reap.sh and test-exec-lock-watch.sh (8 assertions, all FAIL on the previous head, PASS now). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LeNWZV2sVH62vkgfYh5u5
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.
Direct lane
What changed and why (one paragraph):
Before this change, a stale lock was taken over under one
<lock>.takeoverfile. That file went stale by pid or by age, and it was moved aside and put back withln. If a third contender created a new takeover file in that gap, thelnfailed and two contenders were inside the takeover at once. One contender's compare-then-rm could then delete the other's fresh lock (r-eng-backlog item 1). An earlier fix that re-checked our own line just before the rm was rejected, because it only narrowed the window. This PR replaces the takeover file with reap tickets:<lock>.reap.<cksum X>.<N>. The ticket is written to<lock>.tmp.<pid>and thenlned into place, so it is exclusive and never empty.rm, and only if the lock still holds exactly X. It then creates its own lock exclusively.nonce=field, so a line can never come back. This is why the compare-then-rm cannot hit a fresh lock: while the lock holds X, only one live process can be past the ticket check for X.The code now lives once, in
project-bin/_common.sh, asmxtk_lock_take/mxtk_lock_release/mxtk_lock_live(+mxtk_lock_fields,mxtk_lock_blocked_text). It is pure bash 3.2 with POSIX tools and noflock. It replaces exec.sh'sexec_lock_*and test-scratch.sh'stake_over/holder_live. The exec.sh diff is only the call sites plus the older-_common.shguard (the app-restart.sh pattern), so it should not collide much with #355, the exec-on-copy PR or the baseline-lock PR.Review fixes (second commit, 81981f2):
find -mmin +1). The new case intest-lock-reap.shswaps in a fresh empty lock at thecompareanchor. It fails on a677671 and passes now..takeover, never finished,MXTK_LOCK_BLOCKERnames the pid and file. That covers a SIGSTOPped reaper, or a reused pid whereps -o args=cannot tell processes apart.mxtk_lock_blocked_textthen says to resume the process (kill -CONT <pid>/fg) or end it (kill <pid>). exec.sh and test-scratch.sh print this on rc 2.mxtk_pid_argsnot used. Reusingmxtk_pid_argsin the liveness check was left out on purpose. On Windows it runs PowerShell, about a second per call, inside a retry loop. On Linux it gives the same answer asps -o args=for this match. So it does not come out clean.Mixed versions:
.takeoveris live, and clears a stale one once it holds the lock.test-lock-reap.shruns 9 old-block and 9 new contenders against one dead holder, and all 18 acquire.Test anchors:
MXTK_LOCK_TEST_PAUSEnames a command that runs as<cmd> <anchor> <pid>at four points:ticket-taken,compare,rmandcreated. When it is unset it does nothing.MXTK_LOCK_RETRY_SLEEPsets the pause between attempts (default 1 s).Regression harness:
tests/wave2/bench-lock-race.sh.ticket-taken/compare/rm(25% each), and are also SIGKILLed at random 0–50 ms offsets (10%).--oldruns master's block, the verbatim copy infixtures/mxtest-guard/exec-lock-block.sh, identical to d16f578. Anchors are sed-injected at the same points, plusjudgedandaside(the old move-aside step).Measured, Linux, 200 rounds × 10 contenders, same settings (first commit):
--old)The reap-ticket run left 0 files beside the lock at the end.
After the review fixes, 60 rounds × 10 contenders:
created)Fixtures:
tests/wave2/test-lock-reap.sh(45/45) covers each rule separately: the line format and nonce; release removes only our own line; a live holder refuses; every old and new line format is reaped; a live ticket dated 2000 still blocks (no age rule) and names its pid, file and remedy; dead tickets 1 and 2 lead to ticket 3; the line changing at the compare anchor; a higher ticket appearing stops the rm; a reaper SIGKILLed atrmdoes not block the next; GC; an empty lock; an empty lock swapped in mid-reap; legacy.takeoverinterop; mixed-version no-deadlock; and a 15-round harness run.test-shared-lock-takeover.shandtest-test-scratch.shnow source_common.shinstead of awk-lifting the lock code.Field runs
--old(master's block)Field evidence: no consuming-project run; this is concurrency code, and the proof is the harness. The macOS run covers APFS hard links,
ps -o args=on a macOS zombie and fractionalsleep. Still owed before merge: one Git Bash run ofbash tests/wave2/bench-lock-race.sh(and--old) to cover NTFS hard links andkill -0alone deciding liveness. That is why this PR stays a draft.Merge with master (7f4169d):
origin/master(24fe336) is merged in with a merge commit. The end-of-file conflict inproject-bin/_common.shwith #370's mxtest helpers was resolved by keeping both sections. After that, c932fa1 clearsMXTK_LOCK_BLOCKERat the start of every take attempt (review nit), so it describes only the last attempt.CHANGELOG.mdline appended, creditedImpact
rm -- "<ticket>"(79850b9). An empty lock under a minute old now says so instead of "another session keeps re-taking it". Draft until the macOS + Git Bash bench runs exist.project-bin/_common.sh(newmxtk_lock_*),project-bin/exec.sh(call sites),project-bin/test-scratch.sh. The end-of-file conflict with master's mxtest-identity section inproject-bin/_common.shis resolved by merge 7f4169d (both sections kept; fixtures green on c932fa1). Also conflicts with #384 in_common.sh;exec.shmerges clean with #374 and #375. Platform: bash 3.2 + POSIX; relies onlnhard links,ps -o args=,find -mmin, fractionalsleep; macOS + Git Bash runs owed. Reads only its own lock and ticket files.git merge-treevs 868b3cb (CONFLICT in_common.sh) and pairwisebin/render-routing.sh --checkon master 868b3cb vs this head (81981f2) merged onto 868b3cb viagit merge-treein a temporary worktree (conflict markers left inproject-bin/_common.sh, which render-routing does not read)MXTK_LOCK_RETRY_SLEEPdefault 1 s.tests/wave2/bench-lock-race.shresults in the PRUpdate 2026-11-03
Merge:
origin/master(dbd9b3e, 21 commits past the last merge base 6615869, incl. #587/#590 exec speed work, #596/#597/#603/#606) merged into this branch as 91d08ff. No rebase, no force-push. The PR's diff against master is still the same 9 files.Conflicts and how they were resolved:
project-bin/exec.sh, lock block: master had edited the oldexec_lock_*block this PR removes, switching$(date +%s)to${EPOCHSECONDS:-$(date +%s)}(exec: ~60% fewer process starts per exec (Windows overhead, #584/#585) #590, fewer process starts) andmkdir -p "${LOCK%/*}"(merged cleanly). Resolution: the PR'smxtk_lock_takecall sites stay; master's intent is carried over by using${EPOCHSECONDS:-$(date +%s)}for the held-for age in exec.sh and in the shared lock in_common.sh(_mxtk_lock_legacy_busy, the nonce, the lock line). Under bash 3.2, which has noEPOCHSECONDS, it falls back todate.project-bin/exec.sh, EXIT trap: master added theMXTK_PROFILEreport. The trap is nowmxtk_resume_watchers; mxtk_lock_release "$LOCK"; [ "$_PROFILING" != 1 ] || _prof_report.CHANGELOG.md: both sides' Unreleased lines kept. This PR's line stays at the top and now notes the merge.project-bin/_common.shandproject-bin/test-scratch.shmerged without conflicts. Master did not change the lock logic itself, so nothing in this PR was dropped as already on master. I diffed master's current exec.sh lock block againstfixtures/mxtest-guard/exec-lock-block.sh: the only differences are the EPOCHSECONDS spellings and comments.Harness fix:
bench-lock-race.shnow makes a relative--commonpath absolute. Before, the contenders, which run from$WORK, could not source it, and every probe failed.Test evidence, Linux, bash 5.2, on the merged tree (scoped fixtures only, no full suites):
app/layout)bench-lock-race.shwith the new code, 200 rounds × 10 contenders: 0 overlaps, 0 stolen, 0 starved, 0 probe failures, 517 SIGKILLs, 1684 acquisitions, 0 files left.bench-lock-race.sh --oldagainst master's current exec.sh lock block (extracted from dbd9b3e), 40 × 10: 9 overlaps in 4 rounds, 6 stolen, exit 1. The race reproduces on master and is gone with this PR.Still owed: the Git Bash run. This environment has no bash 3.2, so the 3.2 evidence is still the macOS run in #390; the EPOCHSECONDS fallback is a plain
${var:-default}.Review fixes (79850b9):
alive()inbench-lock-race.shtreated emptyps -o args=output as dead, so whereps -odoes not exist every overlap was invisible. It now uses the lock's own rule:case "$args" in ""|*contender-exec.sh*) return 0 ;;. Proof with a fakepson PATH that prints nothing and exits 1 (Git Bash simulated):--old40 × 10 reported overlaps=0 (stolen=5) before the fix and overlaps=10 in 5 rounds (stolen=9) after; two more--oldruns gave 5 and 2 overlaps. The new code under the same fakeps, 100 × 10: 0 overlaps, 0 stolen.killfor a reused pid.mxtk_lock_blocked_texttold the user tokill $pwhen the pid was "not an exec.sh / test-scratch.sh at all (a reused pid)", which can kill an unrelated process. The message now has two separate cases, after aps -o args= -p <pid>check: (a) it IS a stopped or hung exec.sh / test-scratch.sh reaper — resume it (kill -CONT) or end it (kill), and leave its ticket alone; (b) it is NOT one — do not kill it, delete the dead reaper's ticket:rm -- "<ticket file>". A live reaper's ticket is never offered for deletion.mxtk_lock_takesetsMXTK_LOCK_BLOCKER="empty <lock>". exec.sh and test-scratch.sh then say the lock file is empty (a crashed take) and to re-run in a minute or delete it (rm -- "<lock>"), instead of exec.sh's "another session keeps re-taking it"._mxtk_lock_gctests the<lock>.reap.*glob first and computes the cksum/awk key only when a ticket exists, so an uncontended take starts no extra process for it.New fixture assertions (all FAIL on 91d08ff, PASS on 79850b9):
test-lock-reap.sh+6 (the ticketrm --remedy, kill and rm in separate cases, no kill for a reused pid;blocker=empty, the empty-lock remedy),test-exec-lock-watch.sh+5 (exec.sh on a fresh empty lock: refuses, says "lock file is empty", namesrm -- "<lock>", no "keeps re-taking it", nothing executed). On 91d08ff with the new fixtures: test-lock-reap 44/49, test-exec-lock-watch 86/89.Test evidence on 79850b9, Linux, bash 5.2 (scoped fixtures only):
pson PATH (Git Bash simulated)--old, 40 × 10check-portability: clean (339 files). check-scripts: 126/126 shell, 27/27 Node parse.
Git Bash run (owed before merge)
Run on Windows in Git Bash, from a checkout of this branch (79850b9 or later), with a real project at hand:
--oldon the same machine: ≥1 overlap or stolen lockln a bin a real.mpr-snapshotsdir: link count 2Review: pending independent review
Generated by Claude Code