@@ -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
2023use anyhow:: { bail, Context } ;
2124use clap:: Args ;
25+ use rand:: {
26+ distr:: uniform:: { SampleRange , SampleUniform } ,
27+ RngCore ,
28+ } ;
2229use serde:: Deserialize ;
2330use spin_app:: App ;
2431use spin_factors:: RuntimeFactors ;
@@ -31,6 +38,11 @@ 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+ const DEFAULT_REQUEST_TIMEOUT : Option < Range < Duration > > = None ;
44+ const DEFAULT_IDLE_INSTANCE_TIMEOUT : Range < Duration > = Range :: Value ( Duration :: from_secs ( 1 ) ) ;
45+
3446/// A [`spin_trigger::TriggerApp`] for the HTTP trigger.
3547pub ( crate ) type TriggerApp < F > = spin_trigger:: TriggerApp < HttpTrigger , F > ;
3648
@@ -58,6 +70,59 @@ pub struct CliArgs {
5870
5971 #[ clap( long = "find-free-port" ) ]
6072 pub find_free_port : bool ,
73+
74+ /// Maximum number of requests to send to a single component instance before
75+ /// dropping it.
76+ ///
77+ /// This defaults to 1 for WASIp2 components and 128 for WASIp3 components.
78+ /// As of this writing, setting it to more than 1 will have no effect for
79+ /// WASIp2 components, but that may change in the future.
80+ ///
81+ /// This may be specified either as an integer value or as a range,
82+ /// e.g. 1..8. If it's a range, a number will be selected from that range
83+ /// at random for each new instance.
84+ #[ clap( long, value_parser = parse_usize_range) ]
85+ max_instance_reuse_count : Option < Range < usize > > ,
86+
87+ /// Maximum number of concurrent requests to send to a single component
88+ /// instance.
89+ ///
90+ /// This defaults to 1 for WASIp2 components and 16 for WASIp3 components.
91+ /// Note that setting it to more than 1 will have no effect for WASIp2
92+ /// components since they cannot be called concurrently.
93+ ///
94+ /// This may be specified either as an integer value or as a range,
95+ /// e.g. 1..8. If it's a range, a number will be selected from that range
96+ /// at random for each new instance.
97+ #[ clap( long, value_parser = parse_usize_range) ]
98+ max_instance_concurrent_reuse_count : Option < Range < usize > > ,
99+
100+ /// Request timeout to enforce.
101+ ///
102+ /// As of this writing, this only affects WASIp3 components.
103+ ///
104+ /// A number with no suffix or with an `s` suffix is interpreted as seconds;
105+ /// other accepted suffixes include `ms` (milliseconds), `us` or `μs`
106+ /// (microseconds), and `ns` (nanoseconds).
107+ ///
108+ /// This may be specified either as a single time value or as a range,
109+ /// e.g. 1..8s. If it's a range, a value will be selected from that range
110+ /// at random for each new instance.
111+ #[ clap( long, value_parser = parse_duration_range) ]
112+ request_timeout : Option < Range < Duration > > ,
113+
114+ /// Time to hold an idle component instance for possible reuse before
115+ /// dropping it.
116+ ///
117+ /// A number with no suffix or with an `s` suffix is interpreted as seconds;
118+ /// other accepted suffixes include `ms` (milliseconds), `us` or `μs`
119+ /// (microseconds), and `ns` (nanoseconds).
120+ ///
121+ /// This may be specified either as a single time value or as a range,
122+ /// e.g. 1..8s. If it's a range, a value will be selected from that range
123+ /// at random for each new instance.
124+ #[ clap( long, default_value = "1s" , value_parser = parse_duration_range) ]
125+ idle_instance_timeout : Range < Duration > ,
61126}
62127
63128impl CliArgs {
@@ -73,6 +138,112 @@ impl CliArgs {
73138 }
74139}
75140
141+ #[ derive( Copy , Clone ) ]
142+ enum Range < T > {
143+ Value ( T ) ,
144+ Bounds ( T , T ) ,
145+ }
146+
147+ impl < T > Range < T > {
148+ fn map < V > ( self , fun : impl Fn ( T ) -> V ) -> Range < V > {
149+ match self {
150+ Self :: Value ( v) => Range :: Value ( fun ( v) ) ,
151+ Self :: Bounds ( a, b) => Range :: Bounds ( fun ( a) , fun ( b) ) ,
152+ }
153+ }
154+ }
155+
156+ impl < T : SampleUniform + PartialOrd > SampleRange < T > for Range < T > {
157+ fn sample_single < R : RngCore + ?Sized > (
158+ self ,
159+ rng : & mut R ,
160+ ) -> Result < T , rand:: distr:: uniform:: Error > {
161+ match self {
162+ Self :: Value ( v) => Ok ( v) ,
163+ Self :: Bounds ( a, b) => ( a..b) . sample_single ( rng) ,
164+ }
165+ }
166+
167+ fn is_empty ( & self ) -> bool {
168+ match self {
169+ Self :: Value ( _) => false ,
170+ Self :: Bounds ( a, b) => ( a..b) . is_empty ( ) ,
171+ }
172+ }
173+ }
174+
175+ fn parse_range < T : FromStr > ( s : & str ) -> Result < Range < T > , String >
176+ where
177+ T :: Err : Display ,
178+ {
179+ let error = |e| format ! ( "expected integer or range; got {s:?}; {e}" ) ;
180+ if let Some ( ( start, end) ) = s. split_once ( ".." ) {
181+ Ok ( Range :: Bounds (
182+ start. parse ( ) . map_err ( error) ?,
183+ end. parse ( ) . map_err ( error) ?,
184+ ) )
185+ } else {
186+ Ok ( Range :: Value ( s. parse ( ) . map_err ( error) ?) )
187+ }
188+ }
189+
190+ fn parse_usize_range ( s : & str ) -> Result < Range < usize > , String > {
191+ parse_range ( s)
192+ }
193+
194+ struct ParsedDuration ( Duration ) ;
195+
196+ impl FromStr for ParsedDuration {
197+ type Err = String ;
198+
199+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
200+ let error = |e| {
201+ format ! ( "expected integer suffixed by `s`, `ms`, `us`, `μs`, or `ns`; got {s:?}; {e}" )
202+ } ;
203+ Ok ( Self ( match s. parse ( ) {
204+ Ok ( val) => Duration :: from_secs ( val) ,
205+ Err ( err) => {
206+ if let Some ( num) = s. strip_suffix ( "s" ) {
207+ Duration :: from_secs ( num. parse ( ) . map_err ( error) ?)
208+ } else if let Some ( num) = s. strip_suffix ( "ms" ) {
209+ Duration :: from_millis ( num. parse ( ) . map_err ( error) ?)
210+ } else if let Some ( num) = s. strip_suffix ( "us" ) . or ( s. strip_suffix ( "μs" ) ) {
211+ Duration :: from_micros ( num. parse ( ) . map_err ( error) ?)
212+ } else if let Some ( num) = s. strip_suffix ( "ns" ) {
213+ Duration :: from_nanos ( num. parse ( ) . map_err ( error) ?)
214+ } else {
215+ return Err ( error ( err) ) ;
216+ }
217+ }
218+ } ) )
219+ }
220+ }
221+
222+ fn parse_duration_range ( s : & str ) -> Result < Range < Duration > , String > {
223+ parse_range :: < ParsedDuration > ( s) . map ( |v| v. map ( |v| v. 0 ) )
224+ }
225+
226+ #[ derive( Clone , Copy ) ]
227+ pub struct InstanceReuseConfig {
228+ max_instance_reuse_count : Range < usize > ,
229+ max_instance_concurrent_reuse_count : Range < usize > ,
230+ request_timeout : Option < Range < Duration > > ,
231+ idle_instance_timeout : Range < Duration > ,
232+ }
233+
234+ impl Default for InstanceReuseConfig {
235+ fn default ( ) -> Self {
236+ Self {
237+ max_instance_reuse_count : Range :: Value ( DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT ) ,
238+ max_instance_concurrent_reuse_count : Range :: Value (
239+ DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT ,
240+ ) ,
241+ request_timeout : DEFAULT_REQUEST_TIMEOUT ,
242+ idle_instance_timeout : DEFAULT_IDLE_INSTANCE_TIMEOUT ,
243+ }
244+ }
245+ }
246+
76247/// The Spin HTTP trigger.
77248pub struct HttpTrigger {
78249 /// The address the server should listen on.
@@ -83,6 +254,7 @@ pub struct HttpTrigger {
83254 tls_config : Option < TlsConfig > ,
84255 find_free_port : bool ,
85256 http1_max_buf_size : Option < usize > ,
257+ reuse_config : InstanceReuseConfig ,
86258}
87259
88260impl < F : RuntimeFactors > Trigger < F > for HttpTrigger {
@@ -94,13 +266,26 @@ impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
94266 fn new ( cli_args : Self :: CliArgs , app : & spin_app:: App ) -> anyhow:: Result < Self > {
95267 let find_free_port = cli_args. find_free_port ;
96268 let http1_max_buf_size = cli_args. http1_max_buf_size ;
269+ let reuse_config = InstanceReuseConfig {
270+ max_instance_reuse_count : cli_args
271+ . max_instance_reuse_count
272+ . unwrap_or ( Range :: Value ( DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT ) ) ,
273+ max_instance_concurrent_reuse_count : cli_args
274+ . max_instance_concurrent_reuse_count
275+ . unwrap_or ( Range :: Value (
276+ DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT ,
277+ ) ) ,
278+ request_timeout : cli_args. request_timeout ,
279+ idle_instance_timeout : cli_args. idle_instance_timeout ,
280+ } ;
97281
98282 Self :: new (
99283 app,
100284 cli_args. address ,
101285 cli_args. into_tls_config ( ) ,
102286 find_free_port,
103287 http1_max_buf_size,
288+ reuse_config,
104289 )
105290 }
106291
@@ -125,6 +310,7 @@ impl HttpTrigger {
125310 tls_config : Option < TlsConfig > ,
126311 find_free_port : bool ,
127312 http1_max_buf_size : Option < usize > ,
313+ reuse_config : InstanceReuseConfig ,
128314 ) -> anyhow:: Result < Self > {
129315 Self :: validate_app ( app) ?;
130316
@@ -133,6 +319,7 @@ impl HttpTrigger {
133319 tls_config,
134320 find_free_port,
135321 http1_max_buf_size,
322+ reuse_config,
136323 } )
137324 }
138325
@@ -146,13 +333,15 @@ impl HttpTrigger {
146333 tls_config,
147334 find_free_port,
148335 http1_max_buf_size,
336+ reuse_config,
149337 } = self ;
150338 let server = Arc :: new ( HttpServer :: new (
151339 listen_addr,
152340 tls_config,
153341 find_free_port,
154342 trigger_app,
155343 http1_max_buf_size,
344+ reuse_config,
156345 ) ?) ;
157346 Ok ( server)
158347 }
0 commit comments