Skip to content

Commit 10f7840

Browse files
authored
enable connection pooling and reuse for outbound wasi-http (#3229)
* enable connection pooling and reuse for outbound wasi-http This updates `factor-outbound-http/wasi.rs` to use `hyper_util::client::legacy::Client`, which does connection pooling by default. `Client` requires a `tower_service::Service` implementation for creating new connections, which in turn required moving code around to accomodate a different flow of control. It also forces us to work at a slightly higher level of abstraction, so we don't have as much fine-grained control as before, notably: - We can't make HTTP/2-specific request tweaks like we did before (e.g. removing or adding `host` headers, or tweaking the path-with-query). I'm mainly leaving it up to `hyper-util` to do the right thing here :crossed_fingers:. - We can't provide an `AbortOnDropJoinHandle` when constructing an `IncomingResponse` object, which may mean the connection continues to exist after the response is dropped, but that's kind of the point of connection pooling anyway. Open questions: do we want to make this configurable, and if so, should it be enabled or disabled by default? Signed-off-by: Joel Dice <[email protected]> * make outbound HTTP connection pooling runtime-configurable It's enabled by default, but can be disabled via a runtime config file containing: ```toml [outbound_http] connection_pooling = false ``` Signed-off-by: Joel Dice <[email protected]> * use pinned Rust version for `all-integration-tests` This will hopefully fix CI :crossed_fingers: Signed-off-by: Joel Dice <[email protected]> --------- Signed-off-by: Joel Dice <[email protected]>
1 parent 9dbfaec commit 10f7840

File tree

11 files changed

+610
-204
lines changed

11 files changed

+610
-204
lines changed

.github/workflows/build.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ jobs:
194194
steps:
195195
- uses: actions/checkout@v3
196196

197+
- name: setup dependencies
198+
uses: ./.github/actions/spin-ci-dependencies
199+
with:
200+
rust: true
201+
197202
# Install all the toolchain dependencies
198203
- name: Install Rust wasm target
199204
run: rustup target add wasm32-wasip1 wasm32-unknown-unknown

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ tokio = "1"
164164
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] }
165165
toml = "0.8"
166166
toml_edit = "0.22"
167+
tower-service = "0.3.3"
167168
tracing = { version = "0.1.41", features = ["log"] }
168169
url = "2"
169170
walkdir = "2"
@@ -186,4 +187,4 @@ blocks_in_conditions = "allow"
186187

187188
[[bin]]
188189
name = "spin"
189-
path = "src/bin/spin.rs"
190+
path = "src/bin/spin.rs"

crates/factor-outbound-http/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ hyper = { workspace = true }
1313
hyper-util = { workspace = true }
1414
reqwest = { workspace = true, features = ["gzip"] }
1515
rustls = { workspace = true }
16+
serde = { workspace = true }
1617
spin-factor-outbound-networking = { path = "../factor-outbound-networking" }
1718
spin-factors = { path = "../factors" }
1819
spin-telemetry = { path = "../telemetry" }
1920
spin-world = { path = "../world" }
2021
tokio = { workspace = true, features = ["macros", "rt", "net"] }
2122
tokio-rustls = { workspace = true }
23+
tower-service = { workspace = true }
2224
tracing = { workspace = true }
2325
wasmtime = { workspace = true }
2426
wasmtime-wasi = { workspace = true }
@@ -28,5 +30,10 @@ wasmtime-wasi-http = { workspace = true }
2830
spin-factor-variables = { path = "../factor-variables" }
2931
spin-factors-test = { path = "../factors-test" }
3032

33+
[features]
34+
default = ["spin-cli"]
35+
# Includes the runtime configuration handling used by the Spin CLI
36+
spin-cli = []
37+
3138
[lints]
3239
workspace = true

