@@ -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