Files
codex/codex-rs/rollout/src/writer_lock.rs
T
jif 73a1148c9c Coordinate rollout compression with active thread writers (#44138)
## Why

Cold rollout files can still have active writers. Compression must not replace their files while writes are pending, including background work that outlives the local thread store.

## What changed

- Share cross-process writer locks between the rollout compressor and local thread store. Skip busy threads and recheck file state under coordination before publishing compressed files.
- Retain writer ownership through recorder background I/O and compressed rollout materialization. Wait for writers to exit on shutdown or discard, and stop queued file work before deletion.
- Route rollout metadata patches through owned recorders, reject competing writers before updating SQLite, and serialize metadata appends with shutdown.

## Testing

Add regression coverage for compression with live and detached writers, parallel cold-file compression, publication locking, metadata ownership conflicts, updates to compressed rollouts, concurrent shutdown, and discarding deferred items.

GitOrigin-RevId: c52f4037eb2019e8095b554e29bf531af95aa4bd
2026-09-09 12:30:07 +00:00

201 lines
6.7 KiB
Rust

//! Shared cross-process ownership for local thread writers and rollout publication.
//! Thread lock-file creation and removal uses the existing 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 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))?;
file.lock().map_err(|err| {
io::Error::other(format!(
"failed to acquire thread writer coordination lock: {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;