Skip to content

Commit cb4d97a

Browse files
Re-enable HTTP connection pooling (#2927)
Re-enabled HTTP connection pooling. In benchmarks using `criterion`, this results in a 3x performance improvement on repeated HTTP operations against the same server, which is not quite the 10x performance reduction reported in #2901, but is a significant improvement.
1 parent 9ae2ee5 commit cb4d97a

File tree

8 files changed

+449
-10
lines changed

8 files changed

+449
-10
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.

sdk/core/azure_core/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ azure_identity.workspace = true
4444
azure_security_keyvault_certificates.path = "../../keyvault/azure_security_keyvault_certificates"
4545
azure_security_keyvault_secrets.path = "../../keyvault/azure_security_keyvault_secrets"
4646
criterion.workspace = true
47+
reqwest.workspace = true
4748
thiserror.workspace = true
4849
tokio.workspace = true
4950
tracing-subscriber.workspace = true
@@ -87,3 +88,7 @@ features = [
8788
[[bench]]
8889
name = "benchmarks"
8990
harness = false
91+
92+
[[bench]]
93+
name = "http_transport_benchmarks"
94+
harness = false

sdk/core/azure_core/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,46 @@ Though not recommended for production, you can enable normal `core::fmt::Debug`
435435
cargo add azure_core -F debug
436436
```
437437

438+
### Specific issues
439+
440+
#### Hang when invoking multiple HTTP operations using the default HTTP transport.
441+
442+
Some customers have reported hangs when using the default `reqwest` based transport, the issue is tracked in [this GitHub issue](https://github.com/hyperium/hyper/issues/2312). The indicated workaround
443+
for the problem is to disable connection pooling in the `reqwest` transport.
444+
445+
If you are encountering this issue, you can disable connection pooling by using the following function to construct an HTTP client which disables HTTP connection pooling:
446+
447+
```rust
448+
# use std::sync::Arc;
449+
# use azure_core::http::HttpClient;
450+
pub fn create_reqwest_client_without_connection_pooling() -> Arc<dyn HttpClient> {
451+
let client = ::reqwest::ClientBuilder::new()
452+
.pool_max_idle_per_host(0)
453+
.build()
454+
.expect("failed to build `reqwest` client");
455+
456+
Arc::new(client)
457+
}
458+
```
459+
460+
You can then set this transport in the `ClientOptions` used to configure your Azure SDK client:
461+
462+
```rust
463+
# use std::sync::Arc;
464+
# use azure_core::http::{HttpClient, ClientOptions, TransportOptions};
465+
# #[derive(Default)]struct MyServiceClientOptions { client_options: ClientOptions};
466+
# let transport = Arc::new(reqwest::Client::new());
467+
let options = MyServiceClientOptions {
468+
client_options: ClientOptions {
469+
transport: Some(TransportOptions::new(transport)),
470+
..Default::default()
471+
},
472+
..Default::default()
473+
};
474+
```
475+
476+
Note that implementing this workaround can result in a significant (3x) performance slowdown depending on your use case.
477+
438478
## Contributing
439479

440480
See the [CONTRIBUTING.md] for details on building, testing, and contributing to these libraries.

sdk/core/azure_core/benches/benchmarks.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ fn http_transport_test(c: &mut Criterion) {
6060
});
6161
});
6262
}
63+
6364
// Main benchmark configuration
6465
criterion_group! {
6566
name = benchmarks;
6667
config = Criterion::default();
6768
targets = url_parsing_benchmark, http_transport_test
6869
}
70+
6971
criterion_main!(benchmarks);
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
#[cfg_attr(target_os = "macos", allow(unused_imports))]
4+
use azure_core::{
5+
credentials::TokenCredential,
6+
fmt::SafeDebug,
7+
http::{
8+
ClientMethodOptions, ClientOptions, HttpClient, Method, Pipeline, RawResponse, Request,
9+
TransportOptions, Url,
10+
},
11+
Result,
12+
};
13+
#[cfg_attr(target_os = "macos", allow(unused_imports))]
14+
use azure_identity::DeveloperToolsCredential;
15+
use criterion::{criterion_group, criterion_main, Criterion};
16+
use std::sync::Arc;
17+
18+
#[derive(Clone, SafeDebug)]
19+
pub struct TestServiceClientOptions {
20+
pub client_options: ClientOptions,
21+
pub api_version: Option<String>,
22+
}
23+
24+
impl Default for TestServiceClientOptions {
25+
fn default() -> Self {
26+
Self {
27+
client_options: ClientOptions::default(),
28+
api_version: Some("2023-10-01".to_string()),
29+
}
30+
}
31+
}
32+
33+
pub struct TestServiceClient {
34+
endpoint: Url,
35+
api_version: String,
36+
pipeline: Pipeline,
37+
}
38+
39+
#[derive(Default, SafeDebug)]
40+
pub struct TestServiceClientGetMethodOptions<'a> {
41+
pub method_options: ClientMethodOptions<'a>,
42+
}
43+
44+
impl TestServiceClient {
45+
pub fn new(
46+
endpoint: &str,
47+
_credential: Arc<dyn TokenCredential>,
48+
options: Option<TestServiceClientOptions>,
49+
) -> Result<Self> {
50+
let options = options.unwrap_or_default();
51+
let mut endpoint = Url::parse(endpoint)?;
52+
if !endpoint.scheme().starts_with("http") {
53+
return Err(azure_core::Error::message(
54+
azure_core::error::ErrorKind::Other,
55+
format!("{endpoint} must use http(s)"),
56+
));
57+
}
58+
endpoint.set_query(None);
59+
60+
Ok(Self {
61+
endpoint,
62+
api_version: options.api_version.unwrap_or_default(),
63+
pipeline: Pipeline::new(
64+
option_env!("CARGO_PKG_NAME"),
65+
option_env!("CARGO_PKG_VERSION"),
66+
options.client_options,
67+
Vec::default(),
68+
Vec::default(),
69+
),
70+
})
71+
}
72+
73+
/// Returns the Url associated with this client.
74+
pub fn endpoint(&self) -> &Url {
75+
&self.endpoint
76+
}
77+
78+
/// Returns the result of a Get verb against the configured endpoint with the specified path.
79+
///
80+
/// This method demonstrates a service client which does not have per-method spans but which will create
81+
/// HTTP client spans if the `InstrumentationOptions` are configured in the client options.
82+
///
83+
pub async fn get(
84+
&self,
85+
path: &str,
86+
options: Option<TestServiceClientGetMethodOptions<'_>>,
87+
) -> Result<RawResponse> {
88+
let options = options.unwrap_or_default();
89+
let mut url = self.endpoint.clone();
90+
url.set_path(path);
91+
url.query_pairs_mut()
92+
.append_pair("api-version", &self.api_version);
93+
94+
let mut request = Request::new(url, azure_core::http::Method::Get);
95+
96+
let response = self
97+
.pipeline
98+
.send(&options.method_options.context, &mut request)
99+
.await?;
100+
if !response.status().is_success() {
101+
return Err(azure_core::Error::message(
102+
azure_core::error::ErrorKind::HttpResponse {
103+
status: response.status(),
104+
error_code: None,
105+
},
106+
format!("Failed to GET {}: {}", request.url(), response.status()),
107+
));
108+
}
109+
Ok(response)
110+
}
111+
}
112+
113+
pub fn new_reqwest_client_disable_connection_pool() -> Arc<dyn HttpClient> {
114+
let client = ::reqwest::ClientBuilder::new()
115+
.pool_max_idle_per_host(0)
116+
.build()
117+
.expect("failed to build `reqwest` client");
118+
119+
Arc::new(client)
120+
}
121+
122+
pub fn new_default_reqwest_client() -> Arc<dyn HttpClient> {
123+
let client = ::reqwest::Client::new();
124+
125+
Arc::new(client)
126+
}
127+
128+
#[cfg_attr(target_os = "macos", allow(unused_variables))]
129+
pub fn simple_http_transport_test(c: &mut Criterion) {
130+
#[cfg(target_os = "macos")]
131+
return;
132+
133+
#[cfg(not(target_os = "macos"))]
134+
{
135+
let rt = tokio::runtime::Runtime::new().unwrap();
136+
137+
let endpoint = "https://azuresdkforcpp.azurewebsites.net";
138+
let credential = DeveloperToolsCredential::new(None).unwrap();
139+
let options = TestServiceClientOptions::default();
140+
141+
let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap();
142+
143+
// Benchmark GET and POST requests
144+
c.bench_function("default_http_pipeline_test", |b| {
145+
b.to_async(&rt).iter(|| async {
146+
let response = client.get("get", None).await;
147+
assert!(response.is_ok());
148+
let response = response.unwrap();
149+
assert_eq!(response.status(), azure_core::http::StatusCode::Ok);
150+
});
151+
});
152+
}
153+
}
154+
155+
#[cfg_attr(target_os = "macos", allow(unused_variables))]
156+
pub fn disable_pooling_http_transport_test(c: &mut Criterion) {
157+
#[cfg(target_os = "macos")]
158+
return;
159+
160+
#[cfg(not(target_os = "macos"))]
161+
{
162+
let rt = tokio::runtime::Runtime::new().unwrap();
163+
164+
let endpoint = "https://azuresdkforcpp.azurewebsites.net";
165+
let credential = DeveloperToolsCredential::new(None).unwrap();
166+
let transport = new_reqwest_client_disable_connection_pool();
167+
let options = TestServiceClientOptions {
168+
client_options: ClientOptions {
169+
transport: Some(TransportOptions::new(transport)),
170+
..Default::default()
171+
},
172+
..Default::default()
173+
};
174+
175+
let client = TestServiceClient::new(endpoint, credential, Some(options)).unwrap();
176+
177+
// Benchmark GET and POST requests
178+
c.bench_function("disable_pooling_http_pipeline_test", |b| {
179+
b.to_async(&rt).iter(|| async {
180+
let response = client.get("get", None).await;
181+
assert!(response.is_ok());
182+
let response = response.unwrap();
183+
assert_eq!(response.status(), azure_core::http::StatusCode::Ok);
184+
});
185+
});
186+
}
187+
}
188+
189+
#[cfg_attr(target_os = "macos", allow(unused_variables))]
190+
pub fn baseline_http_transport_test(c: &mut Criterion) {
191+
#[cfg(target_os = "macos")]
192+
return;
193+
194+
#[cfg(not(target_os = "macos"))]
195+
{
196+
let rt = tokio::runtime::Runtime::new().unwrap();
197+
let endpoint = "https://azuresdkforcpp.azurewebsites.net";
198+
199+
let http_client = new_default_reqwest_client();
200+
201+
// Benchmark GET and POST requests
202+
c.bench_function("baseline_http_pipeline_test", |b| {
203+
b.to_async(&rt).iter(|| async {
204+
let request = Request::new(
205+
Url::parse(&format!("{}/get", endpoint)).unwrap(),
206+
Method::Get,
207+
);
208+
let response = http_client.execute_request(&request).await;
209+
assert!(response.is_ok());
210+
let response = response.unwrap();
211+
assert_eq!(response.status(), azure_core::http::StatusCode::Ok);
212+
});
213+
});
214+
}
215+
}
216+
217+
// Main benchmark configuration
218+
criterion_group!(name=http_transport_benchmarks;
219+
config=Criterion::default()
220+
.sample_size(100)
221+
.warm_up_time(std::time::Duration::new(10, 0))
222+
.measurement_time(std::time::Duration::new(50, 0));
223+
targets=simple_http_transport_test, disable_pooling_http_transport_test, baseline_http_transport_test
224+
);
225+
226+
criterion_main!(http_transport_benchmarks);

sdk/keyvault/azure_security_keyvault_keys/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ azure_identity.workspace = true
2929
azure_security_keyvault_test = { path = "../azure_security_keyvault_test" }
3030
criterion.workspace = true
3131
rand.workspace = true
32+
reqwest.workspace = true
3233
sha2.workspace = true
3334
tokio.workspace = true
3435

0 commit comments

Comments
 (0)