mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Preserve code mode cancellation during output serialization (#52685)
## Why Reentering V8 during termination can replace uncatchable cancellation with a catchable JavaScript exception, allowing a script to continue after cancellation. ## What changed Skip JSON conversion and exception throwing while V8 execution is terminating, preserving cancellation through `image()` getters and JSON fallback. ## Testing Add regression coverage for cancellation during JSON fallback and reentrant image getters, including prompt termination through the standalone host and continued usability of another session. Extend serialization error tests to cover failing image getters alongside circular JSON. GitOrigin-RevId: f03dd131a62724106fad96111292ddf5b9dc99f6
This commit is contained in:
committed by
copyberry
parent
52950c5924
commit
c261727370
@@ -429,40 +429,64 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminate_execution_stops_cpu_bound_module() {
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
||||
let (_runtime_tx, _runtime_control_tx, runtime_terminate_handle) = spawn_runtime(
|
||||
HashMap::new(),
|
||||
execute_request("while (true) {}"),
|
||||
event_tx,
|
||||
PendingRuntimeMode::Continue,
|
||||
/*task_failure_handler*/ None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let started_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
async fn terminate_execution_stops_javascript() {
|
||||
for source in [
|
||||
r#"notify("started"); while (true) {}"#,
|
||||
// The failed first read enters JSON fallback, whose TryCatch must preserve termination.
|
||||
r#"
|
||||
let reads = 0;
|
||||
const value = {
|
||||
get image_url() {
|
||||
if (++reads === 1) throw new Error("enter fallback");
|
||||
notify("started");
|
||||
while (true) {}
|
||||
}
|
||||
};
|
||||
try { image(value); } catch {}
|
||||
text("continued after cancellation");
|
||||
"#,
|
||||
] {
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
||||
let (_runtime_tx, _runtime_control_tx, runtime_terminate_handle) = spawn_runtime(
|
||||
HashMap::new(),
|
||||
execute_request(source),
|
||||
event_tx,
|
||||
PendingRuntimeMode::Continue,
|
||||
/*task_failure_handler*/ None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(started_event, RuntimeEvent::Started));
|
||||
|
||||
assert!(runtime_terminate_handle.terminate_execution());
|
||||
|
||||
let result_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let RuntimeEvent::Result { error_text, .. } = result_event else {
|
||||
panic!("expected runtime result after termination");
|
||||
};
|
||||
assert!(error_text.is_some());
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
let started_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
.unwrap();
|
||||
assert!(matches!(started_event, RuntimeEvent::Started));
|
||||
|
||||
// Started precedes evaluation; the notification proves the workload is running.
|
||||
assert!(matches!(
|
||||
tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(RuntimeEvent::Notify { .. })
|
||||
));
|
||||
assert!(runtime_terminate_handle.terminate_execution());
|
||||
|
||||
let result_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let RuntimeEvent::Result { error_text, .. } = result_event else {
|
||||
panic!("expected runtime result after termination");
|
||||
};
|
||||
assert!(error_text.is_some());
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -297,6 +297,10 @@ pub(super) fn v8_value_to_json(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
value: v8::Local<'_, v8::Value>,
|
||||
) -> Result<Option<JsonValue>, String> {
|
||||
// Reentering V8 during termination can replace it with a catchable exception.
|
||||
if scope.is_execution_terminating() {
|
||||
return Err("JavaScript execution terminated".to_string());
|
||||
}
|
||||
// V8 stringifies undefined as the non-JSON text "undefined".
|
||||
if value.is_undefined() {
|
||||
return Ok(None);
|
||||
@@ -362,6 +366,10 @@ pub(super) fn value_to_error_text(
|
||||
}
|
||||
|
||||
pub(super) fn throw_type_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
|
||||
// Preserve V8's uncatchable termination instead of replacing it with a JS error.
|
||||
if scope.is_execution_terminating() {
|
||||
return;
|
||||
}
|
||||
if let Some(message) = v8::String::new(scope, message) {
|
||||
scope.throw_exception(message.into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user