BigPizzaV3/CodexPlusPlus · error · std::io::Error (Other)

provider-sync lock ownership changed before release

Error message

provider-sync lock ownership changed before release

What it means

ProviderSyncLifecycleGuard::release first checks OS-level lock ownership before releasing the directory lock. If release_owned_lock reports the current process no longer owns the lock, an ABA conflict is detected (another process took over between acquire and release); release refuses to proceed so it cannot delete a lock that a successor process now owns.

Solutions

  1. Investigate which other process acquired the lock (check lock_dir contents/PIDs) and ensure single-instance semantics
  2. Re-acquire a fresh guard instead of releasing the stale one; treat this instance's session as invalidated
  3. Avoid external cleanup of lock_dir while a sync is in progress
  4. If using a shared/network filesystem, move locks to a local disk to get reliable ownership semantics

Example fix

// before
match guard.release() { Ok(_) => spawn_successor(), Err(_) => {} }
// after
match guard.release() {
    Ok(_) => spawn_successor(),
    Err(e) if e.to_string().contains("ownership changed") => {
        // ABA: do not spawn successor blindly; re-sync state first
        reconcile_with_current_owner(&lock_dir)?;
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before releasing, confirm we still hold the lock
const stillOurs = readLockOwner(lockDir) === myPid;
if (!stillOurs) console.warn('lock ownership changed; do not release blindly');

Type guard

const isOwnershipConflict = (e: Error): boolean => e.message.includes('ownership changed');

Try / catch

match guard.release() {
    Ok(()) => spawn_successor(),
    Err(e) if e.to_string().contains("ownership changed") => {
        // ABA conflict: don't delete; re-acquire fresh lock and reconcile
        reconcile_and_reacquire(&lock_dir)?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling release() on a ProviderSyncLifecycleGuard after the OS lock ownership changed — typically another process stole or re-created the lock in lock_dir, or the lock file was deleted and re-created between acquire and release.

Common situations: Two app instances racing on provider sync; an external cleanup job deleted the lock file mid-run; long-held guards spanning a restart where a successor force-removed the lock; filesystem shared over NFS with inconsistent lock semantics.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/3b844912d4575e9c. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-data/src/provider_sync.rs:72

    /// 锁存在、owner 信息不可读,但仍在宽限期内——无法判断是否有人正在建锁。
    Indeterminate,
}

#[derive(Debug)]
pub struct ProviderSyncLifecycleGuard {
    lock_dir: PathBuf,
    lock_file: File,
    lock_id: String,
    directory_released: bool,
    file_unlocked: bool,
}

impl ProviderSyncLifecycleGuard {
    /// Releases both compatibility and OS ownership before a caller starts a successor process.
    /// A mismatched owner is an ABA conflict and must block the successor instead of deleting it.
    pub fn release(mut self) -> std::io::Result<()> {
        if !release_owned_lock(&self.lock_dir, &self.lock_id)? {
            return Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "provider-sync lock ownership changed before release",
            ));
        }
        self.directory_released = true;
        FileExt::unlock(&self.lock_file)?;
        self.file_unlocked = true;
        Ok(())
    }
}

impl Drop for ProviderSyncLifecycleGuard {
    fn drop(&mut self) {
        if !self.directory_released {
            let _ = release_owned_lock(&self.lock_dir, &self.lock_id);
        }
        if !self.file_unlocked {
            let _ = FileExt::unlock(&self.lock_file);

View on GitHub (pinned to b1ed92e5e4)