@@ -6,18 +6,26 @@ use crate::{DEFAULT_EPOCH_TICK_INTERVAL, Data, Wasi, WasiVersion};
66use anyhow:: Result ;
77use secret:: SecretStore ;
88use std:: sync:: Arc ;
9+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
910use std:: {
1011 collections:: HashMap ,
1112 fmt:: { Debug , Formatter } ,
1213 ops:: { Deref , DerefMut } ,
1314} ;
1415use tracing:: { debug, instrument, trace} ;
1516use utils:: { Dictionary , Utils } ;
17+ use wasmtime:: UpdateDeadline ;
1618use wasmtime:: component:: ResourceTable ;
1719use wasmtime_wasi:: WasiCtxBuilder ;
1820use wasmtime_wasi_http:: WasiHttpCtx ;
1921use 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
97107impl 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