Skip to content

Commit a8457cb

Browse files
committed
feat: add support for epoch-based deadline extensions for host I/O
- Introduced `epoch_pause_ms` to track elapsed host I/O time and extend execution deadlines. - Enhanced `StoreBuilder` with `epoch_pause_ms` and `max_external_duration_ms` configuration. - Updated HTTP backends to deposit I/O time into shared counters and refund guest execution ticks. - Integrated `epoch_deadline_callback` for dynamic deadline adjustments.
1 parent 7dd599c commit a8457cb

5 files changed

Lines changed: 146 additions & 49 deletions

File tree

crates/http-backend/src/lib.rs

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use std::future::Future;
66
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
77
use std::pin::Pin;
88
use std::sync::Arc;
9+
use std::sync::atomic::{AtomicU64, Ordering};
910
use std::task::{Context, Poll};
10-
use std::time::Duration;
11+
use std::time::{Duration, Instant};
1112

1213
use anyhow::{Error, Result, anyhow};
1314
use http::{HeaderMap, HeaderName, Uri, header, uri::Scheme};
@@ -64,6 +65,10 @@ pub struct Backend<C> {
6465
pub strategy: BackendStrategy,
6566
ext_http_stats: Option<Arc<dyn ExtRequestStats>>,
6667
hostname: Option<SmolStr>,
68+
/// Counter shared with the wasmtime Store: each outbound HTTP call
69+
/// deposits its elapsed wall-clock time (ms) here so the epoch
70+
/// deadline callback can refund those ticks to the guest.
71+
epoch_pause_ms: Arc<AtomicU64>,
6772
}
6873

6974
pub struct Builder {
@@ -112,6 +117,7 @@ impl Builder {
112117
strategy: self.strategy,
113118
ext_http_stats: None,
114119
hostname: self.hostname.clone(),
120+
epoch_pause_ms: Arc::new(AtomicU64::new(0)),
115121
}
116122
}
117123
}
@@ -140,6 +146,12 @@ impl<C> Backend<C> {
140146
self.ext_http_stats.replace(stats);
141147
}
142148

149+
/// Share the epoch-pause counter with the Store so that outbound HTTP
150+
/// time can be excluded from the guest's execution-time budget.
151+
pub fn set_epoch_pause_ms(&mut self, counter: Arc<AtomicU64>) {
152+
self.epoch_pause_ms = counter;
153+
}
154+
143155
pub fn propagate_header_names(&self) -> HeaderNameList {
144156
self.propagate_header_names.clone()
145157
}
@@ -321,48 +333,57 @@ where
321333
.as_ref()
322334
.map(|s| ExtStatsTimer::new(s.clone()));
323335

