fix: readers lock read-only instead of skipping, one path per sync

Final Codex (gpt-6.1-sol, xhigh) pass over c65982f confirmed every
earlier item fixed and found:

- high: readers skipped the lock on any error, so a lock file owned by
  another user (who can still sync) let a reader run unlocked. Readers
  now open an existing lock file read-only for the shared lock, and read
  unlocked only when no lock file exists and none can be created.
- low: sync resolved the symlinked path for the install target and the
  lock separately; it now resolves once and passes that path to the
  lock callback, so both always name the same database.

Verified live: a reader with a read-only lock file waits for an
exclusive holder; install waits for readers; a symlinked path keeps its
link. CLI goldens unchanged; live A/B vs 0.2.4: 15 passed, screen hit a
transient MSN 408 on the old build only (retries pass), discover hits
the IDX 403 on both (exit 4 vs 1, intended).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 23:00:26 +00:00
commit db59744976
2 changed files with 59 additions and 29 deletions

View file

@ -144,14 +144,15 @@ pub fn parse_manifest(raw: &str) -> Result<OwnershipSnapshotManifest, IdxError>
Ok(manifest)
}
/// `lock(exclusive)` takes the database's lock: shared while inspecting the
/// local database, exclusive while rechecking and installing, and never held
/// during the downloads.
/// `lock(resolved_db_path, exclusive)` takes the database's lock: shared
/// while inspecting the local database, exclusive while rechecking and
/// installing, and never held during the downloads. The path is resolved
/// once, so the lock and the replaced file are always the same database.
pub fn sync_snapshot<L>(
manifest_source: &str,
db_path: &Path,
force: bool,
lock: impl Fn(bool) -> Result<L, IdxError>,
lock: impl Fn(&Path, bool) -> Result<L, IdxError>,
) -> Result<OwnershipSyncResult, IdxError> {
// Results report the configured path; files are read and replaced at the
// resolved one, so a symlinked path updates its target instead of being
@ -161,7 +162,7 @@ pub fn sync_snapshot<L>(
let db_path = resolved_db_path.as_path();
let manifest = fetch_manifest(manifest_source)?;
let local_state = {
let _shared = lock(false)?;
let _shared = lock(db_path, false)?;
inspect_local_db(db_path)?
};
let decision = build_sync_decision(local_state.as_ref(), &manifest.snapshot, force);
@ -215,7 +216,7 @@ pub fn sync_snapshot<L>(
let install_result = (|| -> Result<(), IdxError> {
validate_snapshot_db(&temp_path, &manifest.snapshot)?;
let _lock = lock(true)?;
let _lock = lock(db_path, true)?;
// The decision was made before the download; an import or edit that
// landed since must not be overwritten without --force.
if !force && !same_local_state(local_state.as_ref(), inspect_local_db(db_path)?.as_ref()) {