Skip to content

Commit 7bae4a1

Browse files
committed
core: Replace epoch deadline with yield
With async yielding, instance execution can be implemented by dropping the async call future, with e.g. tokio::time::timeout. Signed-off-by: Lann Martin <[email protected]>
1 parent 09da960 commit 7bae4a1

File tree

3 files changed

+50
-71
lines changed

3 files changed

+50
-71
lines changed

crates/core/src/lib.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub use io::OutputBuffer;
3838
pub use store::{Store, StoreBuilder, Wasi, WasiVersion};
3939

4040
/// The default [`EngineBuilder::epoch_tick_interval`].
41-
pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(10);
41+
pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(1);
4242

4343
const MB: u64 = 1 << 20;
4444
const GB: u64 = 1 << 30;
@@ -254,10 +254,11 @@ impl<T: Send + Sync> EngineBuilder<T> {
254254
.add_host_component(&mut self.linker, host_component)
255255
}
256256

257-
/// Sets the epoch tick internal for the built [`Engine`].
257+
/// Sets the epoch tick interval for the built [`Engine`].
258258
///
259-
/// This is used by [`Store::set_deadline`] to calculate the number of
260-
/// "ticks" for epoch interruption, and by the default epoch ticker thread.
259+
/// This determines how often the engine's "epoch" will be incremented,
260+
/// which determines the resolution of interrupt-based features like
261+
/// [`Store::yield_interval`].
261262
/// The default is [`DEFAULT_EPOCH_TICK_INTERVAL`].
262263
///
263264
/// See [`EngineBuilder::epoch_ticker_thread`] and
@@ -270,8 +271,8 @@ impl<T: Send + Sync> EngineBuilder<T> {
270271
/// [`Engine`] is built.
271272
///
272273
/// Enabled by default; if disabled, the user must arrange to call
273-
/// `engine.as_ref().increment_epoch()` every `epoch_tick_interval` or
274-
/// interrupt-based features like `Store::set_deadline` will not work.
274+
/// `engine.as_ref().increment_epoch()` periodically or interrupt-based
275+
/// yielding will not work.
275276
pub fn epoch_ticker_thread(&mut self, enable: bool) {
276277
self.epoch_ticker_thread = enable;
277278
}

crates/core/src/store.rs

+37-33
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use anyhow::{anyhow, Result};
1+
use anyhow::{anyhow, ensure, Result};
22
use std::{
33
io::{Read, Write},
44
path::{Path, PathBuf},
5-
time::{Duration, Instant},
5+
time::Duration,
66
};
77
use system_interface::io::ReadReady;
88
use wasi_common_preview1 as wasi_preview1;
@@ -56,35 +56,13 @@ pub enum WasiVersion {
5656
/// A `Store` can be built with a [`StoreBuilder`].
5757
pub struct Store<T> {
5858
inner: wasmtime::Store<Data<T>>,
59-
epoch_tick_interval: Duration,
6059
}
6160

6261
impl<T> Store<T> {
6362
/// Returns a mutable reference to the [`HostComponentsData`] of this [`Store`].
6463
pub fn host_components_data(&mut self) -> &mut HostComponentsData {
6564
&mut self.inner.data_mut().host_components_data
6665
}
67-
68-
/// Sets the execution deadline.
69-
///
70-
/// This is a rough deadline; an instance will trap some time after this
71-
/// deadline, determined by [`EngineBuilder::epoch_tick_interval`] and
72-
/// details of the system's thread scheduler.
73-
///
74-
/// See [`wasmtime::Store::set_epoch_deadline`](https://docs.rs/wasmtime/latest/wasmtime/struct.Store.html#method.set_epoch_deadline).
75-
pub fn set_deadline(&mut self, deadline: Instant) {
76-
let now = Instant::now();
77-
let duration = deadline - now;
78-
let ticks = if duration.is_zero() {
79-
tracing::warn!("Execution deadline set in past: {deadline:?} < {now:?}");
80-
0
81-
} else {
82-
let ticks = duration.as_micros() / self.epoch_tick_interval.as_micros();
83-
let ticks = ticks.min(u64::MAX as u128) as u64;
84-
ticks + 1 // Add one to allow for current partially-completed tick
85-
};
86-
self.inner.set_epoch_deadline(ticks);
87-
}
8866
}
8967

9068
impl<T> AsRef<wasmtime::Store<Data<T>>> for Store<T> {
@@ -119,6 +97,7 @@ impl<T> wasmtime::AsContextMut for Store<T> {
11997
pub struct StoreBuilder {
12098
engine: wasmtime::Engine,
12199
epoch_tick_interval: Duration,
100+
yield_interval: Duration,
122101
wasi: std::result::Result<WasiCtxBuilder, String>,
123102
host_components_data: HostComponentsData,
124103
store_limits: StoreLimitsAsync,
@@ -135,6 +114,7 @@ impl StoreBuilder {
135114
Self {
136115
engine,
137116
epoch_tick_interval,
117+
yield_interval: epoch_tick_interval,
138118
wasi: Ok(wasi.into()),
139119
host_components_data: host_components.new_data(),
140120
store_limits: StoreLimitsAsync::default(),
@@ -149,6 +129,20 @@ impl StoreBuilder {
149129
self.store_limits = StoreLimitsAsync::new(Some(max_memory_size), None);
150130
}
151131

132+
/// Sets the execution yield interval.
133+
///
134+
/// A CPU-bound running instance will be forced to yield approximately
135+
/// every interval, which gives the host thread an opportunity to cancel
136+
/// the instance or schedule other work on the thread.
137+
///
138+
/// The exact interval of yielding is determined by [`EngineBuilder::epoch_tick_interval`]
139+
/// and details of the task scheduler.
140+
///
141+
/// The interval defaults to the epoch tick interval.
142+
pub fn yield_interval(&mut self, interval: Duration) {
143+
self.yield_interval = interval;
144+
}
145+
152146
/// Inherit stdin from the host process.
153147
pub fn inherit_stdin(&mut self) {
154148
self.with_wasi(|wasi| match wasi {
@@ -370,16 +364,26 @@ impl StoreBuilder {
370364

371365
inner.limiter_async(move |data| &mut data.store_limits);
372366

373-
// With epoch interruption enabled, there must be _some_ deadline set
374-
// or execution will trap immediately. Since this is a delta, we need
375-
// to avoid overflow so we'll use 2^63 which is still "practically
376-
// forever" for any plausible tick interval.
377-
inner.set_epoch_deadline(u64::MAX / 2);
367+
ensure!(
368+
!self.epoch_tick_interval.is_zero(),
369+
"epoch_tick_interval may not be zero"
370+
);
371+
let delta = self.yield_interval.as_nanos() / self.epoch_tick_interval.as_nanos();
372+
let delta = if delta == 0 {
373+
tracing::warn!(
374+
"Yield interval {interval:?} too small to resolve; clamping to tick interval {tick:?}",
375+
interval = self.yield_interval,
376+
tick = self.epoch_tick_interval);
377+
1
378+
} else if delta > u64::MAX as u128 {
379+
tracing::warn!("Yield interval too large; yielding effectively disabled");
380+
u64::MAX
381+
} else {
382+
delta as u64
383+
};
384+
inner.epoch_deadline_async_yield_and_update(delta);
378385

379-
Ok(Store {
380-
inner,
381-
epoch_tick_interval: self.epoch_tick_interval,
382-
})
386+
Ok(Store { inner })
383387
}
384388

385389
/// Builds a [`Store`] from this builder with `Default` host state data.

crates/core/tests/integration_test.rs

+6-32
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
use std::{
2-
io::Cursor,
3-
path::PathBuf,
4-
time::{Duration, Instant},
5-
};
1+
use std::{io::Cursor, path::PathBuf, time::Duration};
62

73
use spin_core::{
84
Component, Config, Engine, HostComponent, I32Exit, Store, StoreBuilder, Trap, WasiVersion,
@@ -101,33 +97,11 @@ async fn test_max_memory_size_violated() {
10197
}
10298

10399
#[tokio::test(flavor = "multi_thread")]
104-
async fn test_set_deadline_obeyed() {
105-
run_core_wasi_test_engine(
106-
&test_engine(),
107-
["sleep", "20"],
108-
|_| {},
109-
|store| {
110-
store.set_deadline(Instant::now() + Duration::from_millis(1000));
111-
},
112-
)
113-
.await
114-
.unwrap();
115-
}
116-
117-
#[tokio::test(flavor = "multi_thread")]
118-
async fn test_set_deadline_violated() {
119-
let err = run_core_wasi_test_engine(
120-
&test_engine(),
121-
["sleep", "100"],
122-
|_| {},
123-
|store| {
124-
store.set_deadline(Instant::now() + Duration::from_millis(10));
125-
},
126-
)
127-
.await
128-
.unwrap_err();
129-
let trap = err.downcast::<Trap>().expect("trap");
130-
assert_eq!(trap, Trap::Interrupt);
100+
async fn test_yield_interval_timeout() {
101+
let forever = u64::MAX.to_string();
102+
let fut = run_core_wasi_test(["sleep", &forever], |_| {});
103+
let res = tokio::time::timeout(Duration::from_micros(1), fut).await;
104+
assert!(res.is_err());
131105
}
132106

133107
#[tokio::test(flavor = "multi_thread")]

0 commit comments

Comments
 (0)