Codex Handbook
exec-server/src/server/session_registry.rs 259 lines
use std::collections::HashMap;use std::sync::Arc;use std::sync::Mutex as StdMutex;use std::time::Duration;use codex_app_server_protocol::JSONRPCErrorError;use tokio::sync::Mutex;use uuid::Uuid;use crate::rpc::RpcNotificationSender;use crate::rpc::invalid_request;use crate::server::process_handler::ProcessHandler;#[cfg(test)]const DETACHED_SESSION_TTL: Duration = Duration::from_millis(200);#[cfg(not(test))]const DETACHED_SESSION_TTL: Duration = Duration::from_secs(10);pub(crate) struct SessionRegistry {    sessions: Mutex<HashMap<String, Arc<SessionEntry>>>,}struct SessionEntry {    session_id: String,    process: ProcessHandler,    attachment: StdMutex<AttachmentState>,}struct AttachmentState {    current_connection_id: Option<ConnectionId>,    detached_connection_id: Option<ConnectionId>,    detached_expires_at: Option<tokio::time::Instant>,}#[derive(Clone, Copy, Debug, Eq, PartialEq)]struct ConnectionId(Uuid);impl std::fmt::Display for ConnectionId {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        self.0.fmt(f)    }}#[derive(Clone)]pub(crate) struct SessionHandle {    registry: Arc<SessionRegistry>,    entry: Arc<SessionEntry>,    connection_id: ConnectionId,}impl SessionRegistry {    pub(crate) fn new() -> Arc<Self> {        Arc::new(Self {            sessions: Mutex::new(HashMap::new()),        })    }    pub(crate) async fn attach(        self: &Arc<Self>,        resume_session_id: Option<String>,        notifications: RpcNotificationSender,    ) -> Result<SessionHandle, JSONRPCErrorError> {        enum AttachOutcome {            Attached(Arc<SessionEntry>),            Expired {                session_id: String,                entry: Arc<SessionEntry>,            },        }        let connection_id = ConnectionId(Uuid::new_v4());        let outcome = {            let mut sessions = self.sessions.lock().await;            if let Some(session_id) = resume_session_id {                let entry = sessions                    .get(&session_id)                    .cloned()                    .ok_or_else(|| invalid_request(format!("unknown session id {session_id}")))?;                if entry.is_expired(tokio::time::Instant::now()) {                    let entry = sessions.remove(&session_id).ok_or_else(|| {                        invalid_request(format!("unknown session id {session_id}"))                    })?;                    Ok(AttachOutcome::Expired { session_id, entry })                } else if entry.has_active_connection() {                    Err(invalid_request(format!(                        "session {session_id} is already attached to another connection"                    )))                } else {                    entry.process.set_notification_sender(Some(notifications));                    entry.attach(connection_id);                    Ok(AttachOutcome::Attached(entry))                }            } else {                let session_id = Uuid::new_v4().to_string();                let entry = Arc::new(SessionEntry::new(                    session_id.clone(),                    ProcessHandler::new(notifications),                    connection_id,                ));                sessions.insert(session_id, Arc::clone(&entry));                Ok(AttachOutcome::Attached(entry))            }        };        let entry = match outcome? {            AttachOutcome::Attached(entry) => entry,            AttachOutcome::Expired { session_id, entry } => {                entry.process.shutdown().await;                return Err(invalid_request(format!("unknown session id {session_id}")));            }        };        Ok(SessionHandle {            registry: Arc::clone(self),            entry,            connection_id,        })    }    async fn expire_if_detached(&self, session_id: String, connection_id: ConnectionId) {        tokio::time::sleep(DETACHED_SESSION_TTL).await;        let removed = {            let mut sessions = self.sessions.lock().await;            let Some(entry) = sessions.get(&session_id) else {                return;            };            if !entry.is_detached_connection_expired(connection_id, tokio::time::Instant::now()) {                return;            }            sessions.remove(&session_id)        };        if let Some(entry) = removed {            entry.process.shutdown().await;        }    }}impl Default for SessionRegistry {    fn default() -> Self {        Self {            sessions: Mutex::new(HashMap::new()),        }    }}impl SessionEntry {    fn new(session_id: String, process: ProcessHandler, connection_id: ConnectionId) -> Self {        Self {            session_id,            process,            attachment: StdMutex::new(AttachmentState {                current_connection_id: Some(connection_id),                detached_connection_id: None,                detached_expires_at: None,            }),        }    }    fn attach(&self, connection_id: ConnectionId) {        let mut attachment = self            .attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        attachment.current_connection_id = Some(connection_id);        attachment.detached_connection_id = None;        attachment.detached_expires_at = None;    }    fn detach(&self, connection_id: ConnectionId) -> bool {        let mut attachment = self            .attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        if attachment.current_connection_id != Some(connection_id) {            return false;        }        attachment.current_connection_id = None;        attachment.detached_connection_id = Some(connection_id);        attachment.detached_expires_at = Some(tokio::time::Instant::now() + DETACHED_SESSION_TTL);        true    }    fn has_active_connection(&self) -> bool {        self.attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .current_connection_id            .is_some()    }    fn is_attached_to(&self, connection_id: ConnectionId) -> bool {        self.attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .current_connection_id            == Some(connection_id)    }    fn is_expired(&self, now: tokio::time::Instant) -> bool {        self.attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner)            .detached_expires_at            .is_some_and(|deadline| now >= deadline)    }    fn is_detached_connection_expired(        &self,        connection_id: ConnectionId,        now: tokio::time::Instant,    ) -> bool {        let attachment = self            .attachment            .lock()            .unwrap_or_else(std::sync::PoisonError::into_inner);        attachment.current_connection_id.is_none()            && attachment.detached_connection_id == Some(connection_id)            && attachment                .detached_expires_at                .is_some_and(|deadline| now >= deadline)    }}impl SessionHandle {    pub(crate) fn session_id(&self) -> &str {        &self.entry.session_id    }    pub(crate) fn connection_id(&self) -> String {        self.connection_id.to_string()    }    pub(crate) fn is_session_attached(&self) -> bool {        self.entry.is_attached_to(self.connection_id)    }    pub(crate) fn process(&self) -> &ProcessHandler {        &self.entry.process    }    pub(crate) async fn detach(&self) {        if !self.entry.detach(self.connection_id) {            return;        }        self.entry            .process            .set_notification_sender(/*notifications*/ None);        let registry = Arc::clone(&self.registry);        let session_id = self.entry.session_id.clone();        let connection_id = self.connection_id;        tokio::spawn(async move {            registry.expire_if_detached(session_id, connection_id).await;        });    }}