Skip to content

Lock a copied file only when its writers lock it - #2787

Merged
trissim merged 3 commits into
mainfrom
fix/copy-lock-owners
Oct 23, 2026
Merged

trissim merged 3 commits into
mainfrom
fix/copy-lock-owners

Conversation

@trissim

@trissim trissim commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

The rehearsal copy created a store-lock inode beside every plain file it copied.

Defect. PracticeCopy.include (installer/practice.py:118) copies each owned file through owner.copy_group. Undeclared files resolve to RootFile (locked_store.py owned_files), and the base RootFile.copy_group took a shared _store_lock on the source, which creates .NAME.lock (store_files.py _store_lock_file). On a root with 118,271 selected-tool ledger files, PracticeBus.select left 118,271 lock files, and the ledger carry (tools/cutover/carry_selected_tool_slots.py, draft #2780) counted 236,542 slots instead of 118,271. Install 26 would have done this.

Fix. Whether a file has a store lock is its owner's fact (IMPL-4, half-finished family: SQLiteFile.copy_group already skipped the lock, while the base assumed every file is lockable).

  • The base RootFile.copy_group copies with no lock.
  • LockedFile(RootFile, StoreLockOwner) is now the store lock owner on the file side, and StoreLockOwner's only direct child. It takes the shared lock around a copy, so every lock owner copies under its lock by construction.
  • LockedStore lists only LockedFile.
  • HistoryArchive becomes a LockedFile: its attach writes under the inherited locked() instead of a raw _store_lock(self.path), and it is first in STORE_LOCK_ORDER, since attach reads the source registry while holding it.
  • RegistryFile and SQLiteFile keep their own copy_group.
  • MRO: ReadLedger > ... > LockedStore > FileStore > LockedFile > JsonDocument > RootFile > StoreLockOwner > StoreOwner > DeclaredFamily > ABC. RootFile must precede StoreLockOwner, because FileStore(RootFile, ABC) and DeclaredFamily's ABC otherwise have no consistent order. The only name both sides define is __init_subclass__, and RootFile's calls super.

Measured. I copied 1,000 plain ledger files plus one ReadLedger through owned_files + copy_group. Main 0240b6d left 1,000 ledger locks; this branch leaves 0. The store lock is taken in both.

Test. test_copy_locks_only_files_their_writers_lock: plain ledger files copied through owned_files + copy_group leave no lock beside them, and a LockedStore file's copy still creates its lock. The old test that pinned "RootFile.copy_group refuses a lock file" now checks LockedFile refuses it, and that the base copies a lock without creating a lock of a lock.

🤖 Generated with Claude Code

The rehearsal copy took a shared store lock on every root file it copied,
through the base RootFile.copy_group. Taking the lock creates the lock inode
(store_files.py _store_lock_file), so a root holding 118,271 plain
selected-tool ledger files came out of PracticeBus.select with 118,271
`.NAME.lock` files beside them, and the ledger carry counted each one as a
slot (236,542).

Whether a file has a store lock is a fact of its owner. The base copy_group
now copies without one; the new LockedFile capability carries the shared lock
for the owners whose writers take it: the LockedStore family and
HistoryArchive, which locks history_sources.json itself. SQLiteFile and
RegistryFile keep their own copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trissim
trissim marked this pull request as ready for review October 23, 2026 14:11
Copilot AI balanced review requested due to automatic review settings October 23, 2026 14:11

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Citation fixture and others added 2 commits October 23, 2026 10:14
Review of a580b4f: "this file's writers hold its store lock" was stated
twice. LockedStore listed both StoreLockOwner and LockedFile, and
HistoryArchive took a raw _store_lock(self.path) beside its LockedFile
parent, so a new StoreLockOwner could forget LockedFile and copy unlocked.

LockedFile now is the store lock owner on the file side: LockedFile(RootFile,
StoreLockOwner) is StoreLockOwner's only direct child, LockedStore lists only
LockedFile, and HistoryArchive writes under its inherited locked(), with its
place first in STORE_LOCK_ORDER (attach reads the source registry inside it).
RootFile precedes StoreLockOwner because FileStore(RootFile, ABC) and
DeclaredFamily's ABC otherwise have no consistent MRO; the only name both
sides define is __init_subclass__, and RootFile's calls super.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trissim
trissim merged commit e24f4de into main Oct 23, 2026
7 of 8 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