diff --git a/crates/factors-executor/src/lib.rs b/crates/factors-executor/src/lib.rs index dce2b32594..3e9aa844dc 100644 --- a/crates/factors-executor/src/lib.rs +++ b/crates/factors-executor/src/lib.rs @@ -203,12 +203,12 @@ impl FactorsExecutorApp { /// /// It is generic over the executor's [`RuntimeFactors`] and any ad-hoc additional /// per-instance state needed by the caller. -pub struct FactorsInstanceBuilder<'a, F: RuntimeFactors, U: 'static> { +pub struct FactorsInstanceBuilder<'a, T: RuntimeFactors, U: 'static> { app_component: AppComponent<'a>, store_builder: spin_core::StoreBuilder, - factor_builders: F::InstanceBuilders, - instance_pre: &'a InstancePre, - factors: &'a F, + factor_builders: T::InstanceBuilders, + instance_pre: &'a InstancePre, + factors: &'a T, } impl FactorsInstanceBuilder<'_, T, U> { @@ -244,7 +244,7 @@ impl FactorsInstanceBuilder<'_, T, U> { } impl FactorsInstanceBuilder<'_, T, U> { - /// Instantiates the instance with the given executor instance state + /// Instantiates the instance with the given executor instance state. pub async fn instantiate( self, executor_instance_state: U, @@ -252,16 +252,13 @@ impl FactorsInstanceBuilder<'_, T, U> { spin_core::Instance, spin_core::Store>, )> { - let instance_state = InstanceState { - core: Default::default(), - factors: self.factors.build_instance_state(self.factor_builders)?, - executor: executor_instance_state, - cpu_time_elapsed: Duration::from_millis(0), - cpu_time_last_entry: None, - memory_used_on_init: 0, - component_id: self.app_component.id().into(), - }; - let mut store = self.store_builder.build(instance_state)?; + let mut store = Self::build_store( + self.store_builder, + self.factor_builders, + &self.app_component, + self.factors, + executor_instance_state, + )?; #[cfg(feature = "cpu-time-metrics")] store.as_mut().call_hook(|mut store, hook| { @@ -277,20 +274,38 @@ impl FactorsInstanceBuilder<'_, T, U> { Ok((instance, store)) } + /// Builds a store with the given executor instance state. pub fn instantiate_store( self, executor_instance_state: U, ) -> anyhow::Result>> { + Self::build_store( + self.store_builder, + self.factor_builders, + &self.app_component, + self.factors, + executor_instance_state, + ) + } + + fn build_store( + store_builder: spin_core::StoreBuilder, + factor_builders: T::InstanceBuilders, + app_component: &AppComponent, + factors: &T, + executor_instance_state: U, + ) -> anyhow::Result>> { + let factors = factors.build_instance_state(factor_builders)?; let instance_state = InstanceState { core: Default::default(), - factors: self.factors.build_instance_state(self.factor_builders)?, + factors, executor: executor_instance_state, cpu_time_elapsed: Duration::from_millis(0), cpu_time_last_entry: None, memory_used_on_init: 0, - component_id: self.app_component.id().into(), + component_id: app_component.id().into(), }; - self.store_builder.build(instance_state) + store_builder.build(instance_state) } } diff --git a/crates/http/src/trigger.rs b/crates/http/src/trigger.rs index 6737ed9d11..31e68b9fc8 100644 --- a/crates/http/src/trigger.rs +++ b/crates/http/src/trigger.rs @@ -20,23 +20,25 @@ pub fn default_base() -> String { } /// The type of http handler export used by a component. +// Note: These are ordered by descending newness, reflecting the selection order +// in from_instance_pre below. pub enum HandlerType { - Spin, - Wagi(CommandIndices), - Wasi0_2(ProxyIndices), Wasi0_3(ServiceIndices, ProxyHandler), + Wasi0_2(ProxyIndices), Wasi2023_11_10(ProxyIndices2023_11_10), Wasi2023_10_18(ProxyIndices2023_10_18), + Spin, + Wagi(CommandIndices), } -/// The `incoming-handler` export for `wasi:http` version rc-2023-10-18 -const WASI_HTTP_EXPORT_2023_10_18: &str = "wasi:http/incoming-handler@0.2.0-rc-2023-10-18"; -/// The `incoming-handler` export for `wasi:http` version rc-2023-11-10 -const WASI_HTTP_EXPORT_2023_11_10: &str = "wasi:http/incoming-handler@0.2.0-rc-2023-11-10"; -/// The `incoming-handler` export prefix for all `wasi:http` 0.2 versions -const WASI_HTTP_EXPORT_0_2_PREFIX: &str = "wasi:http/incoming-handler@0.2"; /// The `handler` export `wasi:http` version 0.3.0-rc-2025-08-15 const WASI_HTTP_EXPORT_0_3_0_RC_03_15: &str = "wasi:http/handler@0.3.0-rc-2026-03-15"; +/// The `incoming-handler` export prefix for all `wasi:http` 0.2 versions +const WASI_HTTP_EXPORT_0_2_PREFIX: &str = "wasi:http/incoming-handler@0.2"; +/// The `incoming-handler` export for `wasi:http` version rc-2023-11-10 +const WASI_HTTP_EXPORT_2023_11_10: &str = "wasi:http/incoming-handler@0.2.0-rc-2023-11-10"; +/// The `incoming-handler` export for `wasi:http` version rc-2023-10-18 +const WASI_HTTP_EXPORT_2023_10_18: &str = "wasi:http/incoming-handler@0.2.0-rc-2023-10-18"; /// The `inbound-http` export for `fermyon:spin` const SPIN_HTTP_EXPORT: &str = "fermyon:spin/inbound-http"; @@ -44,9 +46,6 @@ impl> HandlerType { /// Determine the handler type from the exports of a component. pub fn from_instance_pre(pre: &InstancePre, handler_state: S) -> anyhow::Result { let mut candidates = Vec::new(); - if let Ok(indices) = ProxyIndices::new(pre) { - candidates.push(HandlerType::Wasi0_2(indices)); - } if let Ok(pre) = ServicePre::new(pre.clone()) { candidates.push(HandlerType::Wasi0_3( // We `.unwrap()` here because the `Ok(_)` result from @@ -56,12 +55,15 @@ impl> HandlerType { ProxyHandler::new(handler_state, ProxyPre::P3(pre)), )); } - if let Ok(indices) = ProxyIndices2023_10_18::new(pre) { - candidates.push(HandlerType::Wasi2023_10_18(indices)); + if let Ok(indices) = ProxyIndices::new(pre) { + candidates.push(HandlerType::Wasi0_2(indices)); } if let Ok(indices) = ProxyIndices2023_11_10::new(pre) { candidates.push(HandlerType::Wasi2023_11_10(indices)); } + if let Ok(indices) = ProxyIndices2023_10_18::new(pre) { + candidates.push(HandlerType::Wasi2023_10_18(indices)); + } if pre .component() .get_export_index(None, SPIN_HTTP_EXPORT) @@ -71,6 +73,7 @@ impl> HandlerType { } match candidates.len() { + 1 => Ok(candidates.pop().unwrap()), 0 => { anyhow::bail!( "Expected component to export one of \ @@ -83,10 +86,8 @@ impl> HandlerType { If you're sure this is an HTTP module, check if a Spin upgrade is available: this may handle the newer version." ) } - 1 => Ok(candidates.pop().unwrap()), - _ => anyhow::bail!( - "component exports multiple different handlers but \ - it's expected to export only one" + too_many => anyhow::bail!( + "component exports {too_many} different handlers but it's expected to export only one" ), } } diff --git a/crates/trigger-http/src/server.rs b/crates/trigger-http/src/server.rs index 69120b40de..fdd2eb9695 100644 --- a/crates/trigger-http/src/server.rs +++ b/crates/trigger-http/src/server.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - future::Future, io::{ErrorKind, IsTerminal}, net::SocketAddr, sync::{Arc, OnceLock}, @@ -180,9 +179,10 @@ impl HttpServer { None | Some(HttpExecutorType::Http) => HandlerType::from_instance_pre( pre, HttpHandlerState { - trigger_app: trigger_app.clone(), component_id: component_id.into(), reuse_config, + server: Default::default(), + self_scheme: Default::default(), }, )?, Some(HttpExecutorType::Wagi(wagi_config)) => { @@ -329,7 +329,7 @@ impl HttpServer { server_scheme: Scheme, client_addr: SocketAddr, ) -> anyhow::Result> { - set_req_uri(&mut req, server_scheme.clone())?; + set_req_uri(&mut req, server_scheme)?; let app_id = self .trigger_app .app() @@ -355,7 +355,6 @@ impl HttpServer { self.respond_wasm_component( req, route_match, - server_scheme, client_addr, component, &trigger_config.executor, @@ -383,28 +382,10 @@ impl HttpServer { self: &Arc, req: Request, route_match: RouteMatch<'_, '_>, - server_scheme: Scheme, client_addr: SocketAddr, component_id: &str, executor: &Option, ) -> anyhow::Result> { - let mut instance_builder = self.trigger_app.prepare(component_id)?; - - // Set up outbound HTTP request origin and service chaining - // The outbound HTTP factor is required since both inbound and outbound wasi HTTP - // implementations assume they use the same underlying wasmtime resource storage. - // Eventually, we may be able to factor this out to a separate factor. - let outbound_http = instance_builder - .factor_builder::() - .context( - "The wasi HTTP trigger was configured without the required wasi outbound http support", - )?; - - let self_addr = self.get_local_addr(); - let origin = SelfRequestOrigin::create(server_scheme, &self_addr.to_string())?; - outbound_http.set_self_request_origin(origin); - outbound_http.set_request_interceptor(OutboundHttpInterceptor::new(self.clone()))?; - // Prepare HTTP executor let handler_type = self .component_handler_types @@ -414,21 +395,21 @@ impl HttpServer { let res = match executor { HttpExecutorType::Http => match handler_type { - HandlerType::Spin => { - SpinHttpExecutor - .execute(instance_builder, &route_match, req, client_addr) - .await - } HandlerType::Wasi0_3(_, handler) => { Wasip3HttpExecutor(handler) - .execute(&route_match, req, client_addr) + .execute(self, &route_match, req, client_addr) .await } HandlerType::Wasi0_2(_) | HandlerType::Wasi2023_11_10(_) | HandlerType::Wasi2023_10_18(_) => { WasiHttpExecutor { handler_type } - .execute(instance_builder, &route_match, req, client_addr) + .execute(self, &route_match, req, client_addr, component_id) + .await + } + HandlerType::Spin => { + SpinHttpExecutor + .execute(self, &route_match, req, client_addr, component_id) .await } HandlerType::Wagi(_) => unreachable!(), @@ -443,7 +424,7 @@ impl HttpServer { indices, }; executor - .execute(instance_builder, &route_match, req, client_addr) + .execute(self, &route_match, req, client_addr, component_id) .await } }; @@ -460,6 +441,31 @@ impl HttpServer { } } + pub(crate) fn trigger_instance_builder( + self: &'_ Arc, + component_id: &str, + self_scheme: Option<&Scheme>, + ) -> anyhow::Result> { + let mut instance_builder = self.trigger_app.prepare(component_id)?; + + // Set up outbound HTTP request origin and service chaining + // The outbound HTTP factor is required since both inbound and outbound wasi HTTP + // implementations assume they use the same underlying wasmtime resource storage. + // Eventually, we may be able to factor this out to a separate factor. + let outbound_http = instance_builder + .factor_builder::() + .context( + "The wasi HTTP trigger was configured without the required wasi outbound http support", + )?; + + let self_scheme = self_scheme.cloned().unwrap_or(Scheme::HTTPS); + let self_addr = self.get_local_addr(); + let origin = SelfRequestOrigin::create(self_scheme, &self_addr.to_string())?; + outbound_http.set_self_request_origin(origin); + outbound_http.set_request_interceptor(OutboundHttpInterceptor::new(self.clone()))?; + Ok(instance_builder) + } + fn respond_static_response( sr: &spin_http::config::StaticResponse, ) -> anyhow::Result> { @@ -688,21 +694,20 @@ fn set_req_uri(req: &mut Request, scheme: Scheme) -> anyhow::Result<()> { Ok(()) } -/// An HTTP executor. -pub(crate) trait HttpExecutor { - fn execute( - &self, - instance_builder: TriggerInstanceBuilder, - route_match: &RouteMatch<'_, '_>, - req: Request, - client_addr: SocketAddr, - ) -> impl Future>>; -} - pub(crate) struct HttpHandlerState { - trigger_app: Arc>, component_id: String, reuse_config: InstanceReuseConfig, + server: OnceLock>>, + self_scheme: OnceLock, +} + +impl HttpHandlerState { + pub(crate) fn init_once(&self, server: &Arc>, first_uri: &Uri) { + self.server.get_or_init(|| server.clone()); + if let Some(scheme) = first_uri.scheme() { + self.self_scheme.get_or_init(|| scheme.clone()); + } + } } impl HandlerState for HttpHandlerState { @@ -711,8 +716,10 @@ impl HandlerState for HttpHandlerState { fn new_store(&self, _req_id: Option) -> wasmtime::Result> { Ok(StoreBundle { store: self - .trigger_app - .prepare(&self.component_id) + .server + .get() + .expect("server should have been set") + .trigger_instance_builder(&self.component_id, self.self_scheme.get()) .to_wasmtime_result()? .instantiate_store(()) .to_wasmtime_result()? diff --git a/crates/trigger-http/src/spin.rs b/crates/trigger-http/src/spin.rs index a2a8d02925..87d694f8ef 100644 --- a/crates/trigger-http/src/spin.rs +++ b/crates/trigger-http/src/spin.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, sync::Arc}; use anyhow::Result; use http_body_util::BodyExt; @@ -10,32 +10,30 @@ use spin_world::v1::http_types; use tracing::{Level, instrument}; use crate::{ - Body, TriggerInstanceBuilder, + Body, HttpServer, headers::{append_headers, prepare_request_headers}, - server::HttpExecutor, }; /// An [`HttpExecutor`] that uses the `fermyon:spin/inbound-http` interface. #[derive(Clone)] pub struct SpinHttpExecutor; -impl HttpExecutor for SpinHttpExecutor { +impl SpinHttpExecutor { #[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())))] - async fn execute( + pub async fn execute( &self, - instance_builder: TriggerInstanceBuilder<'_, F>, + server: &Arc>, route_match: &RouteMatch<'_, '_>, req: Request, client_addr: SocketAddr, + component_id: &str, ) -> Result> { - let spin_http::routes::TriggerLookupKey::Component(component_id) = route_match.lookup_key() - else { - unreachable!() - }; - tracing::trace!("Executing request using the Spin executor for component {component_id}"); - let (instance, mut store) = instance_builder.instantiate(()).await?; + let (instance, mut store) = server + .trigger_instance_builder(component_id, req.uri().scheme())? + .instantiate(()) + .await?; let headers = prepare_request_headers(&req, route_match, client_addr)?; // Expects here are safe since we have already checked that this diff --git a/crates/trigger-http/src/wagi.rs b/crates/trigger-http/src/wagi.rs index ee6dba5bc2..8900017a98 100644 --- a/crates/trigger-http/src/wagi.rs +++ b/crates/trigger-http/src/wagi.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor, net::SocketAddr}; +use std::{io::Cursor, net::SocketAddr, sync::Arc}; use anyhow::{Context, Result, ensure}; use http_body_util::BodyExt; @@ -11,31 +11,24 @@ use wasmtime_wasi::p2::bindings::CommandIndices; use wasmtime_wasi::p2::pipe::MemoryOutputPipe; use wasmtime_wasi_http::p2::body::HyperIncomingBody as Body; -use crate::{TriggerInstanceBuilder, headers::compute_default_headers, server::HttpExecutor}; +use crate::{HttpServer, headers::compute_default_headers}; pub struct WagiHttpExecutor<'a> { pub wagi_config: &'a WagiTriggerConfig, pub indices: &'a CommandIndices, } -impl HttpExecutor for WagiHttpExecutor<'_> { +impl WagiHttpExecutor<'_> { #[instrument(name = "spin_trigger_http.execute_wagi", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wagi_component {}", route_match.lookup_key().to_string())))] - async fn execute( + pub async fn execute( &self, - mut instance_builder: TriggerInstanceBuilder<'_, F>, + server: &Arc>, route_match: &RouteMatch<'_, '_>, req: Request, client_addr: SocketAddr, + component_id: &str, ) -> Result> { - let spin_http::routes::TriggerLookupKey::Component(component) = route_match.lookup_key() - else { - unreachable!() - }; - - tracing::trace!( - "Executing request using the Wagi executor for component {}", - component - ); + tracing::trace!("Executing request using the Wagi executor for component {component_id}"); let uri_path = req.uri().path(); @@ -79,6 +72,8 @@ impl HttpExecutor for WagiHttpExecutor<'_> { let stdout = MemoryOutputPipe::new(usize::MAX); + let mut instance_builder = + server.trigger_instance_builder(component_id, parts.uri.scheme())?; let wasi_builder = instance_builder .factor_builder::() .context("The wagi HTTP trigger was configured without the required wasi support")?; @@ -110,7 +105,7 @@ impl HttpExecutor for WagiHttpExecutor<'_> { let stdout = stdout.try_into_inner().unwrap(); ensure!( !stdout.is_empty(), - "The {component:?} component is configured to use the WAGI executor \ + "The {component_id:?} component is configured to use the WAGI executor \ but did not write to stdout. Check the `executor` in spin.toml." ); diff --git a/crates/trigger-http/src/wasi.rs b/crates/trigger-http/src/wasi.rs index ae8721983d..e6f3cb79b2 100644 --- a/crates/trigger-http/src/wasi.rs +++ b/crates/trigger-http/src/wasi.rs @@ -1,5 +1,6 @@ use std::io::IsTerminal; use std::net::SocketAddr; +use std::sync::Arc; use anyhow::{Context, Result, anyhow}; use futures::TryFutureExt; @@ -16,7 +17,8 @@ use wasmtime_wasi_http::handler::HandlerState; use wasmtime_wasi_http::p2::bindings::http::types::Scheme; use wasmtime_wasi_http::p2::{bindings::Proxy, body::HyperIncomingBody as Body}; -use crate::{TriggerInstanceBuilder, headers::prepare_request_headers, server::HttpExecutor}; +use crate::HttpServer; +use crate::headers::prepare_request_headers; pub(super) fn prepare_request( route_match: &RouteMatch<'_, '_>, @@ -51,18 +53,22 @@ pub struct WasiHttpExecutor<'a, S: HandlerState> { pub handler_type: &'a HandlerType, } -impl HttpExecutor for WasiHttpExecutor<'_, S> { +impl WasiHttpExecutor<'_, S> { #[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())))] - async fn execute( + pub async fn execute( &self, - instance_builder: TriggerInstanceBuilder<'_, F>, + server: &Arc>, route_match: &RouteMatch<'_, '_>, mut req: Request, client_addr: SocketAddr, + component_id: &str, ) -> Result> { prepare_request(route_match, &mut req, client_addr)?; - let (instance, mut store) = instance_builder.instantiate(()).await?; + let (instance, mut store) = server + .trigger_instance_builder(component_id, req.uri().scheme())? + .instantiate(()) + .await?; let mut wasi_http = spin_factor_outbound_http::OutboundHttpFactor::get_wasi_http_impl( store.data_mut().factors_instance_state_mut(), @@ -81,15 +87,16 @@ impl HttpExecutor for WasiHttpExecutor<'_, S> { } let handler = match self.handler_type { - HandlerType::Wasi2023_10_18(indices) => { - let guest = indices.load(&mut store, &instance)?; - Handler::Handler2023_10_18(guest) - } + HandlerType::Wasi0_2(indices) => Handler::Latest(indices.load(&mut store, &instance)?), HandlerType::Wasi2023_11_10(indices) => { let guest = indices.load(&mut store, &instance)?; Handler::Handler2023_11_10(guest) } - HandlerType::Wasi0_2(indices) => Handler::Latest(indices.load(&mut store, &instance)?), + HandlerType::Wasi2023_10_18(indices) => { + let guest = indices.load(&mut store, &instance)?; + Handler::Handler2023_10_18(guest) + } + HandlerType::Wasi0_3(_, _) => unreachable!("should have used Wasip3HttpExecutor"), HandlerType::Spin => unreachable!("should have used SpinHttpExecutor"), HandlerType::Wagi(_) => unreachable!("should have used WagiExecutor instead"), diff --git a/crates/trigger-http/src/wasip3.rs b/crates/trigger-http/src/wasip3.rs index a65e075902..e5793af684 100644 --- a/crates/trigger-http/src/wasip3.rs +++ b/crates/trigger-http/src/wasip3.rs @@ -1,4 +1,4 @@ -use crate::server::HttpHandlerState; +use crate::{HttpServer, server::HttpHandlerState}; use anyhow::{Context as _, Result}; use futures::{FutureExt, channel::oneshot}; use http_body_util::BodyExt; @@ -6,7 +6,7 @@ use spin_factor_outbound_http::{NotifyOnDropBody, p3_to_p2_error_code}; use spin_factors::RuntimeFactors; use spin_factors_executor::InstanceState; use spin_http::routes::RouteMatch; -use std::net::SocketAddr; +use std::{net::SocketAddr, sync::Arc}; use tracing::{Instrument, Level, instrument}; use wasmtime::component::Accessor; use wasmtime_wasi_http::{ @@ -24,10 +24,12 @@ impl Wasip3HttpExecutor<'_, F> { #[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())))] pub async fn execute( &self, + server: &Arc>, route_match: &RouteMatch<'_, '_>, mut req: http::Request, client_addr: SocketAddr, ) -> Result> { + self.0.state().init_once(server, req.uri()); super::wasi::prepare_request(route_match, &mut req, client_addr)?; let getter = (|data| wasi_http::(data).unwrap())