fix: check for exceptions during promise polling#628
fix: check for exceptions during promise polling#628keabarnes wants to merge 3 commits intoDelSkayn:masterfrom
Conversation
|
I used the #!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! rquickjs = { version = "0.10", features = ["futures", "loader", "parallel"] }
//! tokio = { version = "1", features = ["full", "rt-multi-thread", "macros", "time"] }
//! ```
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use rquickjs::{AsyncContext, AsyncRuntime, Function, Module, async_with};
const JS_SCRIPT: &str = r#"
export default async function main() {
console.log("JS: Awaiting Promise.resolve() chain...");
const result = await Promise.resolve("start")
.then(v => {
console.log(`JS: then #1: ${v} -> processing`);
return "middle";
})
.then(v => {
console.log(`JS: then #2: ${v} -> done`);
return "end";
});
console.log(`JS: Promise chain completed with: ${result}`);
console.log("JS: Starting busy-wait loop (will be interrupted)");
// Busy-wait loop that will be interrupted after ~2 seconds
const start = Date.now();
const duration = 10000; // 10 seconds
let lastLog = start;
while (Date.now() - start < duration) {
if (Date.now() - lastLog >= 1000) {
const elapsed = Math.floor((Date.now() - start) / 1000);
console.log(`JS: Busy-wait loop running... ${elapsed}s elapsed`);
lastLog = Date.now();
}
}
console.log("JS: Busy-wait loop completed (should NOT see this if interrupted)");
return "completed";
}
"#;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Reproduction Test ===\n");
let runtime = AsyncRuntime::new()?;
let context = AsyncContext::full(&runtime).await?;
// Cancellation flag - will be set after 2 seconds
let cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag_for_handler = cancel_flag.clone();
// Set up interrupt handler
runtime
.set_interrupt_handler(Some(Box::new(move || {
let should_cancel = cancel_flag_for_handler.load(Ordering::SeqCst);
if should_cancel {
println!("RUST: Interrupt handler fired, returning true to stop JS execution");
true
} else {
false
}
})))
.await;
// Spawn a task to set the cancellation flag after 2 seconds
tokio::spawn(async move {
println!("RUST: Will trigger cancellation in 2 seconds...");
tokio::time::sleep(Duration::from_secs(2)).await;
println!("RUST: Setting cancellation flag NOW");
cancel_flag.store(true, Ordering::SeqCst);
});
println!("RUST: Starting script execution...\n");
// Timeout around the ENTIRE async_with! block
let result = tokio::time::timeout(
Duration::from_secs(10),
async_with!(context => |ctx| {
// Set up console.log
let console = rquickjs::Object::new(ctx.clone())?;
console.set("log", Function::new(ctx.clone(), |msg: String| {
println!("{}", msg);
}))?;
ctx.globals().set("console", console)?;
// Declare and evaluate the module
let module = Module::declare(ctx.clone(), "script", JS_SCRIPT)?;
let (module, module_promise) = module.eval()?;
module_promise.into_future::<()>().await?;
// Get and call the default export
let main_fn: Function = module.get("default")?;
let promise: rquickjs::Promise = main_fn.call(())?;
println!("\nRUST: Called main(), awaiting pure JS async function promise...");
println!("RUST: (No Rust async functions involved in JS code)\n");
// Await the promise directly
let result: Result<String, _> = promise.into_future().await;
println!("\nRUST: Promise settled!");
result
})
).await;
// Clean up
runtime.idle().await;
println!("\n=== Result ===");
match result {
Ok(Ok(value)) => println!("Success: {}", value),
Ok(Err(e)) => println!("JS Error: {:?}", e),
Err(_) => println!("TIMEOUT: async_with! block did not complete within 10 seconds"),
}
Ok(())
} |
|
Targeting |
|
Updating to my branch shows the early return when the interrupt is set (which generates the "interrupted" exception): |
|
Notably, removing the block below from the JS code and using const result = await Promise.resolve("start")
.then(v => {
console.log(`JS: then #1: ${v} -> processing`);
return "middle";
})
.then(v => {
console.log(`JS: then #2: ${v} -> done`);
return "end";
}); |
|
I have medium confidence that this is the correct solution given I don't know the impact of these changes in other contexts. I'm happy to change the implementation or create an issue instead if an alternate approach is better. |
|
@Sytten I think this looks OK, thoughts? |
|
Seems ok from my very tired brain, but a take from @DelSkayn would be nice. |
Description of changes
This PR adjusts the
Pollbehaviour ofPromiseFutureto return early with aPoll::Readyvariant and an exception indicator.Context: I was trying to make use of
AsyncRuntime.set_interrupt_handlerbut noticed that, if there were anyawaitcalls in my JS code, it would result in the the JS execution halting but theasync_function_promise.into_future().awaithanging.Checklist
Created unit tests for my feature if needed