suprohub opened issue #12485:
Why i see only start and dont see code interrupt every 50 ms?
use std::time::Duration; use anyhow::Result; use wasmtime::*; #[tokio::main] async fn main() -> Result<()> { let mut config = Config::default(); config.async_support(true); config.epoch_interruption(true); let engine = Engine::new(&config)?; let engine_clone = engine.clone(); std::thread::spawn(move || { loop { std::thread::sleep(Duration::from_millis(50)); engine_clone.increment_epoch(); } }); let wat = r#" (module (type (;0;) (func)) (func (;0;) (type 0) loop ;; label = @1 loop i32.const 1 drop br 1 end br 0 (;@1;) end) (memory (;0;) 16) (global (;0;) (mut i32) (i32.const 1048576)) (global (;1;) i32 (i32.const 1048576)) (global (;2;) i32 (i32.const 1048576)) (export "memory" (memory 0)) (export "main" (func 0)) (export "__data_end" (global 1)) (export "__heap_base" (global 2)) ) "#; let module = Module::new(&engine, wat)?; // Create a `Linker` which will be later used to instantiate this module. // Host functionality is defined by name within the `Linker`. let mut linker = Linker::new(&engine); linker.func_wrap("host", "host_func", |caller: Caller<'_, StoreLimits>, param: i32| { println!("Got {} from WebAssembly", param); })?; // All wasm objects operate within the context of a "store". Each // `Store` has a type parameter to store host-specific data, which in // this case we're using `4` for. let mut store = Store::new(&engine, StoreLimits::default()); store.limiter(|state| state); store.set_epoch_deadline(1); store.epoch_deadline_async_yield_and_update(1); let instance = linker.instantiate_async(&mut store, &module).await?; let hello = instance.get_typed_func::<(), ()>(&mut store, "main")?; // And finally we can call the wasm! let call_future = hello.call_async(&mut store, ()); tokio::pin!(call_future); loop { println!("start"); call_future.as_mut().await?; println!("epoch"); tokio::time::sleep(Duration::from_millis(1000)).await; } Ok(()) }
cfallin commented on issue #12485:
call_future.as_mut().await?;Awaiting the call future will block until the call is complete. An epoch yield configured with
epoch_deadline_async_yield_and_updateoperates "below the abstraction level" of future completion: it causes theFuture::pollinvocation on the future to returnPoll::Pendingand mark itself immediately ready to re-execute (via the waker), giving the async event loop a chance to run something else.That gets back to the intent of the epoch mechanism: it is to make arbitrarily-long-running Wasm code play well with an async executor loop.
There are two-and-a-half ways to actually see the yield:
- If you want to observe this at the call level, there's nothing stopping you from writing a custom future combinator that provides futures for each
pollthat always complete but may return a "pending" value at the API level.- You could also configure your engine with
Config::epoch_deadline_callbackand provide a custom callback that does whatever.- You could also configure your engine with
Config::epoch_deadline_trap. This will give you an actual return from the call, as a trap. The caveat is that the trap ends execution; you cannot resume it. (I call this the "half"-way here because it gives you only half of what you want.)
cfallin closed issue #12485:
Why i see only start and dont see code interrupt every 50 ms?
use std::time::Duration; use anyhow::Result; use wasmtime::*; #[tokio::main] async fn main() -> Result<()> { let mut config = Config::default(); config.async_support(true); config.epoch_interruption(true); let engine = Engine::new(&config)?; let engine_clone = engine.clone(); std::thread::spawn(move || { loop { std::thread::sleep(Duration::from_millis(50)); engine_clone.increment_epoch(); } }); let wat = r#" (module (type (;0;) (func)) (func (;0;) (type 0) loop ;; label = @1 loop i32.const 1 drop br 1 end br 0 (;@1;) end) (memory (;0;) 16) (global (;0;) (mut i32) (i32.const 1048576)) (global (;1;) i32 (i32.const 1048576)) (global (;2;) i32 (i32.const 1048576)) (export "memory" (memory 0)) (export "main" (func 0)) (export "__data_end" (global 1)) (export "__heap_base" (global 2)) ) "#; let module = Module::new(&engine, wat)?; // Create a `Linker` which will be later used to instantiate this module. // Host functionality is defined by name within the `Linker`. let mut linker = Linker::new(&engine); linker.func_wrap("host", "host_func", |caller: Caller<'_, StoreLimits>, param: i32| { println!("Got {} from WebAssembly", param); })?; // All wasm objects operate within the context of a "store". Each // `Store` has a type parameter to store host-specific data, which in // this case we're using `4` for. let mut store = Store::new(&engine, StoreLimits::default()); store.limiter(|state| state); store.set_epoch_deadline(1); store.epoch_deadline_async_yield_and_update(1); let instance = linker.instantiate_async(&mut store, &module).await?; let hello = instance.get_typed_func::<(), ()>(&mut store, "main")?; // And finally we can call the wasm! let call_future = hello.call_async(&mut store, ()); tokio::pin!(call_future); loop { println!("start"); call_future.as_mut().await?; println!("epoch"); tokio::time::sleep(Duration::from_millis(1000)).await; } Ok(()) }
cfallin commented on issue #12485:
I'll close this issue since the API is working as designed, but feel free to ask followup questions here or in our Zulip.
Last updated: Feb 24 2026 at 04:36 UTC