Skip to content

Commit 06ac280

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 d1d8184 commit 06ac280

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
@@ -39,7 +39,7 @@ pub use io::OutputBuffer;
3939
pub use store::{Store, StoreBuilder, Wasi, WasiVersion};
4040

4141
/// The default [`EngineBuilder::epoch_tick_interval`].
42-
pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(10);
42+
pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(1);
4343

4444
const MB: u64 = 1 << 20;
4545
const GB: u64 = 1 << 30;
@@ -276,10 +276,11 @@ impl<T: Send + Sync> EngineBuilder<T> {
276276
.add_host_component(&mut self.linker, host_component)
277277
}
278278

279-
/// Sets the epoch tick internal for the built [`Engine`].
279+
/// Sets the epoch tick interval for the built [`Engine`].
280280
///
281-
/// This is used by [`Store::set_deadline`] to calculate the number of
282-
/// "ticks" for epoch interruption, and by the default epoch ticker thread.
281+
/// This determines how often the engine's "epoch" will be incremented,
282+
/// which determines the resolution of interrupt-based features like
283+
/// [`Store::yield_interval`].
283284
/// The default is [`DEFAULT_EPOCH_TICK_INTERVAL`].
284285
///
285286
/// See [`EngineBuilder::epoch_ticker_thread`] and
@@ -292,8 +293,8 @@ impl<T: Send + Sync> EngineBuilder<T> {
292293
/// [`Engine`] is built.
293294
///
294295
/// Enabled by default; if disabled, the user must arrange to call
295-
/// `engine.as_ref().increment_epoch()` every `epoch_tick_interval` or
296-
/// interrupt-based features like `Store::set_deadline` will not work.
296+
/// `engine.as_ref().increment_epoch()` periodically or interrupt-based
297+
/// yielding will not work.
297298
pub fn epoch_ticker_thread(&mut self, enable: bool) {
298299
self.epoch_ticker_thread = enable;
299300
}

crates/core/src/store.rs

+37-33
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use anyhow::{anyhow, Result};
1+
use anyhow::{anyhow, ensure, Result};
22
use std::{
33
io::{Read, Write},
44
path::{Path, PathBuf},
55
sync::Mutex,
6-
time::{Duration, Instant},
6+
time::Duration,
77
};
88
use system_interface::io::ReadReady;
99
use tokio::io::{AsyncRead, AsyncWrite};
@@ -67,35 +67,13 @@ pub enum WasiVersion {
6767
/// A `Store` can be built with a [`StoreBuilder`].
6868
pub struct Store<T> {
6969
inner: wasmtime::Store<Data<T>>,
70-
epoch_tick_interval: Duration,
7170
}
7271

7372
impl<T> Store<T> {
7473
/// Returns a mutable reference to the [`HostComponentsData`] of this [`Store`].
7574
pub fn host_components_data(&mut self) -> &mut HostComponentsData {
7675
&mut self.inner.data_mut().host_components_data
7776
}
78-
79-
/// Sets the execution deadline.
80-
///
81-
/// This is a rough deadline; an instance will trap some time after this
82-
/// deadline, determined by [`EngineBuilder::epoch_tick_interval`] and
83-
/// details of the system's thread scheduler.
84-
///
85-
/// See [`wasmtime::Store::set_epoch_deadline`](https://docs.rs/wasmtime/latest/wasmtime/struct.Store.html#method.set_epoch_deadline).
86-
pub fn set_deadline(&mut self, deadline: Instant) {
87-
let now = Instant::now();
88-
let duration = deadline - now;
89-
let ticks = if duration.is_zero() {
90-
tracing::warn!("Execution deadline set in past: {deadline:?} < {now:?}");
91-
0
92-
} else {
93-
let ticks = duration.as_micros() / self.epoch_tick_interval.as_micros();
94-
let ticks = ticks.min(u64::MAX as u128) as u64;
95-
ticks + 1 // Add one to allow for current partially-completed tick
96-
};
97-
self.inner.set_epoch_deadline(ticks);
98-
}
9977
}
10078

10179
impl<T> AsRef<wasmtime::Store<Data<T>>> for Store<T> {
@@ -130,6 +108,7 @@ impl<T> wasmtime::AsContextMut for Store<T> {
130108
pub struct StoreBuilder {
131109
engine: wasmtime::Engine,
132110
epoch_tick_interval: Duration,
111+
yield_interval: Duration,
133112
wasi: std::result::Result<WasiCtxBuilder, String>,
134113
host_components_data: HostComponentsData,
135114
store_limits: StoreLimitsAsync,
@@ -146,6 +125,7 @@ impl StoreBuilder {
146125
Self {
147126
engine,
148127
epoch_tick_interval,
128+
yield_interval: epoch_tick_interval,
149129
wasi: Ok(wasi.into()),
150130
host_components_data: host_components.new_data(),
151131
store_limits: StoreLimitsAsync::default(),
@@ -160,6 +140,20 @@ impl StoreBuilder {
160140
self.store_limits = StoreLimitsAsync::new(Some(max_memory_size), None);
161141
}
162142

143+
/// Sets the execution yield interval.
144+
///
145+
/// A CPU-bound running instance will be forced to yield approximately
146+
/// every interval, which gives the host thread an opportunity to cancel
147+
/// the instance or schedule other work on the thread.
148+
///
149+
/// The exact interval of yielding is determined by [`EngineBuilder::epoch_tick_interval`]
150+
/// and details of the task scheduler.
151+
///
152+
/// The interval defaults to the epoch tick interval.
153+
pub fn yield_interval(&mut self, interval: Duration) {
154+
self.yield_interval = interval;
155+
}
156+
163157
/// Inherit stdin from the host process.
164158
pub fn inherit_stdin(&mut self) {
165159
self.with_wasi(|wasi| match wasi {
@@ -386,16 +380,26 @@ impl StoreBuilder {
386380

387381
inner.limiter_async(move |data| &mut data.store_limits);
388382

389-
// With epoch interruption enabled, there must be _some_ deadline set
390-
// or execution will trap immediately. Since this is a delta, we need
391-
// to avoid overflow so we'll use 2^63 which is still "practically
392-
// forever" for any plausible tick interval.
393-
inner.set_epoch_deadline(u64::MAX / 2);
383+
ensure!(
384+
!self.epoch_tick_interval.is_zero(),
385+
"epoch_tick_interval may not be zero"
386+
);
387+
let delta = self.yield_interval.as_nanos() / self.epoch_tick_interval.as_nanos();
388+
let delta = if delta == 0 {
389+
tracing::warn!(
390+
"Yield interval {interval:?} too small to resolve; clamping to tick interval {tick:?}",
391+
interval = self.yield_interval,
392+
tick = self.epoch_tick_interval);
393+
1
394+
} else if delta > u64::MAX as u128 {
395+
tracing::warn!("Yield interval too large; yielding effectively disabled");
396+
u64::MAX
397+
} else {
398+
delta as u64
399+
};
400+
inner.epoch_deadline_async_yield_and_update(delta);
394401

395-
Ok(Store {
396-
inner,
397-
epoch_tick_interval: self.epoch_tick_interval,
398-
})
402+
Ok(Store { inner })
399403
}
400404

401405
/// 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 anyhow::Context;
84
use spin_core::{
@@ -102,33 +98,11 @@ async fn test_max_memory_size_violated() {
10298
}
10399

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

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

0 commit comments

Comments
 (0)