mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
## Why Contention on the writer coordination lock can block writer cleanup indefinitely, preventing idle threads from unloading. ## What changed Retry coordination lock acquisition every 50 ms and return `io::ErrorKind::TimedOut` after 30 seconds of contention. This lets cleanup release writer ownership and idle unloading finish even while another process holds coordination. ## Testing Extend writer lock coverage to check the timeout error. Update the idle unload regression test to verify that the thread closes while coordination remains locked and can resume with its persisted turn after the lock is released. GitOrigin-RevId: 513117fdaf3890283e7acdd149ce9abb66075b63
214 lines
7.4 KiB
Rust
214 lines
7.4 KiB
Rust
//! Shared cross-process ownership for local thread writers and rollout publication.
|
|
//! Thread lock-file creation and removal uses a bounded wait for the home coordination lock.
|
|
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::fs::OpenOptions;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use codex_protocol::ThreadId;
|
|
use tracing::warn;
|
|
|
|
const WRITER_LOCK_DIR: &str = "thread-writer-locks";
|
|
const COORDINATION_LOCK_FILE: &str = ".coordination.lock";
|
|
|
|
/// Coordinates writer ownership within one Codex home.
|
|
pub struct WriterLockCoordinator {
|
|
directory: PathBuf,
|
|
cleanup_attempted: AtomicBool,
|
|
}
|
|
|
|
/// Keeps a thread owned until all file work has finished.
|
|
pub struct WriterLockGuard {
|
|
coordinator: Arc<WriterLockCoordinator>,
|
|
path: PathBuf,
|
|
file: Option<File>,
|
|
}
|
|
|
|
impl WriterLockCoordinator {
|
|
/// Uses the same lock namespace as existing local thread-store writers.
|
|
pub fn new(codex_home: &Path) -> Self {
|
|
Self {
|
|
directory: codex_home.join(WRITER_LOCK_DIR),
|
|
cleanup_attempted: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
/// Acquires exclusive writer ownership, returning `WouldBlock` for an active writer.
|
|
pub fn acquire(self: &Arc<Self>, thread_id: ThreadId) -> io::Result<WriterLockGuard> {
|
|
let _coordination_lock = self.lock_coordination()?;
|
|
if !self.cleanup_attempted.swap(true, Ordering::Relaxed)
|
|
&& let Err(err) = self.remove_stale_thread_locks()
|
|
{
|
|
warn!("failed to clean up stale thread writer locks: {err}");
|
|
}
|
|
|
|
let path = self.directory.join(format!("{thread_id}.lock"));
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(false)
|
|
.open(&path)
|
|
.map_err(|err| {
|
|
io::Error::other(format!(
|
|
"failed to open thread writer lock {}: {err}",
|
|
path.display()
|
|
))
|
|
})?;
|
|
|
|
match file.try_lock() {
|
|
Ok(()) => {}
|
|
Err(std::fs::TryLockError::WouldBlock) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::WouldBlock,
|
|
format!("thread {thread_id} already has an active writer"),
|
|
));
|
|
}
|
|
Err(std::fs::TryLockError::Error(err)) => {
|
|
return Err(io::Error::other(format!(
|
|
"failed to acquire thread writer lock {}: {err}",
|
|
path.display()
|
|
)));
|
|
}
|
|
}
|
|
|
|
Ok(WriterLockGuard {
|
|
coordinator: Arc::clone(self),
|
|
path,
|
|
file: Some(file),
|
|
})
|
|
}
|
|
|
|
/// Holds coordination through publication after probing that the thread is idle.
|
|
/// Every writer takes coordination before opening its thread lock, so the probe
|
|
/// itself can be released. Encoding and verification must finish before this call.
|
|
pub(crate) fn try_acquire_for_publication(
|
|
&self,
|
|
thread_id: ThreadId,
|
|
) -> io::Result<Option<File>> {
|
|
let coordination_lock = self.lock_coordination()?;
|
|
let path = self.directory.join(format!("{thread_id}.lock"));
|
|
match OpenOptions::new().read(true).write(true).open(path) {
|
|
Ok(file) => match file.try_lock() {
|
|
Ok(()) => Ok(Some(coordination_lock)),
|
|
Err(std::fs::TryLockError::WouldBlock) => Ok(None),
|
|
Err(std::fs::TryLockError::Error(err)) => Err(err),
|
|
},
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Some(coordination_lock)),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn lock_coordination(&self) -> io::Result<File> {
|
|
fs::create_dir_all(&self.directory)?;
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(false)
|
|
.open(self.directory.join(COORDINATION_LOCK_FILE))?;
|
|
let deadline = Instant::now() + Duration::from_secs(/*secs*/ 30);
|
|
loop {
|
|
match file.try_lock() {
|
|
Ok(()) => break,
|
|
Err(fs::TryLockError::WouldBlock) if Instant::now() < deadline => {
|
|
std::thread::sleep(Duration::from_millis(/*millis*/ 50));
|
|
}
|
|
Err(fs::TryLockError::WouldBlock) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::TimedOut,
|
|
"timed out waiting for the thread writer coordination lock; another Codex process may be suspended on this or another host",
|
|
));
|
|
}
|
|
Err(fs::TryLockError::Error(err)) => return Err(err),
|
|
}
|
|
}
|
|
Ok(file)
|
|
}
|
|
|
|
fn remove_stale_thread_locks(&self) -> io::Result<()> {
|
|
for entry in fs::read_dir(&self.directory)? {
|
|
let entry = entry?;
|
|
let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
continue;
|
|
};
|
|
let Some(thread_id) = file_name.strip_suffix(".lock") else {
|
|
continue;
|
|
};
|
|
if ThreadId::from_string(thread_id).is_err() {
|
|
continue;
|
|
}
|
|
|
|
let path = entry.path();
|
|
let file = match OpenOptions::new().read(true).write(true).open(&path) {
|
|
Ok(file) => file,
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
|
Err(err) => {
|
|
warn!(
|
|
"failed to inspect thread writer lock {}: {err}",
|
|
path.display()
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
match file.try_lock() {
|
|
Ok(()) => {
|
|
drop(file);
|
|
if let Err(err) = fs::remove_file(&path)
|
|
&& err.kind() != io::ErrorKind::NotFound
|
|
{
|
|
warn!(
|
|
"failed to remove stale thread writer lock {}: {err}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
Err(std::fs::TryLockError::WouldBlock) => {}
|
|
Err(std::fs::TryLockError::Error(err)) => {
|
|
warn!(
|
|
"failed to inspect thread writer lock {}: {err}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for WriterLockGuard {
|
|
fn drop(&mut self) {
|
|
let coordination_lock = match self.coordinator.lock_coordination() {
|
|
Ok(lock) => lock,
|
|
Err(err) => {
|
|
warn!("failed to coordinate thread writer lock cleanup: {err}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Close the writer lock before deleting it so cleanup works on Windows too.
|
|
drop(self.file.take());
|
|
if let Err(err) = fs::remove_file(&self.path)
|
|
&& err.kind() != io::ErrorKind::NotFound
|
|
{
|
|
warn!(
|
|
"failed to remove thread writer lock {}: {err}",
|
|
self.path.display()
|
|
);
|
|
}
|
|
drop(coordination_lock);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "writer_lock_tests.rs"]
|
|
mod tests;
|