@@ -12,13 +12,20 @@ mod wasip3;
1212
1313use std::{
1414 error::Error,
15+ fmt::Display,
1516 net::{Ipv4Addr, SocketAddr, ToSocketAddrs},
1617 path::PathBuf,
18+ str::FromStr,
1719 sync::Arc,
20+ time::Duration,
1821};
1922
20- use anyhow::{bail, Context };
23+ use anyhow::{Context, bail };
2124use clap::Args;
25+ use rand::{
26+ RngCore,
27+ distr::uniform::{SampleRange, SampleUniform},
28+ };
2229use serde::Deserialize;
2330use spin_app::App;
2431use spin_factors::RuntimeFactors;
@@ -31,6 +38,9 @@ pub use tls::TlsConfig;
3138
3239pub(crate) use wasmtime_wasi_http::body::HyperIncomingBody as Body;
3340
41+ const DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT: usize = 128;
42+ const DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT: usize = 16;
43+
3444/// A [`spin_trigger::TriggerApp`] for the HTTP trigger.
3545pub(crate) type TriggerApp<F> = spin_trigger::TriggerApp<HttpTrigger, F>;
3646
@@ -58,6 +68,59 @@ pub struct CliArgs {
5868
5969 #[clap(long = "find-free-port")]
6070 pub find_free_port: bool,
71+
72+ /// Maximum number of requests to send to a single component instance before
73+ /// dropping it.
74+ ///
75+ /// This defaults to 1 for WASIp2 components and 128 for WASIp3 components.
76+ /// As of this writing, setting it to more than 1 will have no effect for
77+ /// WASIp2 components, but that may change in the future.
78+ ///
79+ /// This may be specified either as an integer value or as a range,
80+ /// e.g. 1..8. If it's a range, a number will be selected from that range
81+ /// at random for each new instance.
82+ #[clap(long, value_parser = parse_usize_range)]
83+ max_instance_reuse_count: Option<Range<usize>>,
84+
85+ /// Maximum number of concurrent requests to send to a single component
86+ /// instance.
87+ ///
88+ /// This defaults to 1 for WASIp2 components and 16 for WASIp3 components.
89+ /// Note that setting it to more than 1 will have no effect for WASIp2
90+ /// components since they cannot be called concurrently.
91+ ///
92+ /// This may be specified either as an integer value or as a range,
93+ /// e.g. 1..8. If it's a range, a number will be selected from that range
94+ /// at random for each new instance.
95+ #[clap(long, value_parser = parse_usize_range)]
96+ max_instance_concurrent_reuse_count: Option<Range<usize>>,
97+
98+ /// Request timeout to enforce.
99+ ///
100+ /// As of this writing, this only affects WASIp3 components.
101+ ///
102+ /// A number with no suffix or with an `s` suffix is interpreted as seconds;
103+ /// other accepted suffixes include `ms` (milliseconds), `us` or `μs`
104+ /// (microseconds), and `ns` (nanoseconds).
105+ ///
106+ /// This may be specified either as a single time value or as a range,
107+ /// e.g. 1..8s. If it's a range, a value will be selected from that range
108+ /// at random for each new instance.
109+ #[clap(long, value_parser = parse_duration_range)]
110+ request_timeout: Option<Range<Duration>>,
111+
112+ /// Time to hold an idle component instance for possible reuse before
113+ /// dropping it.
114+ ///
115+ /// A number with no suffix or with an `s` suffix is interpreted as seconds;
116+ /// other accepted suffixes include `ms` (milliseconds), `us` or `μs`
117+ /// (microseconds), and `ns` (nanoseconds).
118+ ///
119+ /// This may be specified either as a single time value or as a range,
120+ /// e.g. 1..8s. If it's a range, a value will be selected from that range
121+ /// at random for each new instance.
122+ #[clap(long, default_value = "1s", value_parser = parse_duration_range)]
123+ idle_instance_timeout: Range<Duration>,
61124}
62125
63126impl CliArgs {
@@ -73,6 +136,99 @@ impl CliArgs {
73136 }
74137}
75138
139+ #[derive(Copy, Clone)]
140+ enum Range<T> {
141+ Value(T),
142+ Bounds(T, T),
143+ }
144+
145+ impl<T> Range<T> {
146+ fn map<V>(self, fun: impl Fn(T) -> V) -> Range<V> {
147+ match self {
148+ Self::Value(v) => Range::Value(fun(v)),
149+ Self::Bounds(a, b) => Range::Bounds(fun(a), fun(b)),
150+ }
151+ }
152+ }
153+
154+ impl<T: SampleUniform + PartialOrd> SampleRange<T> for Range<T> {
155+ fn sample_single<R: RngCore + ?Sized>(
156+ self,
157+ rng: &mut R,
158+ ) -> Result<T, rand::distr::uniform::Error> {
159+ match self {
160+ Self::Value(v) => Ok(v),
161+ Self::Bounds(a, b) => (a..b).sample_single(rng),
162+ }
163+ }
164+
165+ fn is_empty(&self) -> bool {
166+ match self {
167+ Self::Value(_) => false,
168+ Self::Bounds(a, b) => (a..b).is_empty(),
169+ }
170+ }
171+ }
172+
173+ fn parse_range<T: FromStr>(s: &str) -> Result<Range<T>, String>
174+ where
175+ T::Err: Display,
176+ {
177+ let error = |e| format!("expected integer or range; got {s:?}; {e}");
178+ if let Some((start, end)) = s.split_once("..") {
179+ Ok(Range::Bounds(
180+ start.parse().map_err(error)?,
181+ end.parse().map_err(error)?,
182+ ))
183+ } else {
184+ Ok(Range::Value(s.parse().map_err(error)?))
185+ }
186+ }
187+
188+ fn parse_usize_range(s: &str) -> Result<Range<usize>, String> {
189+ parse_range(s)
190+ }
191+
192+ struct ParsedDuration(Duration);
193+
194+ impl FromStr for ParsedDuration {
195+ type Err = String;
196+
197+ fn from_str(s: &str) -> Result<Self, Self::Err> {
198+ let error = |e| {
199+ format!("expected integer suffixed by `s`, `ms`, `us`, `μs`, or `ns`; got {s:?}; {e}")
200+ };
201+ Ok(Self(match s.parse() {
202+ Ok(val) => Duration::from_secs(val),
203+ Err(err) => {
204+ if let Some(num) = s.strip_suffix("s") {
205+ Duration::from_secs(num.parse().map_err(error)?)
206+ } else if let Some(num) = s.strip_suffix("ms") {
207+ Duration::from_millis(num.parse().map_err(error)?)
208+ } else if let Some(num) = s.strip_suffix("us").or(s.strip_suffix("μs")) {
209+ Duration::from_micros(num.parse().map_err(error)?)
210+ } else if let Some(num) = s.strip_suffix("ns") {
211+ Duration::from_nanos(num.parse().map_err(error)?)
212+ } else {
213+ return Err(error(err));
214+ }
215+ }
216+ }))
217+ }
218+ }
219+
220+ fn parse_duration_range(s: &str) -> Result<Range<Duration>, String> {
221+ parse_range::<ParsedDuration>(s).map(|v| v.map(|v| v.0))
222+ }
223+
224+ #[derive(Clone, Copy)]
225+ pub struct InstanceReuseConfig {
226+ max_instance_reuse_count: Range<usize>,
227+ max_instance_concurrent_reuse_count: Range<usize>,
228+ request_timeout: Option<Range<Duration>>,
229+ idle_instance_timeout: Range<Duration>,
230+ }
231+
76232/// The Spin HTTP trigger.
77233pub struct HttpTrigger {
78234 /// The address the server should listen on.
@@ -83,6 +239,7 @@ pub struct HttpTrigger {
83239 tls_config: Option<TlsConfig>,
84240 find_free_port: bool,
85241 http1_max_buf_size: Option<usize>,
242+ reuse_config: InstanceReuseConfig,
86243}
87244
88245impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
@@ -94,13 +251,26 @@ impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
94251 fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
95252 let find_free_port = cli_args.find_free_port;
96253 let http1_max_buf_size = cli_args.http1_max_buf_size;
254+ let reuse_config = InstanceReuseConfig {
255+ max_instance_reuse_count: cli_args
256+ .max_instance_reuse_count
257+ .unwrap_or(Range::Value(DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT)),
258+ max_instance_concurrent_reuse_count: cli_args
259+ .max_instance_concurrent_reuse_count
260+ .unwrap_or(Range::Value(
261+ DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT,
262+ )),
263+ request_timeout: cli_args.request_timeout,
264+ idle_instance_timeout: cli_args.idle_instance_timeout,
265+ };
97266
98267 Self::new(
99268 app,
100269 cli_args.address,
101270 cli_args.into_tls_config(),
102271 find_free_port,
103272 http1_max_buf_size,
273+ reuse_config,
104274 )
105275 }
106276
@@ -125,6 +295,7 @@ impl HttpTrigger {
125295 tls_config: Option<TlsConfig>,
126296 find_free_port: bool,
127297 http1_max_buf_size: Option<usize>,
298+ reuse_config: InstanceReuseConfig,
128299 ) -> anyhow::Result<Self> {
129300 Self::validate_app(app)?;
130301
@@ -133,6 +304,7 @@ impl HttpTrigger {
133304 tls_config,
134305 find_free_port,
135306 http1_max_buf_size,
307+ reuse_config,
136308 })
137309 }
138310
@@ -146,13 +318,15 @@ impl HttpTrigger {
146318 tls_config,
147319 find_free_port,
148320 http1_max_buf_size,
321+ reuse_config,
149322 } = self;
150323 let server = Arc::new(HttpServer::new(
151324 listen_addr,
152325 tls_config,
153326 find_free_port,
154327 trigger_app,
155328 http1_max_buf_size,
329+ reuse_config,
156330 )?);
157331 Ok(server)
158332 }
0 commit comments