Skip to content

fix: publish and recover multiscan locks atomically - #196

Draft
mldangelo-oai wants to merge 2 commits into
mainfrom
mdangelo/codex/atomic-multiscan-lock-recovery
Draft

fix: publish and recover multiscan locks atomically#196
mldangelo-oai wants to merge 2 commits into
mainfrom
mdangelo/codex/atomic-multiscan-lock-recovery

Conversation

@mldangelo-oai

Copy link
Copy Markdown
Collaborator

Summary

  • Preserve the original atomic multiscan-lock publication from fix: publish multiscan locks atomically #98 as a standalone commit authored by @GautamSharma99.
  • Serialize stale-lock recovery so concurrent supervisors cannot simultaneously acquire ownership.
  • Atomically replace stale regular locks and protect the temporary publication gap when migrating legacy directory locks.
  • Preserve a competing supervisor's ownership if the lock changes during recovery or release.
  • Reject process IDs outside Node's supported range and recover malformed, incomplete, oversized, or stale legacy lock records.
  • Clean up pending ownership files even when writing, syncing, or closing them fails.

Closes #102.

Why

The original two-step directory lock could permanently block a bulk scan after an interrupted owner write. PR #98 fixes that publication gap, but adversarial QA found that its stale-recovery path could admit several supervisors at once, overwrite a competing lock, accept unusable PID values, and leak pending files on I/O failure.

This replacement keeps the contributor's atomic hard-link publication and adds a separate, atomically published recovery claim. Stale regular locks are replaced without ever vacating the lock path; legacy directories are migrated only while a recovery claim makes competing acquisitions fail closed. Ownership release moves a candidate aside, verifies its token, and restores a replacement without clobbering it.

Regression coverage

  • 24 concurrent contenders recovering both stale regular-file locks and legacy directory locks.
  • A live replacement published while another supervisor checks stale ownership.
  • A replacement substituted between ownership verification and lock release.
  • Missing, truncated, malformed, oversized, zero, negative, out-of-range, and invalid-token owner records.
  • Pending-file cleanup after a simulated fsync failure.
  • Existing live-owner rejection, dead-owner recovery, resumable scan output, and checkout cleanup.

Verification

  • pnpm run types
  • pnpm run format
  • pnpm run build
  • pnpm pack --pack-destination <temporary-directory> and pnpm run check:package <tarball> — installed-package import, CLI, and all 95 bundled plugin files verified.
  • bun test --timeout 30000 ./tests-ts/multiscan.test.ts — 19 passed.
  • bun test --timeout 30000 --rerun-each=10 --test-name-pattern='serializes supervisors while recovering stale file and legacy locks' ./tests-ts/multiscan.test.ts — 10 repeated contention runs passed.
  • pnpm run test — 720 passed, 5 expected platform/integration skips, 0 failed.
  • Independent 24-contender stress harness — 100 stale-file trials and 100 legacy-lock trials with no overlapping supervisors; the original PR overlapped in 28 of 30 trials.
  • git diff --check.

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review exact pushed head 8cdc1f6, including the preserved contributor-authored atomic lock publication, serialized stale-lock recovery, ownership-safe release, bounded PID validation, failure cleanup, and 24-contender regression coverage.

@github-actions github-actions Bot added the bug Something isn't working label Aug 1, 2026

@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: 8cdc1f6880

ℹ️ 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".

if (recovering !== null && processIsRunning(recovering.pid)) {
throw new Error("A multiscan supervisor is already running.");
}
await rm(recovery, { force: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reclaim stale recovery markers without unlinking successors

When a crashed .lock.recovery exists and multiple supervisors restart concurrently, two contenders can both read the old owner; after one removes it and publishes its own recovery claim, the delayed contender can execute this unconditional removal against the new claim. Both can then enter recovery and replace .lock, allowing overlapping campaigns. Move the observed marker aside and verify its token before deleting it, as is done for lock release.

Useful? React with 👍 / 👎.

await releaseLock(path, token);
throw new Error("A multiscan supervisor is already running.");
}
return async () => await releaseLock(path, token);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate ownership before accepting a published lock

During legacy-directory recovery, there is a gap between moving the directory at line 337 and installing the recoverer's lock at line 339. A contender can hard-link its pending file into .lock during that gap, but if the recovery marker is released between its lstat and readFile, readLockOwner(recovery) returns null and this return admits it even though the recoverer has already replaced its lock. Confirm that .lock still contains this contender's token before returning, otherwise both supervisors can run concurrently.

Useful? React with 👍 / 👎.

try {
const moved = await readLockOwner(released);
if (moved?.token !== token) {
await link(released, path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore substituted legacy locks without hard-linking directories

If a legacy directory lock is substituted after the ownership check at line 426 but before the rename at line 431, this code moves that competing lock aside and then attempts to restore it with link. Hard-linking a directory fails on supported POSIX filesystems, and the non-recursive cleanup also cannot remove it, leaving the competitor's ownership stranded under .lock.released-* while .lock is vacant and allowing another supervisor to start.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 8cdc1f6880

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incomplete .lock directory permanently blocks bulk-scan recovery

2 participants