Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneeshammz

Relationships

#2709 Concurrent extension installs and removals can interleave in one checkout: add a pulled-extensions lock and LockfileRepository.refresh()

Opened by hammz · 9/29/2026· Shipped 9/29/2026

Summary

Nothing stops two installs or removals from changing the same checkout's pulled extensions at once. swamp serve's extension.install handler, a CLI extension pull/update/rm, and an auto-resolve in another command can all interleave. The lockfile's advisory .lock covers only the single writeEntry / removeEntry call. The copy into the pulled root, orphan pruning, and the catalog save are unprotected. Interleaving can leave a mix of two versions' files, prune files the other install just wrote, or record an entry whose files are not the ones on disk.

A second problem makes this worse: LockfileRepository serves reads from a construction-time snapshot (src/infrastructure/persistence/lockfile_repository.ts:48-56). Re-reading the lockfile after taking a lock therefore returns the stale snapshot.

This is unit U3 of the swamp-club#2612 split (see the planning ripples there). It affects every repo and ships on its own. Depends on swamp-club#2708 (the installExtension prepare/apply split).

Scope

  1. Per-checkout pulled-extensions lock: an in-process keyed mutex plus a cross-process FileLock (src/infrastructure/persistence/file_lock.ts) on .swamp/pulled-extensions.lock. Provide acquire() and tryAcquire() (the latter is for #2612's convergence, which reports busy instead of waiting).
  2. Reentrancy: an AsyncLocalStorage lease, cleared in finally. An install nested inside a locked section (e.g. an auto-resolve triggered while one is held) runs inline. A promise that resolves after the section has exited cannot bypass the lock.
  3. Where the lock is held: around applyInstall and removal. It is never held across prepareInstall's network I/O, and never across the CLI's conflict prompt (the prompt happens after ConflictError propagates out of the service, so the lock is already released). Entry points:
    • InstallExtensionService.execute, held through the catalog saveAll and the DuplicateTypeError rollback. UpgradeExtensionService and every factory built on it inherit this.
    • RemoveExtensionService.execute (src/libswamp/extensions/rm.ts:211).
    • The auto-resolver's direct installExtension call (src/cli/auto_resolver_adapters.ts:309), via a wrapper.
    • Dependency recursion runs under the parent's lease.
  4. LockfileRepository.refresh(): re-read the file into the cache. It is called first thing under the lock in apply and in RemoveExtensionService.execute (fixes ADV-36 from the #2612 plan review).
  5. Lock order: datastore global lock (when held) → pulled-extensions lock → lockfile advisory .lock (innermost). The auto-resolver runs inside commands that already hold the datastore global lock (model validate/evaluate, workflow evaluate). swamp-club#2495 must therefore take the global lock before this one, never inside apply. Document this in the design doc so #2495 follows it.
  6. Stale lock: FileLock's TTL and heartbeat cover crashed holders. Choose a max wait that fits the longest apply, and make the timeout error name the lock file and the holder.

Tests

  • Unit: two concurrent InstallExtensionService.execute calls on the same checkout run one after the other (assert on ordering via events or call counts, not timing); tryAcquire() fails while the lock is held and succeeds after release.
  • Unit: reentrancy — a nested install inside a held lease does not deadlock; a promise created inside the lease but awaited after it exits waits for the lock.
  • Unit: refresh() observes an entry written by another LockfileRepository instance.
  • Unit: the lock is not held during prepareInstall (fake the registry client and assert the lock is free while it is called) or during the conflict prompt.
  • Integration: an install and a remove racing on the same checkout end with a consistent tree and lockfile.

Docs

design/primitives/extensions.md: the lock, where it is held, and the lock order.

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 13 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/29/2026, 7:53:45 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
hammz assigned hammz9/29/2026, 6:23:47 PM

Sign in to post a ripple.