324-
let res = self.client.request(request).await.map_err(|error| {
325-
warn!(cause=?error, "sending request to backend");
326-
HttpError::RequestError
327-
})?;
328-
329-
let status = res.status().as_u16();
330-
let (parts, body) = res.into_parts();
331-
let headers = if !parts.headers.is_empty() {
332-
Some(
333-
parts
334-
.headers
335-
.iter()
336-
.filter_map(|(name, value)| match value.to_str() {
337-
Ok(value) => Some((name.to_string(), value.to_string())),
338-
Err(error) => {
339-
warn!(cause=?error, "invalid value: {:?}", value);
340-
None
341-
}
342-
})
343-
.collect::<Vec<(String, String)>>(),
344-
)
345-
} else {
346-
None
347-
};
348-
349-
let body_bytes = body
350-
.collect()
351-
.await
352-
.map_err(|error| {
353-
warn!(cause=?error, "receiving body from backend");
336+
// Time the network I/O so we can refund the equivalent epoch ticks
337+
// to the guest. Both the request send and the body read count.
338+
let started = Instant::now();
339+
let result = async {
340+
let res = self.client.request(request).await.map_err(|error| {
341+
warn!(cause=?error, "sending request to backend");
354342
HttpError::RequestError
355-
})?
356-
.to_bytes();
357-
let body = Some(body_bytes.to_vec());
358-
359-
trace!(?status, ?headers, len = body_bytes.len(), "reply");
343+
})?;
344+
345+
let status = res.status().as_u16();
346+
let (parts, body) = res.into_parts();
347+
let headers = if !parts.headers.is_empty() {
348+
Some(
349+
parts
350+
.headers
351+
.iter()
352+
.filter_map(|(name, value)| match value.to_str() {
353+
Ok(value) => Some((name.to_string(), value.to_string())),
354+
Err(error) => {
355+
warn!(cause=?error, "invalid value: {:?}", value);
356+
None
357+
}
358+
})
359+
.collect::<Vec<(String, String)>>(),
360+
)
361+
} else {
362+
None
363+
};
360364

361-
Ok(Response {
362-
status,
363-
headers,
364-
body,
365-
})
365+
let body_bytes = body
366+
.collect()
367+
.await
368+
.map_err(|error| {
369+
warn!(cause=?error, "receiving body from backend");
370+
HttpError::RequestError
371+
})?
372+
.to_bytes();
373+
let body = Some(body_bytes.to_vec());
374+
375+
trace!(?status, ?headers, len = body_bytes.len(), "reply");
376+
377+
Ok::<_, HttpError>(Response {
378+
status,
379+
headers,
380+
body,
381+
})
382+
}
383+
.await;
384+
self.epoch_pause_ms
385+
.fetch_add(started.elapsed().as_millis() as u64, Ordering::Relaxed);
386+
result
366387
}
367388
}
368389

crates/http-service/src/executor/http.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use reactor::gcore::fastedge;
1111
use runtime::util::stats::{StatsTimer, StatsVisitor};
1212
use runtime::{InstancePre, store::StoreBuilder};
1313
use std::sync::Arc;
14+
use std::sync::atomic::AtomicU64;
1415
use std::time::Duration;
1516
use wasmtime_wasi_http::body::HyperOutgoingBody;
1617

@@ -73,14 +74,21 @@ where
7374

7475
let properties = executor::get_properties(&parts.headers);
7576

76-
let store_builder = self.store_builder.with_properties(properties);
77+
// Shared counter so external HTTP time in `http_backend.send_request`
78+
// refunds epoch ticks via the Store's `epoch_deadline_callback`.
79+
let epoch_pause_ms = Arc::new(AtomicU64::new(0));
80+
let store_builder = self
81+
.store_builder
82+
.with_properties(properties)
83+
.epoch_pause_ms(epoch_pause_ms.clone());
7784
let mut http_backend = self.backend;
7885

7986
http_backend
8087
.propagate_headers(parts.headers.clone())
8188
.context("propagate headers")?;
8289

8390
http_backend.set_ext_http_stats(stats.clone());
91+
http_backend.set_epoch_pause_ms(epoch_pause_ms);
8492

8593
let propagate_header_names = http_backend.propagate_header_names();
8694
let backend_uri = http_backend.uri();

crates/http-service/src/executor/wasi_http.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::sync::Arc;
2+
use std::sync::atomic::AtomicU64;
23
use std::time::Duration;
34

45
use crate::executor;
@@ -88,8 +89,16 @@ where
8889
let body = body.boxed();
8990

9091
let properties = executor::get_properties(&parts.headers);
91-
let store_builder = self.store_builder.with_properties(properties);
92+
// Shared counter so wasi-http outbound calls (handled by
93+
// `WasiHttpView::send_request` in the runtime) refund epoch ticks
94+
// via the Store's `epoch_deadline_callback`.
95+
let epoch_pause_ms = Arc::new(AtomicU64::new(0));
96+
let store_builder = self
97+
.store_builder
98+
.with_properties(properties)
99+
.epoch_pause_ms(epoch_pause_ms.clone());
92100
let mut http_backend = self.backend;
101+
http_backend.set_epoch_pause_ms(epoch_pause_ms);
93102

