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

  1. Wait and retry acquiring the guard until the other holder releases it (WouldBlock is transient)
  2. Ensure only one instance of the app/manager runs at a time
  3. On stale-lock suspicion, verify the holder process is gone (the OS lock dies with the process), then retry
  4. 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

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


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)