Skip to content

Commit 1c6e3de

Browse files
committed
support instance reuse for WASIp3 HTTP components
This makes use of the new `wasmtime_wasi_http::handler::ProxyHandler` utility, which provides both serial and concurrent instance reuse. We could hypothetically enable opt-in serial reuse for WASIp2 components as well using the same pattern (which is what `wasmtime serve` does), but I'll leave that for a follow-up PR, if desired. This hard-codes the configuration values (max reuse count = 128, max concurrent reuse count = 16, idle timeout = 1s) for now. Once we've decided where these values should be configured (e.g. in the spin.toml manifest, in the runtime config, or at runtime via the component itself), we can support that. See WebAssembly/wasi-http#190 for related discussion. Signed-off-by: Joel Dice <[email protected]>
1 parent 05eb532 commit 1c6e3de

File tree

7 files changed

+137
-54
lines changed

7 files changed

+137
-54
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ wasm-pkg-common = "0.11"
179179
wasmparser = "0.240.0"
180180
wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "8e22ff89", features = ["component-model-async"] }
181181
wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "8e22ff89", features = ["p3"] }
182-
wasmtime-wasi-http = { git = "https://github.com/bytecodealliance/wasmtime", rev = "8e22ff89", features = ["p3"] }
182+
wasmtime-wasi-http = { git = "https://github.com/bytecodealliance/wasmtime", rev = "8e22ff89", features = ["p3", "component-model-async"] }
183183
wit-component = "0.240.0"
184184
wit-parser = "0.240.0"
185185

crates/core/src/store.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ impl<T: 'static> Store<T> {
4848
pub fn data_mut(&mut self) -> &mut T {
4949
self.inner.data_mut()
5050
}
51+
52+
/// Convert `self` to the inner [`wasmtime::Store`].
53+
pub fn into_inner(self) -> wasmtime::Store<T> {
54+
self.inner
55+
}
5156
}
5257

5358
impl<T: 'static> AsRef<wasmtime::Store<T>> for Store<T> {

crates/factors-executor/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,18 @@ impl<T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'_, T, U> {
260260
let instance = self.instance_pre.instantiate_async(&mut store).await?;
261261
Ok((instance, store))
262262
}
263+
264+
pub fn instantiate_store(
265+
self,
266+
executor_instance_state: U,
267+
) -> anyhow::Result<spin_core::Store<InstanceState<T::InstanceState, U>>> {
268+
let instance_state = InstanceState {
269+
core: Default::default(),
270+
factors: self.factors.build_instance_state(self.factor_builders)?,
271+
executor: executor_instance_state,
272+
};
273+
self.store_builder.build(instance_state)
274+
}
263275
}
264276

265277
/// InstanceState is the [`spin_core::Store`] `data` for an instance.

crates/http/src/trigger.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use spin_factor_outbound_http::wasi_2023_11_10::ProxyIndices as ProxyIndices2023
44
use wasmtime::component::InstancePre;
55
use wasmtime_wasi::p2::bindings::CommandIndices;
66
use wasmtime_wasi_http::bindings::ProxyIndices;
7-
use wasmtime_wasi_http::p3::bindings::ProxyIndices as P3ProxyIndices;
7+
use wasmtime_wasi_http::handler::{HandlerState, ProxyHandler, ProxyPre};
8+
use wasmtime_wasi_http::p3::bindings::ProxyPre as P3ProxyPre;
89

910
use crate::config::HttpExecutorType;
1011

@@ -21,11 +22,11 @@ pub fn default_base() -> String {
2122
}
2223

2324
/// The type of http handler export used by a component.
24-
pub enum HandlerType {
25+
pub enum HandlerType<S: HandlerState> {
2526
Spin,
2627
Wagi(CommandIndices),
2728
Wasi0_2(ProxyIndices),
28-
Wasi0_3(P3ProxyIndices),
29+
Wasi0_3(ProxyHandler<S>),
2930
Wasi2023_11_10(ProxyIndices2023_11_10),
3031
Wasi2023_10_18(ProxyIndices2023_10_18),
3132
}
@@ -41,15 +42,18 @@ const WASI_HTTP_EXPORT_0_3_0_RC_2025_09_16: &str = "wasi:http/[email protected]
4142
/// The `inbound-http` export for `fermyon:spin`
4243
const SPIN_HTTP_EXPORT: &str = "fermyon:spin/inbound-http";
4344

44-
impl HandlerType {
45+
impl<T, S: HandlerState<StoreData = T>> HandlerType<S> {
4546
/// Determine the handler type from the exports of a component.
46-
pub fn from_instance_pre<T>(pre: &InstancePre<T>) -> anyhow::Result<HandlerType> {
47+
pub fn from_instance_pre(pre: &InstancePre<T>, handler_state: S) -> anyhow::Result<Self> {
4748
let mut candidates = Vec::new();
4849
if let Ok(indices) = ProxyIndices::new(pre) {
4950
candidates.push(HandlerType::Wasi0_2(indices));
5051
}
51-
if let Ok(indices) = P3ProxyIndices::new(pre) {
52-
candidates.push(HandlerType::Wasi0_3(indices));
52+
if let Ok(pre) = P3ProxyPre::new(pre.clone()) {
53+
candidates.push(HandlerType::Wasi0_3(ProxyHandler::new(
54+
handler_state,
55+
ProxyPre::P3(pre),
56+
)));
5357
}
5458
if let Ok(indices) = ProxyIndices2023_10_18::new(pre) {
5559
candidates.push(HandlerType::Wasi2023_10_18(indices));

crates/trigger-http/src/server.rs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{
44
io::{ErrorKind, IsTerminal},
55
net::SocketAddr,
66
sync::Arc,
7+
time::Duration,
78
};
89

910
use anyhow::{bail, Context};
@@ -23,6 +24,7 @@ use hyper_util::{
2324
use spin_app::{APP_DESCRIPTION_KEY, APP_NAME_KEY};
2425
use spin_factor_outbound_http::{OutboundHttpFactor, SelfRequestOrigin};
2526
use spin_factors::RuntimeFactors;
27+
use spin_factors_executor::InstanceState;
2628
use spin_http::{
2729
app_info::AppInfo,
2830
body,
@@ -38,6 +40,7 @@ use tokio::{
3840
use tracing::Instrument;
3941
use wasmtime_wasi::p2::bindings::CommandIndices;
4042
use wasmtime_wasi_http::body::HyperOutgoingBody;
43+
use wasmtime_wasi_http::handler::{HandlerState, StoreBundle};
4144

4245
use crate::{
4346
headers::strip_forbidden_headers,
@@ -63,11 +66,11 @@ pub struct HttpServer<F: RuntimeFactors> {
6366
/// Request router.
6467
router: Router,
6568
/// The app being triggered.
66-
trigger_app: TriggerApp<F>,
69+
trigger_app: Arc<TriggerApp<F>>,
6770
// Component ID -> component trigger config
6871
component_trigger_configs: HashMap<spin_http::routes::TriggerLookupKey, HttpTriggerConfig>,
6972
// Component ID -> handler type
70-
component_handler_types: HashMap<String, HandlerType>,
73+
component_handler_types: HashMap<String, HandlerType<HttpHandlerState<F>>>,
7174
}
7275

7376
impl<F: RuntimeFactors> HttpServer<F> {
@@ -119,6 +122,8 @@ impl<F: RuntimeFactors> HttpServer<F> {
119122
// Now that router is built we can merge duplicate routes by component
120123
let component_trigger_configs = HashMap::from_iter(component_trigger_configs);
121124

125+
let trigger_app = Arc::new(trigger_app);
126+
122127
let component_handler_types = component_trigger_configs
123128
.iter()
124129
.filter_map(|(key, trigger_config)| match key {
@@ -145,14 +150,20 @@ impl<F: RuntimeFactors> HttpServer<F> {
145150
}
146151

147152
fn handler_type_for_component(
148-
trigger_app: &TriggerApp<F>,
153+
trigger_app: &Arc<TriggerApp<F>>,
149154
component_id: &str,
150155
executor: &Option<HttpExecutorType>,
151-
) -> anyhow::Result<HandlerType> {
156+
) -> anyhow::Result<HandlerType<HttpHandlerState<F>>> {
152157
let pre = trigger_app.get_instance_pre(component_id)?;
153158
let handler_type = match executor {
154159
None | Some(HttpExecutorType::Http) | Some(HttpExecutorType::Wasip3Unstable) => {
155-
let handler_type = HandlerType::from_instance_pre(pre)?;
160+
let handler_type = HandlerType::from_instance_pre(
161+
pre,
162+
HttpHandlerState {
163+
trigger_app: trigger_app.clone(),
164+
component_id: component_id.into(),
165+
},
166+
)?;
156167
handler_type.validate_executor(executor)?;
157168
handler_type
158169
}
@@ -330,12 +341,16 @@ impl<F: RuntimeFactors> HttpServer<F> {
330341
)
331342
.await
332343
}
333-
(None, Some(static_response)) => {
334-
Self::respond_static_response(static_response)
335-
},
344+
(None, Some(static_response)) => Self::respond_static_response(static_response),
336345
// These error cases should have been ruled out by this point but belt and braces
337-
(None, None) => Err(anyhow::anyhow!("Triggers must specify either component or static_response - neither is specified for {}", route_match.raw_route())),
338-
(Some(_), Some(_)) => Err(anyhow::anyhow!("Triggers must specify either component or static_response - both are specified for {}", route_match.raw_route())),
346+
(None, None) => Err(anyhow::anyhow!(
347+
"Triggers must specify either component or static_response - neither is specified for {}",
348+
route_match.raw_route()
349+
)),
350+
(Some(_), Some(_)) => Err(anyhow::anyhow!(
351+
"Triggers must specify either component or static_response - both are specified for {}",
352+
route_match.raw_route()
353+
)),
339354
}
340355
}
341356

@@ -377,9 +392,9 @@ impl<F: RuntimeFactors> HttpServer<F> {
377392
.execute(instance_builder, &route_match, req, client_addr)
378393
.await
379394
}
380-
HandlerType::Wasi0_3(indices) => {
381-
Wasip3HttpExecutor(indices)
382-
.execute(instance_builder, &route_match, req, client_addr)
395+
HandlerType::Wasi0_3(handler) => {
396+
Wasip3HttpExecutor(handler)
397+
.execute(&route_match, req, client_addr)
383398
.await
384399
}
385400
HandlerType::Wasi0_2(_)
@@ -607,3 +622,47 @@ pub(crate) trait HttpExecutor {
607622
client_addr: SocketAddr,
608623
) -> impl Future<Output = anyhow::Result<Response<Body>>>;
609624
}
625+
626+
pub(crate) struct HttpHandlerState<F: RuntimeFactors> {
627+
trigger_app: Arc<TriggerApp<F>>,
628+
component_id: String,
629+
}
630+
631+
impl<F: RuntimeFactors> HandlerState for HttpHandlerState<F> {
632+
type StoreData = InstanceState<F::InstanceState, ()>;
633+
634+
fn new_store(&self, _req_id: Option<u64>) -> anyhow::Result<StoreBundle<Self::StoreData>> {
635+
Ok(StoreBundle {
636+
store: self
637+
.trigger_app
638+
.prepare(&self.component_id)?
639+
.instantiate_store(())?
640+
.into_inner(),
641+
write_profile: Box::new(|_| ()),
642+
})
643+
}
644+
645+
fn request_timeout(&self) -> Duration {
646+
// TODO: Make this configurable
647+
Duration::MAX
648+
}
649+
650+
fn idle_instance_timeout(&self) -> Duration {
651+
// TODO: Make this configurable
652+
Duration::from_secs(1)
653+
}
654+
655+
fn max_instance_reuse_count(&self) -> usize {
656+
// TODO: Make this configurable
657+
128
658+
}
659+
660+
fn max_instance_concurrent_reuse_count(&self) -> usize {
661+
// TODO: Make this configurable
662+
16
663+
}
664+
665+
fn handle_worker_error(&self, error: anyhow::Error) {
666+
tracing::warn!("worker error: {error:?}")
667+
}
668+
}

crates/trigger-http/src/wasi.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use spin_http::trigger::HandlerType;
1313
use tokio::{sync::oneshot, task};
1414
use tracing::{instrument, Instrument, Level};
1515
use wasmtime_wasi_http::bindings::http::types::Scheme;
16-
use wasmtime_wasi_http::{bindings::Proxy, body::HyperIncomingBody as Body, WasiHttpView};
16+
use wasmtime_wasi_http::{
17+
bindings::Proxy, body::HyperIncomingBody as Body, handler::HandlerState, WasiHttpView,
18+
};
1719

1820
use crate::{headers::prepare_request_headers, server::HttpExecutor, TriggerInstanceBuilder};
1921

@@ -46,11 +48,11 @@ pub(super) fn prepare_request(
4648
}
4749

4850
/// An [`HttpExecutor`] that uses the `wasi:http/incoming-handler` interface.
49-
pub struct WasiHttpExecutor<'a> {
50-
pub handler_type: &'a HandlerType,
51+
pub struct WasiHttpExecutor<'a, S: HandlerState> {
52+
pub handler_type: &'a HandlerType<S>,
5153
}
5254

53-
impl HttpExecutor for WasiHttpExecutor<'_> {
55+
impl<S: HandlerState> HttpExecutor for WasiHttpExecutor<'_, S> {
5456
#[instrument(name = "spin_trigger_http.execute_wasm", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wasm_component {}", route_match.lookup_key().to_string())))]
5557
async fn execute<F: RuntimeFactors>(
5658
&self,

crates/trigger-http/src/wasip3.rs

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{server::HttpExecutor, TriggerInstanceBuilder};
1+
use crate::server::HttpHandlerState;
22
use anyhow::{Context as _, Result};
33
use futures::{channel::oneshot, FutureExt};
44
use http_body_util::BodyExt;
@@ -7,50 +7,51 @@ use spin_factors::RuntimeFactors;
77
use spin_factors_executor::InstanceState;
88
use spin_http::routes::RouteMatch;
99
use std::net::SocketAddr;
10-
use tokio::task;
1110
use tracing::{instrument, Instrument, Level};
12-
use wasmtime::AsContextMut;
11+
use wasmtime::component::Accessor;
1312
use wasmtime_wasi_http::{
1413
body::HyperIncomingBody as Body,
15-
p3::{
16-
bindings::{http::types, ProxyIndices},
17-
WasiHttpCtxView,
18-
},
14+
handler::{Proxy, ProxyHandler},
15+
p3::{bindings::http::types, WasiHttpCtxView},
1916
};
2017

2118
/// An [`HttpExecutor`] that uses the `wasi:[email protected].*/handler` interface.
22-
pub(super) struct Wasip3HttpExecutor<'a>(pub(super) &'a ProxyIndices);
19+
pub(super) struct Wasip3HttpExecutor<'a, F: RuntimeFactors>(
20+
pub(super) &'a ProxyHandler<HttpHandlerState<F>>,
21+
);
2322

24-
impl HttpExecutor for Wasip3HttpExecutor<'_> {
23+
impl<F: RuntimeFactors> Wasip3HttpExecutor<'_, F> {
2524
#[instrument(name = "spin_trigger_http.execute_wasm", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wasm_component {}", route_match.lookup_key().to_string())))]
26-
async fn execute<F: RuntimeFactors>(
25+
pub async fn execute(
2726
&self,
28-
instance_builder: TriggerInstanceBuilder<'_, F>,
2927
route_match: &RouteMatch<'_, '_>,
3028
mut req: http::Request<Body>,
3129
client_addr: SocketAddr,
3230
) -> Result<http::Response<Body>> {
3331
super::wasi::prepare_request(route_match, &mut req, client_addr)?;
3432

35-
let (instance, mut store) = instance_builder.instantiate(()).await?;
36-
3733
let getter = (|data| wasi_http::<F>(data).unwrap())
3834
as fn(&mut InstanceState<F::InstanceState, ()>) -> WasiHttpCtxView<'_>;
3935

4036
let (request, body) = req.into_parts();
4137
let body = body.map_err(spin_factor_outbound_http::p2_to_p3_error_code);
4238
let request = http::Request::from_parts(request, body);
4339
let (request, request_io_result) = types::Request::from_http(request);
44-
let request = wasi_http::<F>(store.data_mut())?.table.push(request)?;
45-
46-
let guest = self.0.load(&mut store, &instance)?;
4740

4841
let (tx, rx) = oneshot::channel();
49-
task::spawn(
50-
async move {
51-
store
52-
.as_context_mut()
53-
.run_concurrent(async move |store| {
42+
self.0.spawn(
43+
None,
44+
Box::new(move |store: &Accessor<_>, guest: &Proxy| {
45+
Box::pin(
46+
async move {
47+
let Proxy::P3(guest) = guest else {
48+
unreachable!();
49+
};
50+
51+
let request = store.with(|mut store| {
52+
anyhow::Ok(wasi_http::<F>(store.data_mut())?.table.push(request)?)
53+
})?;
54+
5455
let (response, task) = guest
5556
.wasi_http_handler()
5657
.call_handle(store, request)
@@ -67,14 +68,14 @@ impl HttpExecutor for Wasip3HttpExecutor<'_> {
6768
task.block(store).await;
6869

6970
anyhow::Ok(())
70-
})
71-
.await?
72-
}
73-
.in_current_span()
74-
.inspect(|result| {
75-
if let Err(error) = result {
76-
tracing::error!("Component error handling request: {error:?}");
77-
}
71+
}
72+
.in_current_span()
73+
.map(|result| {
74+
if let Err(error) = result {
75+
tracing::error!("Component error handling request: {error:?}");
76+
}
77+
}),
78+
)
7879
}),
7980
);
8081

0 commit comments

Comments
 (0)