94103
http_backend
95104
.propagate_headers(parts.headers.clone())

crates/runtime/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use crate::app::KvStoreOption;
22
use crate::store::HasStats;
33
use http_backend::stats::ExtStatsTimer;
44
use std::sync::Arc;
5+
use std::sync::atomic::{AtomicU64, Ordering};
6+
use std::time::Instant;
57
use std::{fmt::Debug, ops::Deref};
68
use utils::{Dictionary, Utils};
79
use wasmtime_wasi::ResourceTable;
@@ -97,6 +99,11 @@ pub struct Data<T: 'static> {
9799
pub dictionary: Dictionary,
98100
pub utils: Utils,
99101
pub cache: cache::CacheImpl,
102+
/// Milliseconds of host I/O that should not count against the epoch
103+
/// deadline. The `epoch_deadline_callback` installed on the Store reads
104+
/// and clears this counter, converting milliseconds into extra ticks to
105+
/// extend the deadline.
106+
pub epoch_pause_ms: Arc<AtomicU64>,
100107
}
101108

102109
pub trait BackendRequest {
@@ -143,13 +150,16 @@ impl<T: Send + BackendRequest + HasStats> WasiHttpView for Data<T> {
143150
let request = Request::from_parts(head, body);
144151
// start external request stats timer
145152
let stats = self.inner.get_stats();
153+
let epoch_pause_ms = self.epoch_pause_ms.clone();
146154

147155
let handle = wasmtime_wasi::runtime::spawn(async move {
148156
let _stats_timer = ExtStatsTimer::new(stats); // keep timer alive until request is done
149-
Ok(
157+
let started = Instant::now();
158+
let resp =
150159
default_send_request_handler(request, OutgoingRequestConfig { use_tls, ..config })
151-
.await,
152-
)
160+
.await;
161+
epoch_pause_ms.fetch_add(started.elapsed().as_millis() as u64, Ordering::Relaxed);
162+
Ok(resp)
153163
});
154164
Ok(HostFutureIncomingResponse::pending(handle))
155165
}

crates/runtime/src/store.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,26 @@ use crate::{DEFAULT_EPOCH_TICK_INTERVAL, Data, Wasi, WasiVersion};
66
use anyhow::Result;
77
use secret::SecretStore;
88
use std::sync::Arc;
9+
use std::sync::atomic::{AtomicU64, Ordering};
910
use std::{
1011
collections::HashMap,
1112
fmt::{Debug, Formatter},
1213
ops::{Deref, DerefMut},
1314
};
1415
use tracing::{debug, instrument, trace};
1516
use utils::{Dictionary, Utils};
17+
use wasmtime::UpdateDeadline;
1618
use wasmtime::component::ResourceTable;
1719
use wasmtime_wasi::WasiCtxBuilder;
1820
use wasmtime_wasi_http::WasiHttpCtx;
1921
use wasmtime_wasi_nn::wit::WasiNnCtx;
2022

23+
/// Default extra wall-clock budget (in ms) added to the tokio timeout that
24+
/// wraps wasm execution. Acts as a soft outer bound that must comfortably
25+
/// exceed the epoch budget once host I/O credit (e.g. external HTTP) extends
26+
/// the epoch deadline.
27+
pub const DEFAULT_MAX_EXTERNAL_DURATION_MS: u64 = 30_000;
28+
2129
/// A `Store` holds the runtime state of a app instance.
2230
///
2331
/// `Store` lives only for the lifetime of a single app invocation.
@@ -92,6 +100,8 @@ pub struct StoreBuilder {
92100
key_value_store: key_value_store::Builder,
93101
dictionary: Dictionary,
94102
cache_backend: Option<Arc<dyn cache::CacheBackend>>,
103+
epoch_pause_ms: Option<Arc<AtomicU64>>,
104+
max_external_duration_ms: u64,
95105
}
96106

97107
impl StoreBuilder {
@@ -110,6 +120,8 @@ impl StoreBuilder {
110120
key_value_store: key_value_store::Builder::default(),
111121
dictionary: Default::default(),
112122
cache_backend: None,
123+
epoch_pause_ms: None,
124+
max_external_duration_ms: DEFAULT_MAX_EXTERNAL_DURATION_MS,
113125
}
114126
}
115127

@@ -193,6 +205,28 @@ impl StoreBuilder {
193205
}
194206
}
195207

