Skip to content

Commit 824ffbc

Browse files
committed
snapshot
Signed-off-by: Joel Dice <[email protected]>
1 parent 9a37e13 commit 824ffbc

File tree

6 files changed

+199
-14
lines changed

6 files changed

+199
-14
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ spin-trigger = { path = "crates/trigger" }
7474
spin-trigger-http = { path = "crates/trigger-http" }
7575
spin-trigger-redis = { path = "crates/trigger-redis" }
7676
terminal = { path = "crates/terminal" }
77+
rand.workspace = true
7778

7879
[target.'cfg(target_os = "linux")'.dependencies]
7980
# This needs to be an explicit dependency to enable

crates/trigger-http/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ http-body-util = { workspace = true }
1717
hyper = { workspace = true }
1818
hyper-util = { workspace = true }
1919
pin-project-lite = { workspace = true }
20+
rand.workspace = true
2021
rustls = { workspace = true }
2122
rustls-pki-types = { workspace = true }
2223
serde = { workspace = true }

crates/trigger-http/src/lib.rs

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ mod wasip3;
1212

1313
use 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};
2124
use clap::Args;
25+
use rand::{
26+
RngCore,
27+
distr::uniform::{SampleRange, SampleUniform},
28+
};
2229
use serde::Deserialize;
2330
use spin_app::App;
2431
use spin_factors::RuntimeFactors;
@@ -31,6 +38,9 @@ pub use tls::TlsConfig;
3138

3239
pub(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.
3545
pub(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

63126
impl 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.
77233
pub 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

88245
impl<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
}

crates/trigger-http/src/server.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use std::{
77
time::Duration,
88
};
99

10-
use anyhow::{bail, Context};
10+
use anyhow::{Context, bail};
1111
use http::{
12-
uri::{Authority, Scheme},
1312
Request, Response, StatusCode, Uri,
13+
uri::{Authority, Scheme},
1414
};
1515
use http_body_util::BodyExt;
1616
use hyper::{
@@ -21,6 +21,7 @@ use hyper_util::{
2121
rt::{TokioExecutor, TokioIo},
2222
server::conn::auto::Builder,
2323
};
24+
use rand::Rng;
2425
use spin_app::{APP_DESCRIPTION_KEY, APP_NAME_KEY};
2526
use spin_factor_outbound_http::{OutboundHttpFactor, SelfRequestOrigin};
2627
use spin_factors::RuntimeFactors;
@@ -43,14 +44,14 @@ use wasmtime_wasi_http::body::HyperOutgoingBody;
4344
use wasmtime_wasi_http::handler::{HandlerState, StoreBundle};
4445

4546
use crate::{
47+
Body, InstanceReuseConfig, NotFoundRouteKind, TlsConfig, TriggerApp, TriggerInstanceBuilder,
4648
headers::strip_forbidden_headers,
47-
instrument::{finalize_http_span, http_span, instrument_error, MatchedRoute},
49+
instrument::{MatchedRoute, finalize_http_span, http_span, instrument_error},
4850
outbound_http::OutboundHttpInterceptor,
4951
spin::SpinHttpExecutor,
5052
wagi::WagiHttpExecutor,
5153
wasi::WasiHttpExecutor,
5254
wasip3::Wasip3HttpExecutor,
53-
Body, NotFoundRouteKind, TlsConfig, TriggerApp, TriggerInstanceBuilder,
5455
};
5556

5657
pub const MAX_RETRIES: u16 = 10;
@@ -83,6 +84,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
8384
find_free_port: bool,
8485
trigger_app: TriggerApp<F>,
8586
http1_max_buf_size: Option<usize>,
87+
reuse_config: InstanceReuseConfig,
8688
) -> anyhow::Result<Self> {
8789
// This needs to be a vec before building the router to handle duplicate routes
8890
let component_trigger_configs = trigger_app
@@ -135,6 +137,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
135137
&trigger_app,
136138
component,
137139
&trigger_config.executor,
140+
reuse_config,
138141
)
139142
.map(|ht| (component.clone(), ht)),
140143
),
@@ -157,6 +160,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
157160
trigger_app: &Arc<TriggerApp<F>>,
158161
component_id: &str,
159162
executor: &Option<HttpExecutorType>,
163+
reuse_config: InstanceReuseConfig,
160164
) -> anyhow::Result<HandlerType<HttpHandlerState<F>>> {
161165
let pre = trigger_app.get_instance_pre(component_id)?;
162166
let handler_type = match executor {
@@ -166,6 +170,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
166170
HttpHandlerState {
167171
trigger_app: trigger_app.clone(),
168172
component_id: component_id.into(),
173+
reuse_config,
169174
},
170175
)?;
171176
handler_type.validate_executor(executor)?;
@@ -636,6 +641,7 @@ pub(crate) trait HttpExecutor {
636641
pub(crate) struct HttpHandlerState<F: RuntimeFactors> {
637642
trigger_app: Arc<TriggerApp<F>>,
638643
component_id: String,
644+
reuse_config: InstanceReuseConfig,
639645
}
640646

641647
impl<F: RuntimeFactors> HandlerState for HttpHandlerState<F> {
@@ -653,23 +659,22 @@ impl<F: RuntimeFactors> HandlerState for HttpHandlerState<F> {
653659
}
654660

655661
fn request_timeout(&self) -> Duration {
656-
// TODO: Make this configurable
657-
Duration::MAX
662+
self.reuse_config
663+
.request_timeout
664+
.map(|range| rand::rng().random_range(range))
665+
.unwrap_or(Duration::MAX)
658666
}
659667

660668
fn idle_instance_timeout(&self) -> Duration {
661-
// TODO: Make this configurable
662-
Duration::from_secs(1)
669+
rand::rng().random_range(self.reuse_config.idle_instance_timeout)
663670
}
664671

665672
fn max_instance_reuse_count(&self) -> usize {
666-
// TODO: Make this configurable
667-
128
673+
rand::rng().random_range(self.reuse_config.max_instance_reuse_count)
668674
}
669675

670676
fn max_instance_concurrent_reuse_count(&self) -> usize {
671-
// TODO: Make this configurable
672-
16
677+
rand::rng().random_range(self.reuse_config.max_instance_concurrent_reuse_count)
673678
}
674679

675680
fn handle_worker_error(&self, error: anyhow::Error) {

0 commit comments

Comments
 (0)