BigPizzaV3/CodexPlusPlus · warning · std::io::Error (WouldBlock)
loopback port guard lock is already held
Error message
loopback port guard lock is already held
What it means
The loopback port guard serializes access with an exclusive file lock (try_lock_exclusive). When the underlying OS error is errno 33 (EWOULDBLOCK/EAGAIN — lock held by another process), normalize_lock_error converts it into io::ErrorKind::WouldBlock with this message so callers can recognize contention instead of a generic failure.
Solutions
- Wait and retry acquiring the guard until the other holder releases it (WouldBlock is transient)
- Ensure only one instance of the app/manager runs at a time
- On stale-lock suspicion, verify the holder process is gone (the OS lock dies with the process), then retry
- Wrap acquisition in a retry-with-backoff loop matching on ErrorKind::WouldBlock
Example fix
// before
let guard = acquire_port_guard()?; // fails when held
// after
let guard = loop {
match acquire_port_guard() {
Ok(g) => break g,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(200));
}
Err(e) => return Err(e),
}
}; Defensive patterns
Strategy: retry
Validate before calling
// probe the guard before doing work let probe = std::fs::OpenOptions::new().write(true).open(&lock_path)?; let free = probe.try_lock_exclusive().is_ok();
Type guard
const isLockContention = (e: NodeJS.ErrnoException): boolean => e.code === 'EWOULDBLOCK' || e.code === 'EAGAIN';
Try / catch
match acquire_port_guard() {
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => retry_with_backoff(10, Duration::from_millis(200)),
other => other,
} Prevention
- Enforce single-instance behavior in the app (e.g. a startup instance lock)
- Always acquire the guard with a bounded retry loop instead of failing fast
- Never hold the port guard across long-running operations
When it happens
Trigger: Two processes (or two app instances) concurrently attempt to acquire the loopback port guard lock file; the second caller's try_lock_exclusive fails with errno 33 and is normalized to this error.
Common situations: Launching a second manager instance while one is running; a crashed process left the lock file but the OS released the lock (less common); CI/test parallelism running two apply flows against the same port-guard file; NFS mounts where lock semantics misbehave.
Related errors
- provider-sync lock ownership changed before release
- bridge context lock poisoned
- .codex-global-state.json changed while deleting thread
- .codex-global-state.json changed while restoring sidebar…
- Concurrent adapter upgrade
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/215473903b7bfcaf.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/ports.rs:249
}
fn acquire_lock_guard(port: u16, state_dir: &Path) -> std::io::Result<(File, PathBuf)> {
let dir = state_dir.join("locks");
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!("loopback-port-{port}.lock"));
let file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)?;
file.try_lock_exclusive().map_err(normalize_lock_error)?;
Ok((file, path))
}
fn normalize_lock_error(error: std::io::Error) -> std::io::Error {
match error.raw_os_error() {
Some(33) => std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"loopback port guard lock is already held",
),
_ => error,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
static GUARD_PORT_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn resilient_guard_holds_lock_and_listener_when_requested_port_is_available() {
let temp = tempfile::tempdir().unwrap();
let port = find_available_loopback_port();View on GitHub (pinned to b1ed92e5e4)