208+
/// Provide the shared counter that host functions use to "pause" the
209+
/// epoch deadline. Each ms accumulated here grants the guest extra
210+
/// ticks when the epoch deadline next fires. If unset, the store
211+
/// falls back to a private counter (no host call can deposit credit).
212+
pub fn epoch_pause_ms(self, counter: Arc<AtomicU64>) -> Self {
213+
Self {
214+
epoch_pause_ms: Some(counter),
215+
..self
216+
}
217+
}
218+
219+
/// Wall-clock slack (in ms) added to the tokio timeout that wraps
220+
/// wasm execution. Must comfortably exceed the worst-case total time
221+
/// spent in epoch-paused host calls so the tokio bound does not fire
222+
/// before the epoch one.
223+
pub fn max_external_duration_ms(self, ms: u64) -> Self {
224+
Self {
225+
max_external_duration_ms: ms,
226+
..self
227+
}
228+
}
229+
196230
pub fn make_wasi_nn(&self) -> Result<WasiNnCtx> {
197231
// initialize application specific graph
198232
let backends: Vec<&str> = self
@@ -271,6 +305,9 @@ impl StoreBuilder {
271305
self.cache_backend
272306
.unwrap_or_else(|| Arc::new(cache::NoCacheBackend)),
273307
);
308+
let epoch_pause_ms = self
309+
.epoch_pause_ms
310+
.unwrap_or_else(|| Arc::new(AtomicU64::new(0)));
274311

275312
let mut inner = wasmtime::Store::new(
276313
&self.engine,
@@ -279,7 +316,8 @@ impl StoreBuilder {
279316
wasi,
280317
wasi_nn,
281318
store_limits: self.store_limits,
282-
timeout: (self.max_duration + 1) * DEFAULT_EPOCH_TICK_INTERVAL,
319+
timeout: (self.max_duration + 1) * DEFAULT_EPOCH_TICK_INTERVAL
320+
+ self.max_external_duration_ms,
283321
table,
284322
logger,
285323
http: WasiHttpCtx::new(),
@@ -288,12 +326,23 @@ impl StoreBuilder {
288326
dictionary: self.dictionary,
289327
utils,
290328
cache: cache_impl,
329+
epoch_pause_ms: epoch_pause_ms.clone(),
291330
},
292331
);
293332
inner.limiter(|state| &mut state.store_limits);
294333
// allow max number of epoch ticks (1 tick = 10 ms)
295-
inner.set_epoch_deadline(self.max_duration); // allow max number of epoch ticks (1 tick = 10 ms)
296-
inner.epoch_deadline_trap();
334+
inner.set_epoch_deadline(self.max_duration);
335+
// When the deadline fires, consume any host-call credit accumulated by
336+
// `epoch_pause_ms` to extend it; if there is no credit, trap as before.
337+
inner.epoch_deadline_callback(move |_ctx| {
338+
let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed);
339+
if credit_ms == 0 {
340+
Err(anyhow::Error::new(wasmtime::Trap::Interrupt))
341+
} else {
342+
let credit_ticks = (credit_ms / DEFAULT_EPOCH_TICK_INTERVAL).max(1);
343+
Ok(UpdateDeadline::Continue(credit_ticks))
344+
}
345+
});
297346
Ok(Store { inner })
298347
}
299348
}

0 commit comments

Comments
 (0)