Skip to content

fix(lock): kernel-mediated byte lock with fail-safe cleanup and classified contention - #1

Merged
trollbot2012 merged 4 commits into
mainfrom
fix/repository-lock-concurrency
Aug 17, 2026
Merged

fix(lock): kernel-mediated byte lock with fail-safe cleanup and classified contention#1
trollbot2012 merged 4 commits into
mainfrom
fix/repository-lock-concurrency

Conversation

@trollbot2012

@trollbot2012 trollbot2012 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces the stale-file sweep (read pid, unlink path) with a kernel-mediated byte lock: the C runtime _locking operation on Windows, fcntl.flock on POSIX. The OS releases the lock when the holder dies, so stale recovery is kernel-provided and lock files are never unlinked.
  • Nested cleanup: the in-process thread lock is released even when unlock or close fails, preserving the primary exception (same for failed-acquisition cleanup).
  • Claim errors are classified: BlockingIOError/EACCES (POSIX) and EACCES/EDEADLK (Windows) map to the documented busy error; genuine errors (EBADF, EINVAL, …) keep their cause.
  • The pid record is diagnostic-only — write failures no longer abort acquisition (documented deliberate behavior change).

Lint policy

Ruff 0.16 expanded its default rule set and began failing on already-shipped code (RUF022, RUF100, BLE001, …) — a zero-change PR from main fails the same way. This PR pins the historical default selection explicitly (select = ["E4", "E7", "E9", "F"]) instead of capping the version, keeping lint behavior version-independent; re-expanding the rule set is left as a deliberate follow-up.

Test plan

  • 19 focused lock tests: deterministic two-contender race (subprocess barriers, no sleep-and-hope), injected os.open/os.write/unlock/close failures, errno classification matrix for both platforms, in-process bounded wait, live-holder fail-fast, stale-file recovery, parallel worktree stress.
  • Non-vacuity: repro tests fail against the original implementation and a restored faulty __exit__ ordering.
  • CI green on all four matrix jobs; Ubuntu py3.10 and py3.12 each run the full suite (109 passed), exercising the POSIX fcntl.flock path.

The stale-file sweep read a pid from the lock file and unlinked the path
in separate steps, so a contender could unlink a live lock a peer had
recreated after the contender's stale observation — two processes could
hold the same key. Exceptional acquisition paths also leaked the
in-process thread lock, and every claim error was reported as contention.

Cross-process exclusion is now a kernel-mediated byte lock on a
persistent per-key lock file (the C runtime _locking operation on
Windows, fcntl.flock on POSIX); the OS releases it when the holder dies,
so no sweep exists and lock files are never unlinked during acquisition
or release. Cleanup is nested so the thread lock is released even when
unlocking or closing fails, preserving the primary exception. Claim
errors are classified: BlockingIOError/EACCES (POSIX) and EACCES/EDEADLK
(Windows) are contention and map to the documented busy error; other
errors keep their cause. The pid record is diagnostic-only: write
failures no longer abort acquisition (previously they raised) — a
deliberate behavior change.
…rage

Subprocess contenders coordinated by marker-file barriers force the
stale-observation overlap that previously let both processes hold the
lock; fault injection covers os.open, os.write, unlock, and close
failures; errno classification is pinned for both platforms; the
in-process waiting floor is exercised with bounded event coordination.
Fail-fast and transient-holder tests now use real subprocess holders —
writing a live pid into the lock file no longer represents holding the
lock. Verified non-vacuous against the original implementation and a
restored faulty __exit__ ordering.
Resolve the four ruff findings in the test suite: unused redact import,
unused os import, and two never-read task bindings (side-effect calls
kept).
@trollbot2012
trollbot2012 force-pushed the fix/repository-lock-concurrency branch from cd98dfe to bf6e6e9 Compare August 17, 2026 02:50
Ruff 0.16 expanded its default rule set substantially, so an unpinned
install (the cap resolved 0.15.22) began failing on already-shipped code
(RUF022, RUF100, BLE001, ...). Select the historical default rules
(E4, E7, E9, F) explicitly so lint behavior is version-independent and
re-expanding the rule set is a deliberate future decision rather than
release-note roulette. Reverts the incidental __all__ sorting and drops
the <0.16 version cap.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bf6e6e9. Configure here.

self._path.unlink(missing_ok=True)
continue
if time.monotonic() < deadline:
_claim_lock_file(candidate)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lock scheme breaks mixed upgrades

Medium Severity

The new kernel byte lock still uses the same per-key .lock path as the old O_EXCL scheme. An upgraded process can open that file and take flock/msvcrt.locking while a pre-upgrade holder only owns the path via exclusive create and an open fd, so both can enter the critical section during a rolling upgrade.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bf6e6e9. Configure here.

@trollbot2012
trollbot2012 merged commit f1d86e4 into main Aug 17, 2026
5 checks passed
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