| use std::collections::hash_map::DefaultHasher; |
| use std::hash::Hash; |
| use std::hash::Hasher; |
| use std::sync::Arc; |
| use std::sync::atomic::Ordering; |
| use std::time::Duration; |
|
|
| use codex_network_proxy::NetworkDecision; |
| use codex_network_proxy::NetworkPolicyDecision; |
| use codex_network_proxy::NetworkPolicyRequest; |
| use codex_network_proxy::NetworkProtocol; |
| use codex_network_proxy::NetworkRequestCancellation; |
| use codex_network_proxy::NetworkRequestCancellationReason; |
| use serde_json::Value; |
| use tokio::sync::mpsc; |
| use tokio::time::Instant; |
| use tokio::time::sleep; |
| use tokio::time::timeout; |
| use tokio::time::timeout_at; |
| use tokio_util::sync::CancellationToken; |
| use tracing::Instrument; |
| use tracing::debug; |
|
|
| use super::ConnectionStatus; |
| use super::ExecServerClient; |
| use super::ExecServerError; |
| use super::Inner; |
| use super::OrderedSessionEvents; |
| use super::RecoveryPolicy; |
| use super::SessionState; |
| use super::disconnected_message; |
| use super::fail_all_in_flight_work; |
| use super::handle_server_notification; |
| use super::is_transport_closed_error; |
| use crate::client_transport::ExecServerReconnectStrategy; |
| use crate::process::ExecProcessEvent; |
| use crate::protocol::EXEC_READ_METHOD; |
| use crate::protocol::EXEC_TERMINATE_METHOD; |
| use crate::protocol::ExecServerNetworkPolicyDecision; |
| use crate::protocol::ExecServerNetworkProtocol; |
| use crate::protocol::MAX_NETWORK_POLICY_HOST_BYTES; |
| use crate::protocol::MAX_NETWORK_POLICY_PROCESS_ID_BYTES; |
| use crate::protocol::MAX_NETWORK_POLICY_REASON_BYTES; |
| use crate::protocol::NETWORK_POLICY_REQUEST_METHOD; |
| use crate::protocol::NetworkPolicyRequestParams; |
| use crate::protocol::NetworkPolicyRequestResponse; |
| use crate::protocol::ReadParams; |
| use crate::protocol::ReadResponse; |
| use crate::protocol::TerminateParams; |
| use crate::protocol::TerminateResponse; |
| use crate::rpc::RpcClient; |
| use crate::rpc::RpcClientEvent; |
| use crate::rpc::RpcInboundRequestAdmissionError; |
| use crate::rpc::SESSION_ALREADY_ATTACHED_ERROR_CODE; |
| use crate::rpc::invalid_params; |
| use crate::rpc::method_not_found; |
|
|
| #[cfg(test)] |
| const SESSION_RECOVERY_TIMEOUT: Duration = Duration::from_millis(500); |
| #[cfg(not(test))] |
| |
| |
| const SESSION_RECOVERY_TIMEOUT: Duration = Duration::from_secs(25); |
| const SESSION_RECOVERY_RETRY_INTERVAL: Duration = Duration::from_millis(100); |
| const REGISTRY_RECOVERY_INITIAL_RETRY_INTERVAL: Duration = Duration::from_millis(500); |
| const REGISTRY_RECOVERY_MAX_RETRY_INTERVAL: Duration = Duration::from_secs(5); |
| const NETWORK_POLICY_DENIAL_REASON: &str = "not_allowed"; |
|
|
| struct ClientRequestOutcome { |
| span: tracing::Span, |
| result: &'static str, |
| } |
|
|
| impl ClientRequestOutcome { |
| fn complete(&mut self, result: &'static str) { |
| self.result = result; |
| } |
| } |
|
|
| impl Drop for ClientRequestOutcome { |
| fn drop(&mut self) { |
| self.span.record("result", self.result); |
| } |
| } |
|
|
| impl SessionState { |
| fn last_published_seq(&self) -> u64 { |
| self.ordered_events |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner) |
| .last_published_seq |
| } |
|
|
| fn recover_events(&self, response: ReadResponse) -> Result<bool, ExecServerError> { |
| let ReadResponse { |
| chunks, |
| next_seq, |
| exited, |
| exit_code, |
| closed, |
| failure, |
| sandbox_denied, |
| } = response; |
| if let Some(message) = failure { |
| return Err(ExecServerError::Protocol(format!( |
| "process failed while recovering: {message}" |
| ))); |
| } |
|
|
| let target_seq = next_seq.saturating_sub(1); |
| let published_closed = { |
| let mut ordered_events = self |
| .ordered_events |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| if ordered_events.failure.is_some() |
| || ordered_events.closed_published |
| || target_seq <= ordered_events.last_published_seq |
| { |
| return Ok(false); |
| } |
| let pending_exit = ordered_events.pending.range_mut(..=target_seq).find_map( |
| |(_, event)| match event { |
| ExecProcessEvent::Exited { |
| sandbox_denied: pending_sandbox_denied, |
| .. |
| } => Some(pending_sandbox_denied), |
| _ => None, |
| }, |
| ); |
| let exit_pending = pending_exit.is_some(); |
| if let Some(pending_sandbox_denied) = pending_exit { |
| *pending_sandbox_denied = |
| Some(pending_sandbox_denied.unwrap_or(false) || sandbox_denied); |
| } |
| let mut exit_known = ordered_events.exit_published || exit_pending; |
| if closed |
| && (matches!( |
| ordered_events.pending.get(&target_seq), |
| Some(event) if !matches!(event, ExecProcessEvent::Closed { .. }) |
| ) || chunks.iter().any(|chunk| chunk.seq == target_seq)) |
| { |
| return Err(ExecServerError::Protocol(format!( |
| "process close sequence {target_seq} conflicts with recovered output" |
| ))); |
| } |
| let mut published_closed = false; |
| for chunk in chunks { |
| if chunk.seq > target_seq { |
| return Err(ExecServerError::Protocol(format!( |
| "recovered process output sequence {} exceeds target sequence {target_seq}", |
| chunk.seq |
| ))); |
| } |
| let next_seq = ordered_events.last_published_seq.saturating_add(1); |
| if exited && !exit_known && chunk.seq > next_seq { |
| let exit_code = exit_code.ok_or_else(|| { |
| ExecServerError::Protocol( |
| "recovering exited process did not include its exit code".to_string(), |
| ) |
| })?; |
| ordered_events |
| .insert_pending(ExecProcessEvent::Exited { |
| seq: next_seq, |
| exit_code, |
| sandbox_denied: Some(sandbox_denied), |
| }) |
| .map_err(ExecServerError::Protocol)?; |
| published_closed |= self.publish_ready(&mut ordered_events); |
| exit_known = true; |
| } |
| if chunk.seq > ordered_events.last_published_seq { |
| ordered_events |
| .insert_pending(ExecProcessEvent::Output(chunk)) |
| .map_err(ExecServerError::Protocol)?; |
| published_closed |= self.publish_ready(&mut ordered_events); |
| } |
| } |
| if closed |
| && !ordered_events.closed_published |
| && !matches!( |
| ordered_events.pending.get(&target_seq), |
| Some(ExecProcessEvent::Closed { .. }) |
| ) |
| { |
| ordered_events |
| .insert_pending(ExecProcessEvent::Closed { seq: target_seq }) |
| .map_err(ExecServerError::Protocol)?; |
| } |
|
|
| let event_count = target_seq.saturating_sub(ordered_events.last_published_seq); |
| let first_unpublished_seq = ordered_events.last_published_seq.saturating_add(1); |
| let retained_count = if first_unpublished_seq <= target_seq { |
| ordered_events |
| .pending |
| .range(first_unpublished_seq..=target_seq) |
| .count() as u64 |
| } else { |
| 0 |
| }; |
| let missing_count = event_count.saturating_sub(retained_count); |
| if exited && !exit_known { |
| if missing_count != 1 { |
| return Err(recovery_gap_error(target_seq)); |
| } |
| let seq = first_missing_seq(&ordered_events, target_seq); |
| let exit_code = exit_code.ok_or_else(|| { |
| ExecServerError::Protocol( |
| "recovering exited process did not include its exit code".to_string(), |
| ) |
| })?; |
| ordered_events |
| .insert_pending(ExecProcessEvent::Exited { |
| seq, |
| exit_code, |
| sandbox_denied: Some(sandbox_denied), |
| }) |
| .map_err(ExecServerError::Protocol)?; |
| } else if missing_count != 0 { |
| return Err(recovery_gap_error(target_seq)); |
| } |
| published_closed |= self.publish_ready(&mut ordered_events); |
| published_closed |
| }; |
|
|
| self.note_change(target_seq); |
| Ok(published_closed) |
| } |
| } |
|
|
| fn first_missing_seq(events: &OrderedSessionEvents, target_seq: u64) -> u64 { |
| let mut expected = events.last_published_seq.saturating_add(1); |
| for seq in events |
| .pending |
| .range(expected..=target_seq) |
| .map(|(seq, _)| *seq) |
| { |
| if seq != expected { |
| break; |
| } |
| expected = expected.saturating_add(1); |
| } |
| expected |
| } |
|
|
| fn recovery_gap_error(target_seq: u64) -> ExecServerError { |
| ExecServerError::Protocol(format!( |
| "process events are no longer retained while recovering through sequence {target_seq}" |
| )) |
| } |
|
|
| impl Inner { |
| pub(super) async fn rpc_client(self: &Arc<Self>) -> Result<Arc<RpcClient>, ExecServerError> { |
| let mut connection_changed = self.connection_changed.subscribe(); |
| loop { |
| if let Some(message) = self.failure_message() { |
| return Err(ExecServerError::Disconnected(message)); |
| } |
|
|
| let rpc_client = { |
| let connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| match &connection.status { |
| ConnectionStatus::Connected(rpc_client) => Some(Arc::clone(rpc_client)), |
| ConnectionStatus::Recovering | ConnectionStatus::Failed(_) => None, |
| } |
| }; |
| let Some(rpc_client) = rpc_client else { |
| let _ = connection_changed.changed().await; |
| continue; |
| }; |
| if !rpc_client.is_disconnected() { |
| return Ok(rpc_client); |
| } |
|
|
| let _ = connection_changed.changed().await; |
| } |
| } |
|
|
| pub(super) fn begin_process_start(&self, expected: &Arc<RpcClient>) -> bool { |
| let mut connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| let ConnectionStatus::Connected(current) = &connection.status else { |
| return false; |
| }; |
| if !Arc::ptr_eq(current, expected) || expected.is_disconnected() { |
| return false; |
| } |
| connection.active_process_starts += 1; |
| true |
| } |
|
|
| pub(super) fn finish_process_start(&self) { |
| { |
| let mut connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| if connection.active_process_starts == 0 { |
| tracing::error!("finished an exec-server process start that was not active"); |
| return; |
| } |
| connection.active_process_starts -= 1; |
| } |
| self.notify_connection_changed(); |
| } |
|
|
| pub(super) fn is_failed(&self) -> bool { |
| self.failure_message().is_some() |
| } |
|
|
| pub(super) fn failure_message(&self) -> Option<String> { |
| let connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| match &connection.status { |
| ConnectionStatus::Failed(message) => Some(message.clone()), |
| ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => None, |
| } |
| } |
|
|
| pub(super) fn request_recovery( |
| self: &Arc<Self>, |
| failed_rpc_client: Arc<RpcClient>, |
| disconnect_message: String, |
| ) { |
| let should_recover = { |
| let mut connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| match &connection.status { |
| ConnectionStatus::Connected(current) |
| if Arc::ptr_eq(current, &failed_rpc_client) => |
| { |
| connection.set_status(ConnectionStatus::Recovering); |
| true |
| } |
| ConnectionStatus::Connected(_) |
| | ConnectionStatus::Recovering |
| | ConnectionStatus::Failed(_) => false, |
| } |
| }; |
| if !should_recover { |
| return; |
| } |
|
|
| self.notify_connection_changed(); |
| let inner = Arc::clone(self); |
| tokio::spawn(async move { |
| tokio::select! { |
| biased; |
| _ = inner.retired.cancelled() => {}, |
| _ = inner.recover(disconnect_message) => {}, |
| } |
| }); |
| } |
|
|
| async fn recover(self: &Arc<Self>, disconnect_message: String) { |
| let deadline = Instant::now() + SESSION_RECOVERY_TIMEOUT; |
| self.fail_all_http_body_streams(disconnect_message.clone()) |
| .await; |
| if timeout_at(deadline, self.wait_for_process_starts()) |
| .await |
| .is_err() |
| { |
| let message = format!( |
| "{disconnect_message}; failed to resume exec-server session: recovery timed out after {SESSION_RECOVERY_TIMEOUT:?}" |
| ); |
| self.fail(message).await; |
| return; |
| } |
| if self.reconnect_strategy.is_none() { |
| self.fail(disconnect_message).await; |
| return; |
| } |
|
|
| let Some(session_id) = self.session_id.get().cloned() else { |
| let message = format!( |
| "{disconnect_message}; failed to resume exec-server session: missing session id" |
| ); |
| self.fail(message).await; |
| return; |
| }; |
| let uses_registry_backoff = matches!( |
| self.reconnect_strategy.as_ref(), |
| Some(ExecServerReconnectStrategy::NoiseRendezvous { .. }) |
| ); |
| let mut registry_retry_attempt = 0; |
| let last_error = loop { |
| match timeout_at(deadline, self.resume_once(&session_id)).await { |
| Ok(Ok((rpc_client, _attempt))) => { |
| if !rpc_client.is_disconnected() && self.install_recovered_client(rpc_client) { |
| return; |
| } |
| } |
| Ok(Err(error)) if !is_retryable_recovery_error(&error) => { |
| break error.to_string(); |
| } |
| Ok(Err(_)) => {} |
| Err(_) => { |
| break format!("recovery timed out after {SESSION_RECOVERY_TIMEOUT:?}"); |
| } |
| } |
|
|
| let retry_delay = if uses_registry_backoff { |
| let delay = registry_recovery_retry_delay(&session_id, registry_retry_attempt); |
| registry_retry_attempt = registry_retry_attempt.saturating_add(1); |
| delay |
| } else { |
| SESSION_RECOVERY_RETRY_INTERVAL |
| }; |
|
|
| let now = Instant::now(); |
| if now >= deadline { |
| break format!("recovery timed out after {SESSION_RECOVERY_TIMEOUT:?}"); |
| } |
| sleep(retry_delay.min(deadline - now)).await; |
| }; |
|
|
| let message = |
| format!("{disconnect_message}; failed to resume exec-server session: {last_error}"); |
| self.fail(message).await; |
| } |
|
|
| async fn wait_for_process_starts(&self) { |
| let mut connection_changed = self.connection_changed.subscribe(); |
| loop { |
| let starts_are_done = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner) |
| .active_process_starts |
| == 0; |
| if starts_are_done { |
| return; |
| } |
| let _ = connection_changed.changed().await; |
| } |
| } |
|
|
| fn install_recovered_client(&self, rpc_client: Arc<RpcClient>) -> bool { |
| let installed = { |
| let mut connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| if !matches!(connection.status, ConnectionStatus::Recovering) |
| || rpc_client.is_disconnected() |
| { |
| false |
| } else { |
| connection.set_status(ConnectionStatus::Connected(rpc_client)); |
| true |
| } |
| }; |
| if installed { |
| self.notify_connection_changed(); |
| } |
| installed |
| } |
|
|
| fn notify_connection_changed(&self) { |
| self.connection_changed.send_replace(()); |
| } |
|
|
| async fn resume_once( |
| self: &Arc<Self>, |
| session_id: &str, |
| ) -> Result<(Arc<RpcClient>, Option<tokio::sync::OwnedSemaphorePermit>), ExecServerError> { |
| let reconnect_strategy = self |
| .reconnect_strategy |
| .as_ref() |
| .ok_or_else(|| ExecServerError::Protocol("missing reconnect strategy".to_string()))?; |
| let attempt = reconnect_strategy.resume(session_id).await?; |
| let (connection, options, attempt_permit, noise_context) = attempt.into_parts(); |
| let (rpc_client, events_rx) = RpcClient::new(connection); |
| let rpc_client = Arc::new(rpc_client); |
| let client = ExecServerClient { |
| inner: Arc::clone(self), |
| recovery_policy: RecoveryPolicy::Wait, |
| }; |
| |
| |
| |
| |
| client.spawn_rpc_reader(&rpc_client, events_rx); |
| client |
| .initialize_rpc(&rpc_client, options, noise_context) |
| .await?; |
|
|
| self.recover_processes(&rpc_client).await?; |
| Ok((rpc_client, attempt_permit)) |
| } |
|
|
| async fn recover_processes( |
| self: &Arc<Self>, |
| rpc_client: &RpcClient, |
| ) -> Result<(), ExecServerError> { |
| let sessions = self.sessions.load_full(); |
| for (process_id, session) in sessions.iter() { |
| if !session.recoverable.load(Ordering::Acquire) { |
| continue; |
| } |
| let response = rpc_client |
| .call::<_, ReadResponse>( |
| EXEC_READ_METHOD, |
| &ReadParams { |
| process_id: process_id.clone(), |
| after_seq: Some(session.last_published_seq()), |
| max_bytes: None, |
| wait_ms: Some(0), |
| }, |
| ) |
| .await |
| .map_err(ExecServerError::from); |
| let recovered = match response { |
| Ok(response) => session.recover_events(response), |
| Err(error) if is_transport_closed_error(&error) => return Err(error), |
| Err(error) => Err(error), |
| }; |
| match recovered { |
| Ok(true) => self.remove_session_if(process_id, session), |
| Ok(false) => {} |
| Err(error) => { |
| session |
| .network_policy |
| .cancellation |
| .record(NetworkRequestCancellationReason::ProcessCancelled); |
| let terminated: Result<TerminateResponse, ExecServerError> = rpc_client |
| .call_for_cleanup( |
| EXEC_TERMINATE_METHOD, |
| &TerminateParams { |
| process_id: process_id.clone(), |
| }, |
| ) |
| .await |
| .map_err(ExecServerError::from); |
| if let Err(terminate_error) = terminated |
| && is_transport_closed_error(&terminate_error) |
| { |
| return Err(terminate_error); |
| } |
| self.remove_session_if(process_id, session); |
| session.set_failure(format!("failed to recover process {process_id}: {error}")); |
| } |
| } |
| } |
| Ok(()) |
| } |
|
|
| async fn fail(self: &Arc<Self>, message: String) { |
| let (message, newly_failed) = { |
| let mut connection = self |
| .connection |
| .lock() |
| .unwrap_or_else(std::sync::PoisonError::into_inner); |
| match &connection.status { |
| ConnectionStatus::Failed(existing) => (existing.clone(), false), |
| ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => { |
| connection.set_status(ConnectionStatus::Failed(message.clone())); |
| (message, true) |
| } |
| } |
| }; |
| if newly_failed { |
| self.notify_connection_changed(); |
| fail_all_in_flight_work(self, message.clone()).await; |
| } |
| } |
| } |
|
|
| impl ExecServerClient { |
| pub(super) fn spawn_rpc_reader( |
| &self, |
| rpc_client: &Arc<RpcClient>, |
| mut events_rx: mpsc::Receiver<RpcClientEvent>, |
| ) { |
| let inner = Arc::downgrade(&self.inner); |
| let rpc_inbound_request_slots = Arc::clone(&self.inner.rpc_inbound_request_slots); |
| let rpc_client = Arc::downgrade(rpc_client); |
| let connection_cancelled = CancellationToken::new(); |
| let connection_cancel_guard = connection_cancelled.clone().drop_guard(); |
| tokio::spawn(async move { |
| let _connection_cancel_guard = connection_cancel_guard; |
| while let Some(event) = events_rx.recv().await { |
| let (Some(inner), Some(rpc_client)) = (inner.upgrade(), rpc_client.upgrade()) |
| else { |
| return; |
| }; |
| match event { |
| RpcClientEvent::Request { |
| request, |
| request_span, |
| } => { |
| let mut request_outcome = ClientRequestOutcome { |
| span: request_span, |
| result: "disconnected", |
| }; |
| if request.method != NETWORK_POLICY_REQUEST_METHOD { |
| let error = method_not_found(format!( |
| "exec-server client does not implement `{}` yet", |
| request.method |
| )); |
| if rpc_client.respond_error(request.id, error).await.is_err() { |
| inner.request_recovery( |
| rpc_client, |
| disconnected_message( None), |
| ); |
| return; |
| } |
| request_outcome.complete("error"); |
| continue; |
| } |
| request_outcome |
| .span |
| .record("otel.name", NETWORK_POLICY_REQUEST_METHOD); |
|
|
| let request_guard = match rpc_client |
| .admit_inbound_request(&request.id, &rpc_inbound_request_slots) |
| { |
| Ok(request_guard) => request_guard, |
| Err(RpcInboundRequestAdmissionError::InvalidRequestId) => { |
| rpc_client.close_transport().await; |
| inner.request_recovery( |
| rpc_client, |
| "exec-server sent an invalid request ID".to_string(), |
| ); |
| return; |
| } |
| Err(RpcInboundRequestAdmissionError::DuplicateRequestId) => { |
| rpc_client.close_transport().await; |
| inner.request_recovery( |
| rpc_client, |
| "exec-server reused an in-flight request ID".to_string(), |
| ); |
| return; |
| } |
| Err(RpcInboundRequestAdmissionError::AtCapacity) => { |
| let response = NetworkPolicyRequestResponse { |
| decision: ExecServerNetworkPolicyDecision::Deny { |
| reason: NETWORK_POLICY_DENIAL_REASON.to_string(), |
| }, |
| }; |
| if rpc_client.respond(request.id, &response).await.is_err() { |
| inner.request_recovery( |
| rpc_client, |
| disconnected_message( None), |
| ); |
| return; |
| } |
| request_outcome.complete("success"); |
| continue; |
| } |
| }; |
| let request_id = request.id; |
| let params: NetworkPolicyRequestParams = |
| match serde_json::from_value(request.params.unwrap_or(Value::Null)) { |
| Ok(params) => params, |
| Err(_) => { |
| let error = invalid_params( |
| "invalid network policy request params".to_string(), |
| ); |
| if rpc_client.respond_error(request_id, error).await.is_err() { |
| inner.request_recovery( |
| rpc_client, |
| disconnected_message( None), |
| ); |
| return; |
| } |
| request_outcome.complete("error"); |
| continue; |
| } |
| }; |
| let process_id = params.process_id; |
| let request = params.request; |
| let process_id_valid = !process_id.is_empty() |
| && process_id.len() <= MAX_NETWORK_POLICY_PROCESS_ID_BYTES; |
| let host_valid = !request.host.is_empty() |
| && request.host.len() <= MAX_NETWORK_POLICY_HOST_BYTES |
| && !request.host.chars().any(char::is_control) |
| && !request.host.chars().any(char::is_whitespace); |
| let session = (process_id_valid && host_valid) |
| .then(|| inner.get_session(&process_id)) |
| .flatten(); |
| let controller = session |
| .as_ref() |
| .and_then(|session| session.network_policy.controller.load_full()); |
| let process_cancelled = session |
| .as_ref() |
| .map(|session| session.network_policy.cancelled.clone()); |
| let process_cancellation = session |
| .as_ref() |
| .map(|session| session.network_policy.cancellation.clone()); |
| let cancellation = NetworkRequestCancellation::default(); |
| let expected_session = session.as_ref().map(Arc::downgrade); |
| let policy_request = |
| (process_id_valid && host_valid).then_some(NetworkPolicyRequest { |
| protocol: match request.protocol { |
| ExecServerNetworkProtocol::Http => NetworkProtocol::Http, |
| ExecServerNetworkProtocol::HttpsConnect => { |
| NetworkProtocol::HttpsConnect |
| } |
| ExecServerNetworkProtocol::Socks5Tcp => { |
| NetworkProtocol::Socks5Tcp |
| } |
| ExecServerNetworkProtocol::Socks5Udp => { |
| NetworkProtocol::Socks5Udp |
| } |
| }, |
| host: request.host, |
| port: request.port, |
| environment_id: None, |
| client_addr: None, |
| method: None, |
| command: None, |
| exec_policy_hint: None, |
| execution_id: None, |
| disconnect: None, |
| cancellation: Some(cancellation.clone()), |
| }); |
| let inner = Arc::downgrade(&inner); |
| let rpc_client = Arc::downgrade(&rpc_client); |
| let connection_cancelled = connection_cancelled.clone(); |
| let task_span = request_outcome.span.clone(); |
| let task = async move { |
| let _request_guard = request_guard; |
| let decision = match (controller, policy_request, process_cancelled) { |
| (Some(controller), Some(request), Some(process_cancelled)) => { |
| |
| |
| let mut decision = controller.decider.decide(request); |
| tokio::select! { |
| biased; |
| _ = connection_cancelled.cancelled() => { |
| cancellation.record(NetworkRequestCancellationReason::ConnectionClosed); |
| return; |
| }, |
| _ = process_cancelled.cancelled() => { |
| cancellation.record(process_cancellation.as_ref() |
| .and_then(NetworkRequestCancellation::reason) |
| .unwrap_or(NetworkRequestCancellationReason::ProcessCancelled)); |
| NetworkDecision::deny(NETWORK_POLICY_DENIAL_REASON) |
| } |
| result = timeout( |
| controller.timeout, |
| &mut decision, |
| ) => result.unwrap_or_else(|_| { |
| cancellation.record(NetworkRequestCancellationReason::TimedOut); |
| NetworkDecision::deny(NETWORK_POLICY_DENIAL_REASON) |
| }), |
| } |
| } |
| (None, _, _) | (_, None, _) | (_, _, None) => { |
| NetworkDecision::deny(NETWORK_POLICY_DENIAL_REASON) |
| } |
| }; |
| if let Some(expected_session) = expected_session { |
| let (Some(inner), Some(expected_session)) = |
| (inner.upgrade(), expected_session.upgrade()) |
| else { |
| return; |
| }; |
| if !inner |
| .get_session(&process_id) |
| .is_some_and(|session| Arc::ptr_eq(&session, &expected_session)) |
| { |
| return; |
| } |
| } |
| let Some(rpc_client) = rpc_client.upgrade() else { |
| return; |
| }; |
| let decision = match decision { |
| NetworkDecision::Allow => ExecServerNetworkPolicyDecision::Allow, |
| NetworkDecision::Deny { |
| reason, decision, .. |
| } if reason.len() <= MAX_NETWORK_POLICY_REASON_BYTES |
| && !reason.chars().any(char::is_control) => |
| { |
| match decision { |
| NetworkPolicyDecision::Deny => { |
| ExecServerNetworkPolicyDecision::Deny { reason } |
| } |
| NetworkPolicyDecision::Ask => { |
| ExecServerNetworkPolicyDecision::Ask { reason } |
| } |
| } |
| } |
| NetworkDecision::Deny { .. } => { |
| ExecServerNetworkPolicyDecision::Deny { |
| reason: NETWORK_POLICY_DENIAL_REASON.to_string(), |
| } |
| } |
| }; |
| if let Err(error) = rpc_client |
| .respond(request_id, &NetworkPolicyRequestResponse { decision }) |
| .await |
| { |
| debug!( |
| ?error, |
| "failed to send network policy decision to exec-server" |
| ); |
| } else { |
| request_outcome.complete("success"); |
| } |
| }; |
| tokio::spawn(task.instrument(task_span)); |
| } |
| RpcClientEvent::Notification(notification) => { |
| if let Err(error) = handle_server_notification(&inner, notification).await { |
| rpc_client.close_transport().await; |
| inner.request_recovery( |
| rpc_client, |
| format!("exec-server notification handling failed: {error}"), |
| ); |
| return; |
| } |
| } |
| RpcClientEvent::Disconnected { reason } => { |
| inner.request_recovery(rpc_client, disconnected_message(reason.as_deref())); |
| return; |
| } |
| } |
| } |
| }); |
| } |
| } |
|
|
| pub(crate) fn is_retryable_recovery_error(error: &ExecServerError) -> bool { |
| if let ExecServerError::ConnectionAttempt(error) = error { |
| return is_retryable_recovery_error(error.as_ref()); |
| } |
| is_transport_closed_error(error) |
| || matches!( |
| error, |
| ExecServerError::ProvisioningFailed(_) |
| | ExecServerError::WebSocketConnectTimeout { .. } |
| | ExecServerError::WebSocketConnect { .. } |
| | ExecServerError::InitializeTimedOut { .. } |
| ) |
| || is_retryable_registry_error(error) |
| || matches!( |
| error, |
| ExecServerError::Server { code, .. } |
| if *code == SESSION_ALREADY_ATTACHED_ERROR_CODE |
| ) |
| } |
|
|
| pub(crate) fn is_retryable_registry_error(error: &ExecServerError) -> bool { |
| matches!( |
| error, |
| ExecServerError::EnvironmentRegistryRequest(error) |
| if error.is_connect() |
| || error.is_timeout() |
| || error.is_body() |
| || matches!( |
| error, |
| codex_http_client::RouteAwareRequestError::Request(error) |
| if error.is_decode() |
| ) |
| ) || matches!( |
| error, |
| ExecServerError::EnvironmentRegistryHttp { status, .. } |
| if status.is_server_error() |
| || *status == http::StatusCode::REQUEST_TIMEOUT |
| || *status == http::StatusCode::TOO_MANY_REQUESTS |
| ) || is_environment_offline_error(error) |
| } |
|
|
| pub(crate) fn is_environment_offline_error(error: &ExecServerError) -> bool { |
| matches!( |
| error, |
| ExecServerError::EnvironmentRegistryHttp { status, code, .. } |
| if *status == http::StatusCode::CONFLICT |
| && code.as_deref() == Some("environment_offline") |
| ) |
| } |
|
|
| pub(crate) fn registry_recovery_retry_delay(retry_key: &str, attempt: u32) -> Duration { |
| let multiplier = 1_u32.checked_shl(attempt.min(4)).unwrap_or(u32::MAX); |
| let base_delay = REGISTRY_RECOVERY_INITIAL_RETRY_INTERVAL |
| .saturating_mul(multiplier) |
| .min(REGISTRY_RECOVERY_MAX_RETRY_INTERVAL); |
| let base_millis = base_delay.as_millis() as u64; |
| let mut hasher = DefaultHasher::new(); |
| retry_key.hash(&mut hasher); |
| attempt.hash(&mut hasher); |
|
|
| Duration::from_millis(base_millis + hasher.finish() % (base_millis / 2 + 1)) |
| } |
|
|
| #[cfg(test)] |
| #[path = "client_recovery_tests.rs"] |
| mod tests; |
|
|