Skip to content

Serialize test suites sharing build artifacts with a per-checkout lock - #8658

Open
cristianoc wants to merge 1 commit into
masterfrom
cristianoc/test-artifact-lock
Open

cristianoc wants to merge 1 commit into
masterfrom
cristianoc/test-artifact-lock

Conversation

@cristianoc

@cristianoc cristianoc commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Why

Two agent sessions — or an agent plus a terminal — sharing one checkout run test suites concurrently and corrupt each other's build artifacts. The cost is worse than a lost run: make test-analysis cleans Belt partway through someone else's make test, and the victim reports missing-module or stale-artifact errors that look like real compiler bugs. The agent then chases a phantom failure, re-running and "fixing" code that was never broken.

The conflicts:

  • Belt. make test builds it via lib, then cleans and rebuilds it in the mocha phase (scripts/test.js:89); tests/analysis_tests/Makefile:11 cleans and rebuilds the same workspace as a workaround for Rebuild source dependencies when package output settings change #8539.
  • Syntax fixtures. Two scripts/test_syntax.sh runs both rm -rf tests/temp and rewrite the same expected/*.txt.
  • _build. Concurrent dune build already hard-fails on dune's own lock; this makes it wait instead.

Approach

scripts/with_test_lock.py takes a flock on .rescript-test.lock and execs the command, so PID, signals and exit status are preserved, and the kernel releases the lock once every process inheriting the descriptor exits — including after SIGKILL. Root make test* targets route through it via a _locked-* indirection; node scripts/test.js self-locks so direct invocation is covered too.

Two choices worth review:

  • Locked before prerequisites are entered, not around the test recipe — otherwise the lib build and suite cleanup still race.
  • Recursive Make runs with -j1 so nested clean test goals stay ordered. This costs no real parallelism: the suites are sequential recipe lines (tests/analysis_tests/Makefile:3 is five make -C lines in one recipe), test_syntax.sh fans out on its own, and dune, cargo and rewatch parallelize internally regardless of Make.

Nested commands reuse the active lock through an environment marker validated against the lock file contents and a live-PID check, so a stale marker from an exited owner cannot skip locking.

Windows has no flock and CI runs the suites there, so Windows runs unlocked with a notice; any other platform missing flock fails rather than racing silently.

Testing

  • scripts/test_test_lock.py — contention, nesting, exit status, killed owners, separate checkouts, stale markers, and POSIX-without-flock. 8/8.
  • make test-syntax → exit 0 through the locked path. Two concurrent invocations: the second waited, then acquired; both green. Unlocked, those two clobber each other's fixtures.
  • make checkformat clean. Full make test / make test-all not run.

No CHANGELOG entry — contributor tooling, no user-facing surface.

Open questions

  1. scripts/test_test_lock.py currently gates make test, adding ~1 s of process-handshake tests with 5 s deadlines that could flake on a loaded machine. Move it to its own target?
  2. make test-all is not atomic — the lock drops between leaf targets, so a concurrent session can clean Belt mid-sequence. Left alone since it is run rarely.
  3. Follow-up, not this PR: the parallelism worth having is inside scripts/test.js, where 39 tests/build_tests/* directories are awaited one at a time (scripts/test.js:153). Unaffected by this change, since Make's -j never reached that script.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fz7ZXPYxfMT8B1sRgqqDoD

Root `make test*` targets and `node scripts/test.js` now acquire a
`flock` held for the whole command, so concurrent invocations in one
checkout stop racing on shared build outputs.

The conflicts are real. `make test` builds Belt through `lib` and then
cleans and rebuilds it again in the mocha phase, while
`tests/analysis_tests` cleans and rebuilds the same workspace as a
workaround for #8539; either landing mid-run breaks the other. Two
`scripts/test_syntax.sh` runs both `rm -rf tests/temp` and rewrite the
same `expected` fixtures. Concurrent `dune build` invocations already
fail outright on dune's own `_build` lock.

The lock is taken before prerequisites are entered, so the build phase
is covered rather than just the test recipe, and each suite's recursive
Make runs with `-j1` so nested `clean test` goals stay ordered. This
costs no real parallelism: the suites run as sequential recipe lines,
and dune, cargo and rewatch parallelize internally regardless of Make.
Nested commands reuse the active lock through an environment marker
validated against the lock file contents and a live-PID check, so a
stale marker cannot skip locking.

The wrapper `exec`s its command, so the PID, signal disposition and
exit status are preserved, and the kernel releases the lock once every
process inheriting the descriptor exits, even after SIGKILL.

Windows has no `flock` and CI runs the suites there, so commands on
Windows print a notice and run unlocked; any other platform missing
`flock` fails rather than racing silently.

Signed-Off-By: Cristiano Calcagno <ccrisccris@gmail.com>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz7ZXPYxfMT8B1sRgqqDoD
@cristianoc
cristianoc requested a review from cknitt September 15, 2026 01:30
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T01:35:42.681028Z dfd52bf PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cristianoc

Copy link
Copy Markdown
Collaborator Author

Prob a few rough edges -- but gives the idea.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfd52bf668

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib_dev/test_lock.js
} catch {
// Missing/stale ownership: acquire through the OS lock, never skip it.
}
const child = spawn(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ensure the lock child dies with the direct runner

When a direct node scripts/test.js process is killed with SIGKILL (or another fatal signal not handled below), only this parent dies; the spawned wrapper/test process remains alive and continues holding the checkout lock. I reproduced this by killing the original Node PID, after which a second lock command waited until the inner PID was manually killed. This can leave tests running unexpectedly and block later suites, so the child must be tied to the parent's lifetime or the persistent intermediary should be avoided.

AGENTS.md reference: AGENTS.md:L163-L166

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Sep 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@8658

@rescript/belt

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/belt@8658

@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@8658

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@8658

@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@8658

@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@8658

@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@8658

@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@8658

commit: dfd52bf

@github-actions

Copy link
Copy Markdown

@cknitt

cknitt commented Sep 16, 2026

Copy link
Copy Markdown
Member

Thanks! Will test next week when I am back from vacation.

First Python script that we have in this project, but that's required to access flock.

Could you address the Codex review feedback and open questions 1 and 2?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants