mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
## Why Exclusive coordination serialized writer acquisition across unrelated threads and could delay releasing writer ownership while waiting to remove a lock file. ## What changed Use shared coordination when opening thread writer locks. Attempt the initial stale-lock sweep only when exclusive coordination is immediately available, and keep rollout publication exclusively coordinated. When dropping a writer guard, remove its lock file only if exclusive coordination can be acquired immediately. Otherwise, release ownership and leave the file available for reuse or later cleanup. ## Testing Extend the writer-lock test to cover acquisition for different threads under shared coordination, rejection of competing owners for the same thread, and prompt ownership release and reacquisition when cleanup cannot acquire exclusive coordination. GitOrigin-RevId: 3e187b32dbef3bcfdd8c62e91eca58b615f3879e
245 lines
8.5 KiB
Rust
245 lines
8.5 KiB
Rust
//! Shared cross-process ownership for local thread writers and rollout publication.
|
|
//! Opening a thread lock file uses a bounded wait for home coordination. Cleanup
|
|
//! removes the file only if it can acquire exclusive coordination immediately.
|
|
|
|
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 file = self.open_coordination()?;
|
|
// Never queue a directory sweep behind another writer. Removal remains exclusive.
|
|
let have_exclusive = !self.cleanup_attempted.swap(true, Ordering::Relaxed)
|
|
&& match file.try_lock() {
|
|
Ok(()) => true,
|
|
Err(fs::TryLockError::WouldBlock) => false,
|
|
Err(fs::TryLockError::Error(err)) => return Err(err),
|
|
};
|
|
if have_exclusive {
|
|
if let Err(err) = self.remove_stale_thread_locks() {
|
|
warn!("failed to clean up stale thread writer locks: {err}");
|
|
}
|
|
} else {
|
|
Self::wait_for_coordination(|| file.try_lock_shared())?;
|
|
}
|
|
let _coordination_lock = file;
|
|
|
|
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 open_coordination(&self) -> io::Result<File> {
|
|
fs::create_dir_all(&self.directory)?;
|
|
OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(false)
|
|
.open(self.directory.join(COORDINATION_LOCK_FILE))
|
|
}
|
|
|
|
fn lock_coordination(&self) -> io::Result<File> {
|
|
let file = self.open_coordination()?;
|
|
Self::wait_for_coordination(|| file.try_lock())?;
|
|
Ok(file)
|
|
}
|
|
|
|
fn wait_for_coordination(
|
|
try_lock: impl Fn() -> Result<(), fs::TryLockError>,
|
|
) -> io::Result<()> {
|
|
let deadline = Instant::now() + Duration::from_secs(/*secs*/ 30);
|
|
loop {
|
|
match try_lock() {
|
|
Ok(()) => return Ok(()),
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
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.open_coordination() {
|
|
Ok(lock) => lock,
|
|
Err(err) => {
|
|
warn!("failed to coordinate thread writer lock cleanup: {err}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
match coordination_lock.try_lock() {
|
|
Ok(()) => {}
|
|
// Leave the file for the next writer. Dropping the guard still closes it.
|
|
Err(fs::TryLockError::WouldBlock) => return,
|
|
Err(fs::TryLockError::Error(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;
|