mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Enforce writer ownership for thread archive and deletion (#35031)
## Why Paginated threads allow only one app-server process to write at a time. Archive and delete operations must not mutate a thread or its spawned descendants while another process owns any of them. ## What changed - Acquire paginated writer locks for the full archive or deletion set before changing rollout files, including descendants whose rollout has not materialized yet. - Add a batch archive store operation so ownership conflicts are detected before any thread in the subtree is archived. - Return ownership conflicts as JSON-RPC invalid-request errors and document the behavior. ## Testing - Cover archive and deletion conflicts for owned threads and descendants, including unmaterialized rollouts. - Verify deletion still removes rollouts with unreadable metadata. GitOrigin-RevId: f62a3e0c1ee2631cea48dd9145b588a1486e2425
This commit is contained in:
1 parent
9fc4e5a7aa
commit
963316583b
12 files changed
+437
-49
No files matched your search
@@ -34,6 +34,7 @@ use tokio::sync::OwnedMutexGuard;
|
||||
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
@@ -91,7 +92,7 @@ struct LiveRecorderEntry {
|
||||
// canonical SessionMeta is durable. Retain the mode captured when live persistence was opened
|
||||
// so missing SQLite rows can still be seeded.
|
||||
history_mode: ThreadHistoryMode,
|
||||
_writer_lock: Option<WriterLockGuard>,
|
||||
writer_lock: Option<WriterLockGuard>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -207,6 +208,42 @@ impl LocalThreadStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn acquire_paginated_writer_locks(
|
||||
&self,
|
||||
thread_ids: &[ThreadId],
|
||||
) -> ThreadStoreResult<Vec<WriterLockGuard>> {
|
||||
let mut writer_locks = Vec::new();
|
||||
for &thread_id in thread_ids {
|
||||
if self
|
||||
.live_recorders
|
||||
.lock()
|
||||
.await
|
||||
.get(&thread_id)
|
||||
.is_some_and(|entry| entry.writer_lock.is_some())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only a readable legacy header proves no paginated writer can own this id. Missing
|
||||
// lazy rollouts and damaged headers must conservatively try the lock.
|
||||
let history_mode = match read_thread::resolve_rollout_path(
|
||||
self, thread_id, /*include_archived*/ true,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(rollout_path) => codex_rollout::read_session_meta_line(rollout_path.as_path())
|
||||
.await
|
||||
.ok()
|
||||
.map(|meta_line| meta_line.meta.history_mode),
|
||||
None => None,
|
||||
};
|
||||
if !matches!(history_mode, Some(ThreadHistoryMode::Legacy)) {
|
||||
writer_locks.push(self.writer_lock_coordinator.acquire(thread_id)?);
|
||||
}
|
||||
}
|
||||
Ok(writer_locks)
|
||||
}
|
||||
|
||||
async fn insert_live_recorder(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
@@ -222,7 +259,7 @@ impl LocalThreadStore {
|
||||
entry.insert(LiveRecorderEntry {
|
||||
recorder,
|
||||
history_mode,
|
||||
_writer_lock: writer_lock,
|
||||
writer_lock,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -402,7 +439,24 @@ impl ThreadStore for LocalThreadStore {
|
||||
}
|
||||
|
||||
fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()> {
|
||||
Box::pin(async move { archive_thread::archive_thread(self, params).await })
|
||||
Box::pin(async move {
|
||||
archive_thread::archive_threads(
|
||||
self,
|
||||
ArchiveThreadsParams {
|
||||
thread_ids: vec![params.thread_id],
|
||||
writer_lock_thread_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
fn archive_threads(
|
||||
&self,
|
||||
params: ArchiveThreadsParams,
|
||||
) -> ThreadStoreFuture<'_, Vec<ThreadId>> {
|
||||
Box::pin(async move { archive_thread::archive_threads(self, params).await })
|
||||
}
|
||||
|
||||
fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread> {
|
||||
|
||||
Reference in new issue
Block a user