Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/exec-server-protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub const FS_COPY_METHOD: &str = "fs/copy";
pub const HTTP_REQUEST_METHOD: &str = "http/request";
/// JSON-RPC notification method for streamed executor HTTP response bodies.
pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
/// Maximum decoded response-body bytes carried by one streamed HTTP notification.
pub const MAX_HTTP_BODY_DELTA_BYTES: usize = 1024 * 1024;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
Expand Down
8 changes: 6 additions & 2 deletions codex-rs/exec-server/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use futures::future::BoxFuture;
use serde_json::Value;
use tokio::sync::Mutex;
use tokio::sync::OnceCell;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::sync::watch;
use tokio_util::task::AbortOnDropHandle;
Expand All @@ -24,6 +25,8 @@ use tracing::Instrument;
use tracing::debug;

use crate::ProcessId;
use crate::client::http_client::response_body_stream::MAX_QUEUED_HTTP_BODY_BYTES;
use crate::client::http_client::response_body_stream::QueuedHttpBodyDelta;
use crate::client_api::ExecServerClientConnectOptions;
use crate::client_api::ExecServerTransportParams;
use crate::client_api::HttpClient;
Expand Down Expand Up @@ -86,7 +89,6 @@ use crate::protocol::FsWalkResponse;
use crate::protocol::FsWriteFileParams;
use crate::protocol::FsWriteFileResponse;
use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD;
use crate::protocol::HttpRequestBodyDeltaNotification;
use crate::protocol::INITIALIZE_METHOD;
use crate::protocol::INITIALIZED_METHOD;
use crate::protocol::InitializeParams;
Expand Down Expand Up @@ -202,9 +204,10 @@ struct Inner {
// because they share the same connection-global notification channel as
// process output. Keep the routing table local to the client so higher
// layers can consume body chunks like a normal byte stream.
http_body_streams: ArcSwap<HashMap<String, mpsc::Sender<HttpRequestBodyDeltaNotification>>>,
http_body_streams: ArcSwap<HashMap<String, mpsc::Sender<QueuedHttpBodyDelta>>>,
http_body_stream_failures: ArcSwap<HashMap<String, String>>,
http_body_streams_write_lock: Mutex<()>,
http_body_stream_byte_budget: Arc<Semaphore>,
http_body_stream_next_id: AtomicU64,
session_id: OnceLock<String>,
reconnect_strategy: Option<ExecServerReconnectStrategy>,
Expand Down Expand Up @@ -851,6 +854,7 @@ impl ExecServerClient {
http_body_streams: ArcSwap::from_pointee(HashMap::new()),
http_body_stream_failures: ArcSwap::from_pointee(HashMap::new()),
http_body_streams_write_lock: Mutex::new(()),
http_body_stream_byte_budget: Arc::new(Semaphore::new(MAX_QUEUED_HTTP_BODY_BYTES)),
http_body_stream_next_id: AtomicU64::new(1),
session_id,
reconnect_strategy,
Expand Down
100 changes: 86 additions & 14 deletions codex-rs/exec-server/src/client/http_response_body_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use reqwest::Response;
use serde_json::Value;
use serde_json::from_value;
use tokio::runtime::Handle;
use tokio::sync::OwnedSemaphorePermit;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tracing::debug;
Expand All @@ -23,8 +24,29 @@ use crate::client::ExecServerError;
use crate::client::Inner;
use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD;
use crate::protocol::HttpRequestBodyDeltaNotification;
use crate::protocol::MAX_HTTP_BODY_DELTA_BYTES;
use crate::rpc::RpcNotificationSender;

pub(crate) const MAX_QUEUED_HTTP_BODY_BYTES: usize = 16 * 1024 * 1024;
const MAX_ENCODED_HTTP_BODY_DELTA_BYTES: usize = MAX_HTTP_BODY_DELTA_BYTES.div_ceil(3) * 4;

pub(crate) struct QueuedHttpBodyDelta {
notification: HttpRequestBodyDeltaNotification,
_byte_permit: Option<OwnedSemaphorePermit>,
}

impl QueuedHttpBodyDelta {
pub(crate) fn new(
notification: HttpRequestBodyDeltaNotification,
byte_permit: Option<OwnedSemaphorePermit>,
) -> Self {
Self {
notification,
_byte_permit: byte_permit,
}
}
}

pub(super) struct HttpBodyStreamRegistration {
inner: Arc<Inner>,
request_id: String,
Expand All @@ -39,7 +61,7 @@ enum HttpResponseBodyStreamInner {
inner: Arc<Inner>,
request_id: String,
next_seq: u64,
rx: mpsc::Receiver<HttpRequestBodyDeltaNotification>,
rx: mpsc::Receiver<QueuedHttpBodyDelta>,
pending_eof: bool,
closed: bool,
},
Expand All @@ -66,7 +88,7 @@ impl HttpResponseBodyStream {
pub(super) fn remote(
inner: Arc<Inner>,
request_id: String,
rx: mpsc::Receiver<HttpRequestBodyDeltaNotification>,
rx: mpsc::Receiver<QueuedHttpBodyDelta>,
) -> Self {
Self {
inner: HttpResponseBodyStreamInner::Remote {
Expand Down Expand Up @@ -107,7 +129,11 @@ impl HttpResponseBodyStream {
return Ok(None);
}

let Some(delta) = rx.recv().await else {
let Some(QueuedHttpBodyDelta {
notification: delta,
..
}) = rx.recv().await
else {
finish_remote_stream(inner, request_id, closed).await;
if let Some(error) = inner.take_http_body_stream_failure(request_id).await {
return Err(ExecServerError::Protocol(format!(
Expand Down Expand Up @@ -220,7 +246,22 @@ impl Inner {
&self,
params: Option<Value>,
) -> Result<(), ExecServerError> {
let params: HttpRequestBodyDeltaNotification = from_value(params.unwrap_or(Value::Null))?;
let params = params.unwrap_or(Value::Null);
if params
.get("deltaBase64")
.and_then(Value::as_str)
.is_some_and(|delta| delta.len() > MAX_ENCODED_HTTP_BODY_DELTA_BYTES)
{
return Err(ExecServerError::Protocol(format!(
"http response body delta exceeds {MAX_HTTP_BODY_DELTA_BYTES} bytes"
)));
}
let params: HttpRequestBodyDeltaNotification = from_value(params)?;
if params.delta.0.len() > MAX_HTTP_BODY_DELTA_BYTES {
return Err(ExecServerError::Protocol(format!(
"http response body delta exceeds {MAX_HTTP_BODY_DELTA_BYTES} bytes"
)));
}
// Unknown request ids are ignored intentionally: a stream may have already
// reached EOF and released its route.
if let Some(tx) = self
Expand All @@ -231,7 +272,33 @@ impl Inner {
{
let request_id = params.request_id.clone();
let terminal_delta = params.done || params.error.is_some();
match tx.try_send(params) {
let queued_bytes = params
.delta
.0
.len()
.saturating_add(params.error.as_deref().map_or(0, str::len));
let byte_permit = if queued_bytes == 0 {
None
} else {
u32::try_from(queued_bytes).ok().and_then(|queued_bytes| {
Arc::clone(&self.http_body_stream_byte_budget)
.try_acquire_many_owned(queued_bytes)
.ok()
})
};
if queued_bytes > 0 && byte_permit.is_none() {
self.record_http_body_stream_failure(
&request_id,
format!("queued body deltas exceed {MAX_QUEUED_HTTP_BODY_BYTES} bytes"),
)
.await;
self.remove_http_body_stream(&request_id).await;
debug!(
"closing http response stream `{request_id}` after exhausting the queued byte budget"
);
return Ok(());
}
match tx.try_send(QueuedHttpBodyDelta::new(params, byte_permit)) {
Ok(()) => {
if terminal_delta {
self.remove_http_body_stream(&request_id).await;
Expand Down Expand Up @@ -265,14 +332,19 @@ impl Inner {
let streams = streams.as_ref().clone();
self.http_body_streams.store(Arc::new(HashMap::new()));
for (request_id, tx) in streams {
// Failure notifications must wake every stream even when no
// byte-budget permits remain.
if tx
.try_send(HttpRequestBodyDeltaNotification {
request_id: request_id.clone(),
seq: 1,
delta: Vec::new().into(),
done: true,
error: Some(message.clone()),
})
.try_send(QueuedHttpBodyDelta::new(
HttpRequestBodyDeltaNotification {
request_id: request_id.clone(),
seq: 1,
delta: Vec::new().into(),
done: true,
error: Some(message.clone()),
},
/*byte_permit*/ None,
))
.is_err()
{
let mut next_failures = self.http_body_stream_failures.load().as_ref().clone();
Expand All @@ -295,7 +367,7 @@ impl Inner {
pub(super) async fn insert_http_body_stream(
&self,
request_id: String,
tx: mpsc::Sender<HttpRequestBodyDeltaNotification>,
tx: mpsc::Sender<QueuedHttpBodyDelta>,
) -> Result<(), ExecServerError> {
let _streams_write_guard = self.http_body_streams_write_lock.lock().await;
let streams = self.http_body_streams.load();
Expand All @@ -321,7 +393,7 @@ impl Inner {
pub(super) async fn remove_http_body_stream(
&self,
request_id: &str,
) -> Option<mpsc::Sender<HttpRequestBodyDeltaNotification>> {
) -> Option<mpsc::Sender<QueuedHttpBodyDelta>> {
let _streams_write_guard = self.http_body_streams_write_lock.lock().await;
let streams = self.http_body_streams.load();
let stream = streams.get(request_id).cloned();
Expand Down
31 changes: 17 additions & 14 deletions codex-rs/exec-server/src/client/reqwest_http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::protocol::HttpRedirectPolicy;
use crate::protocol::HttpRequestBodyDeltaNotification;
use crate::protocol::HttpRequestParams;
use crate::protocol::HttpRequestResponse;
use crate::protocol::MAX_HTTP_BODY_DELTA_BYTES;
use crate::rpc::RpcNotificationSender;
use crate::rpc::internal_error;
use crate::rpc::invalid_params;
Expand Down Expand Up @@ -222,21 +223,23 @@ impl ReqwestHttpRequestRunner {
while let Some(chunk) = body.next().await {
match chunk {
Ok(bytes) => {
if !send_body_delta(
&notifications,
HttpRequestBodyDeltaNotification {
request_id: request_id.clone(),
seq,
delta: bytes.to_vec().into(),
done: false,
error: None,
},
)
.await
{
return;
for chunk in bytes.chunks(MAX_HTTP_BODY_DELTA_BYTES) {
if !send_body_delta(
&notifications,
HttpRequestBodyDeltaNotification {
request_id: request_id.clone(),
seq,
delta: chunk.to_vec().into(),
done: false,
error: None,
},
)
.await
{
return;
}
seq += 1;
}
seq += 1;
}
Err(error) => {
let _ = send_body_delta(
Expand Down
Loading
Loading