mirror of
https://github.com/openai/codex.git
synced 2026-11-14 06:48:26 +00:00
Add durable per-thread user submission queues (#36952)
## What changed - Add a storage-neutral `QueueStore` interface backed by a dedicated SQLite database for ordered, thread-scoped user submissions. - Support enqueueing, paginated listing, editing, deletion, and atomic reordering, with a limit of 100 pending items per thread. - Remove queued submissions when their thread is deleted. ## Testing - Cover concurrent inserts, ordering and pagination, edits, deletion, cross-thread isolation, queue limits, and thread cleanup. GitOrigin-RevId: 0a8e95ac71137afc6f3a039d4b5fc8603110c891
This commit is contained in:
1 parent
449f099f1c
commit
b87981a510
14 files changed
+585
-2
No files matched your search
@@ -0,0 +1,134 @@
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_rollout::StateDbHandle;
|
||||
use codex_state::QueuedUserSubmissionRecord;
|
||||
use codex_state::SqliteQueueStore;
|
||||
|
||||
use crate::MAX_QUEUE_ITEMS;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreFuture;
|
||||
|
||||
/// Storage-neutral persistence for ordered, thread-scoped user messages.
|
||||
pub trait QueueStore: Send + Sync {
|
||||
fn enqueue(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
payload: String,
|
||||
) -> ThreadStoreFuture<'_, QueuedUserSubmissionRecord>;
|
||||
|
||||
fn list_page(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> ThreadStoreFuture<'_, Vec<QueuedUserSubmissionRecord>>;
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
item_id: String,
|
||||
payload: String,
|
||||
) -> ThreadStoreFuture<'_, Option<QueuedUserSubmissionRecord>>;
|
||||
|
||||
fn delete(&self, thread_id: ThreadId, item_id: String) -> ThreadStoreFuture<'_, bool>;
|
||||
|
||||
/// Atomically replace queue order with every current item ID exactly once.
|
||||
///
|
||||
/// Returns [`ThreadStoreError::InvalidRequest`] when `item_ids` is not a
|
||||
/// permutation of the complete queue.
|
||||
fn reorder(&self, thread_id: ThreadId, item_ids: Vec<String>) -> ThreadStoreFuture<'_, ()>;
|
||||
}
|
||||
|
||||
/// Adapts the local state runtime to the shared queue-storage interface.
|
||||
#[derive(Clone)]
|
||||
pub struct LocalQueueStore {
|
||||
state_db: StateDbHandle,
|
||||
}
|
||||
|
||||
impl LocalQueueStore {
|
||||
pub fn new(state_db: StateDbHandle) -> Self {
|
||||
Self { state_db }
|
||||
}
|
||||
|
||||
fn queue(&self) -> &SqliteQueueStore {
|
||||
self.state_db.thread_queue()
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_future<'a, T, E>(
|
||||
future: impl Future<Output = Result<T, E>> + Send + 'a,
|
||||
) -> ThreadStoreFuture<'a, T>
|
||||
where
|
||||
T: Send + 'a,
|
||||
E: Display + Send + 'a,
|
||||
{
|
||||
Box::pin(async move {
|
||||
future.await.map_err(|error| ThreadStoreError::Internal {
|
||||
message: format!("queue storage failed: {error}"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl QueueStore for LocalQueueStore {
|
||||
fn enqueue(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
payload: String,
|
||||
) -> ThreadStoreFuture<'_, QueuedUserSubmissionRecord> {
|
||||
Box::pin(async move {
|
||||
self.queue()
|
||||
.enqueue(thread_id, &payload)
|
||||
.await
|
||||
.map_err(|error| match error.downcast_ref::<sqlx::Error>() {
|
||||
Some(sqlx::Error::RowNotFound) => ThreadStoreError::InvalidRequest {
|
||||
message: format!("queue cannot contain more than {MAX_QUEUE_ITEMS} items"),
|
||||
},
|
||||
_ => ThreadStoreError::Internal {
|
||||
message: format!("queue storage failed: {error}"),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn list_page(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> ThreadStoreFuture<'_, Vec<QueuedUserSubmissionRecord>> {
|
||||
queue_future(self.queue().list_page(thread_id, offset, limit))
|
||||
}
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
item_id: String,
|
||||
payload: String,
|
||||
) -> ThreadStoreFuture<'_, Option<QueuedUserSubmissionRecord>> {
|
||||
queue_future(async move { self.queue().update(thread_id, &item_id, &payload).await })
|
||||
}
|
||||
|
||||
fn delete(&self, thread_id: ThreadId, item_id: String) -> ThreadStoreFuture<'_, bool> {
|
||||
queue_future(async move { self.queue().delete(thread_id, &item_id).await })
|
||||
}
|
||||
|
||||
fn reorder(&self, thread_id: ThreadId, item_ids: Vec<String>) -> ThreadStoreFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.queue()
|
||||
.reorder(thread_id, &item_ids)
|
||||
.await
|
||||
.map_err(|error| match error.downcast_ref::<std::io::Error>() {
|
||||
Some(error) if error.kind() == std::io::ErrorKind::InvalidInput => {
|
||||
ThreadStoreError::InvalidRequest {
|
||||
message: error.to_string(),
|
||||
}
|
||||
}
|
||||
_ => ThreadStoreError::Internal {
|
||||
message: format!("queue storage failed: {error}"),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user