Skip to content

cli: fix concurrent mops invocations clobbering shared scratch dirs#532

Merged
Kamirus merged 1 commit into
mainfrom
cli/fix-concurrent-mops-races
May 6, 2026
Merged

cli: fix concurrent mops invocations clobbering shared scratch dirs#532
Kamirus merged 1 commit into
mainfrom
cli/fix-concurrent-mops-races

Conversation

@Kamirus
Copy link
Copy Markdown
Collaborator

@Kamirus Kamirus commented May 6, 2026

Problem

Running two mops processes on the same project (e.g. an editor watcher and caffeine check --fix, or two back-to-back invocations from a script) intermittently failed with misleading errors:

  • .mops/.check-stable/new.most: No such file or directory from mops check-stable
  • EEXIST: file already exists, symlink ... from mops check/build/check-stable when [canisters.<name>.migrations] was configured

Worse, check-stable would print a "Hint: You may need a migration" suggestion (now removed in #531) that pointed users at the wrong cause entirely.

Root cause

Two scratch directories were shared across all mops processes for the same project:

  1. .mops/.check-stable/runStableCheck did rm -rf <dir>mkdir <dir> at the start and rm -rf <dir> at the end. Two concurrent runs would: A creates new.most, B's startup rm -rf deletes it, A's subsequent --stable-compatible read fails.
  2. <parent>/.migrations-<canister>/prepareMigrationArgs (used when migrations need a trimmed/next-merged staging dir) wiped the dir and re-symlinked. Two concurrent runs raced on symlinkSync (EEXIST) or on each other's wipe.

Both directories existed mostly to give moc a stable path to read from during the invocation; they weren't actually shared state that needed to be coordinated.

Fix

Switch both directories to per-invocation unique paths created with fs.mkdtempSync:

  • .mops/.check-stable/.mops/.check-stable-XXXXXX/
  • <parent>/.migrations-<canister>/<parent>/.migrations-<canister>-XXXXXX/

Each process owns its own dir for its lifetime and removes it on exit. No shared mutable state → no race possible by construction. Existing .gitignore rules already cover both patterns (.mops/ and .migrations-*/).

Caller behavior is unchanged: prepareMigrationArgs still returns a cleanup() callback that removes the staged dir in finally.

Caveats

  • A crashed process leaks its scratch dir (small — symlinks + a couple of .most/.wasm files). Both locations are gitignored, so it's cosmetic.
  • Stale .mops/.check-stable/ or <parent>/.migrations-<canister>/ directories from older releases aren't auto-cleaned. They'll stay until the user removes them; harmless but visible in git status for projects that don't ignore them.

Test plan

  • Added regression test: 10 parallel mops check-stable invocations against a fixture with check-limit = 3 and 4 chain migrations (forces both the check-stable scratch path and the migration staging mkdtemp branch).
  • Existing migrate / check / build / check-stable suites pass (103/103, 64 snapshots).
  • npm run lint + npm run check clean.
  • Empirical reproduction of both error symptoms before the fix; both gone after.
  • Reviewer sanity: trigger the original failure with two parallel mops check-stable (or caffeine check --fix + an editor watcher) on a project with [migrations] configured; confirm clean output.

Made with Cursor

`mops check-stable` shared `.mops/.check-stable/`, and `mops check`/`build`/`check-stable`
shared `<parent>/.migrations-<canister>/` for migration staging. Two processes running on
the same project (e.g. an editor watcher + `caffeine check --fix`) raced on these dirs and
surfaced as `new.most: No such file or directory` or `EEXIST: file already exists, symlink`.

Switch both to per-invocation `mkdtempSync` dirs so each process owns its own scratch space
and removes it in `finally`. No coordination needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Kamirus Kamirus requested a review from a team as a code owner May 6, 2026 12:05
@Kamirus Kamirus merged commit 140150f into main May 6, 2026
25 checks passed
@Kamirus Kamirus deleted the cli/fix-concurrent-mops-races branch May 6, 2026 12:41
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.

1 participant