mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## Why Replacing a gRPC code-mode runtime loses stored values and running cells. Silently continuing after reconnection hides that loss, while prior external tool operations may already have completed. ## What changed - Capture the runtime generation when `execute` and `wait` calls are created. If recovery replaces it, reject those calls with an explicit reset error, including concurrent calls and calls first polled after recovery. - Explain the lost state and advise inspecting prior tool results before retrying. Allow a fresh `exec` call to continue on the replacement runtime. - Keep `terminate` from reopening the runtime so cleanup cannot hide a reset from the next request. ## Testing Extend gRPC recovery tests to cover reset errors after host restarts, missing sessions, and route loss, rejection of queued calls, and successful fresh execution with cleared stored state. GitOrigin-RevId: 914cab3bb45e30a3f3b2afacb5631e42b18e21b0
124 lines
4.3 KiB
Rust
124 lines
4.3 KiB
Rust
//! Exercises missing-session recovery while the host's lease streams remain open.
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::sync::atomic::AtomicUsize;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
use anyhow::Result;
|
|
use codex_code_mode::CodeModeSessionProvider;
|
|
use codex_code_mode::GrpcCodeModeSessionProvider;
|
|
use codex_code_mode::NoopCodeModeSessionDelegate;
|
|
use codex_code_mode_host::GrpcCodeModeHost;
|
|
use codex_code_mode_protocol::grpc::code_mode_host_server::CodeModeHostServer;
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::net::TcpListener;
|
|
use tokio_stream::wrappers::TcpListenerStream;
|
|
use tokio_util::task::AbortOnDropHandle;
|
|
use tonic::Code;
|
|
use tonic::Status;
|
|
use tonic::transport::Server;
|
|
|
|
use super::execute;
|
|
use super::request;
|
|
use super::text_response;
|
|
|
|
#[tokio::test]
|
|
async fn only_missing_sessions_reopen_without_replaying_execution() -> Result<()> {
|
|
for (code, route_lost, expected_cell, expected_value) in [
|
|
(Code::NotFound, false, "g2:1", "undefined"),
|
|
(Code::InvalidArgument, false, "2", "1"),
|
|
(Code::Unavailable, false, "2", "1"),
|
|
(Code::Unavailable, true, "2", "1"),
|
|
] {
|
|
let rejection = Arc::new(Mutex::new(/*t*/ None));
|
|
let next_rejection = Arc::clone(&rejection);
|
|
let request_count = Arc::new(AtomicUsize::new(/*v*/ 0));
|
|
let count = Arc::clone(&request_count);
|
|
let service = CodeModeHostServer::with_interceptor(
|
|
GrpcCodeModeHost::new(),
|
|
move |request: tonic::Request<()>| {
|
|
count.fetch_add(/*val*/ 1, Ordering::SeqCst);
|
|
match next_rejection.lock().unwrap().take() {
|
|
Some(rejection) => Err(rejection),
|
|
None => Ok(request),
|
|
}
|
|
},
|
|
);
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let endpoint = format!("http://{}", listener.local_addr()?);
|
|
let _server = AbortOnDropHandle::new(tokio::spawn(
|
|
Server::builder()
|
|
.add_service(service)
|
|
.serve_with_incoming(TcpListenerStream::new(listener)),
|
|
));
|
|
let session = GrpcCodeModeSessionProvider::new(endpoint)
|
|
.create_session()
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
execute(
|
|
&session,
|
|
request(r#"store("before", 1);"#),
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
)
|
|
.await?;
|
|
|
|
// Reject the next Execute without closing the lease or subscription streams.
|
|
let mut failure = Status::new(code, "injected RPC rejection");
|
|
if route_lost {
|
|
// A direct host has no proxy route to retire.
|
|
failure
|
|
.metadata_mut()
|
|
.insert("x-code-mode-route-lost", "true".parse()?);
|
|
}
|
|
*rejection.lock().unwrap() = Some(failure.clone());
|
|
let requests_before_rejection = request_count.load(Ordering::SeqCst);
|
|
let failed = execute(
|
|
&session,
|
|
request(r#"store("before", "replayed");"#),
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
failed.to_string(),
|
|
format!("gRPC code-mode execution failed: {failure}")
|
|
);
|
|
assert_eq!(
|
|
request_count.load(Ordering::SeqCst),
|
|
requests_before_rejection + 1,
|
|
"the failed call must return without opening a session or replaying Execute"
|
|
);
|
|
if code == Code::NotFound {
|
|
let reset = execute(
|
|
&session,
|
|
request(r#"store("before", "must not run");"#),
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(
|
|
reset
|
|
.to_string()
|
|
.contains("Stored values and running cells were lost")
|
|
);
|
|
}
|
|
let actual = execute(
|
|
&session,
|
|
request(r#"text(String(load("before")))"#),
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
actual,
|
|
text_response(
|
|
expected_cell,
|
|
expected_value,
|
|
actual.code_mode_host_duration()
|
|
)
|
|
);
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
}
|
|
Ok(())
|
|
}
|