crates/factor-outbound-http/src/lib.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod intercept;
2+
pub mod runtime_config;
23
mod spin;
34
mod wasi;
45
pub mod wasi_2023_10_18;
@@ -12,6 +13,7 @@ use http::{
1213
HeaderValue, Uri,
1314
};
1415
use intercept::OutboundHttpInterceptor;
16+
use runtime_config::RuntimeConfig;
1517
use spin_factor_outbound_networking::{
1618
config::{allowed_hosts::OutboundAllowedHosts, blocked_networks::BlockedNetworks},
1719
ComponentTlsClientConfigs, OutboundNetworkingFactor,
@@ -34,8 +36,8 @@ pub struct OutboundHttpFactor {
3436
}
3537

3638
impl Factor for OutboundHttpFactor {
37-
type RuntimeConfig = ();
38-
type AppState = ();
39+
type RuntimeConfig = RuntimeConfig;
40+
type AppState = AppState;
3941
type InstanceBuilder = InstanceState;
4042

4143
fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
@@ -46,9 +48,14 @@ impl Factor for OutboundHttpFactor {
4648

4749
fn configure_app<T: RuntimeFactors>(
4850
&self,
49-
_ctx: ConfigureAppContext<T, Self>,
51+
mut ctx: ConfigureAppContext<T, Self>,
5052
) -> anyhow::Result<Self::AppState> {
51-
Ok(())
53+
Ok(AppState {
54+
connection_pooling: ctx
55+
.take_runtime_config()
56+
.unwrap_or_default()
57+
.connection_pooling,
58+
})
5259
}
5360

5461
fn prepare<T: RuntimeFactors>(
@@ -67,6 +74,8 @@ impl Factor for OutboundHttpFactor {
6774
self_request_origin: None,
6875
request_interceptor: None,
6976
spin_http_client: None,
77+
wasi_http_clients: None,
78+
connection_pooling: ctx.app_state().connection_pooling,
7079
})
7180
}
7281
}
@@ -80,6 +89,9 @@ pub struct InstanceState {
8089
request_interceptor: Option<Arc<dyn OutboundHttpInterceptor>>,
8190
// Connection-pooling client for 'fermyon:spin/http' interface
8291
spin_http_client: Option<reqwest::Client>,
92+
// Connection pooling client for `wasi:http/outgoing-handler` interface
93+
wasi_http_clients: Option<wasi::HttpClients>,
94+
connection_pooling: bool,
8395
}
8496

8597
impl InstanceState {
@@ -157,3 +169,7 @@ impl std::fmt::Display for SelfRequestOrigin {
157169
write!(f, "{}://{}", self.scheme, self.authority)
158170
}
159171
}
172+
173+
pub struct AppState {
174+
connection_pooling: bool,
175+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#[cfg(feature = "spin-cli")]
2+
pub mod spin;
3+
4+
/// Runtime configuration for outbound HTTP.
5+
#[derive(Debug)]
6+
pub struct RuntimeConfig {
7+
/// If true, enable connection pooling and reuse.
8+
pub connection_pooling: bool,
9+
}
10+
11+
impl Default for RuntimeConfig {
12+
fn default() -> Self {
13+
Self {
14+
connection_pooling: true,
15+
}
16+
}
17+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use serde::Deserialize;
2+
use spin_factors::runtime_config::toml::GetTomlValue;
3+
4+
/// Get the runtime configuration for outbound HTTP from a TOML table.
5+
///
6+
/// Expects table to be in the format:
7+
/// ```toml
8+
/// [outbound_http]
9+
/// connection_pooling = true
10+
/// ```
11+
pub fn config_from_table(
12+
table: &impl GetTomlValue,
13+
) -> anyhow::Result<Option<super::RuntimeConfig>> {
14+
if let Some(outbound_http) = table.get("outbound_http") {
15+
Ok(Some(super::RuntimeConfig {
16+
connection_pooling: outbound_http
17+
.clone()
18+
.try_into::<OutboundHttpToml>()?
19+
.connection_pooling,
20+
}))
21+
} else {
22+
Ok(None)
23+
}
24+
}
25+
26+
#[derive(Debug, Default, Deserialize)]
27+
#[serde(deny_unknown_fields)]
28+
struct OutboundHttpToml {
29+
#[serde(default)]
30+
connection_pooling: bool,
31+
}

crates/factor-outbound-http/src/spin.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ impl spin_http::Host for crate::InstanceState {
9090

9191
// Allow reuse of Client's internal connection pool for multiple requests
9292
// in a single component execution
93-
let client = self.spin_http_client.get_or_insert_with(Default::default);
93+
let client = self.spin_http_client.get_or_insert_with(|| {
94+
let mut builder = reqwest::Client::builder();
95+
if !self.connection_pooling {
96+
builder = builder.pool_max_idle_per_host(0);
97+
}
98+
builder.build().unwrap()
99+
});
94100

95101
let resp = client.execute(req).await.map_err(log_reqwest_error)?;
96102

0 commit comments

Comments
 (0)