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
- Investigate which other process acquired the lock (check lock_dir contents/PIDs) and ensure single-instance semantics
- Re-acquire a fresh guard instead of releasing the stale one; treat this instance's session as invalidated
- Avoid external cleanup of lock_dir while a sync is in progress
- 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
- Keep guard lifetimes short so ownership can't change mid-run
- Never let external jobs delete lock_dir contents while sync is active
- Store owner PID/uuid in the lock file and verify before every release
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
- loopback port guard lock is already held
- bridge context is not initialized
- bridge context lock poisoned
- .codex-global-state.json changed while deleting thread
- .codex-global-state.json changed while restoring sidebar…
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)