Skip to content

Commit e68b1b8

Browse files
committed
perf(pm): fan out registry http clients
1 parent 519961e commit e68b1b8

1 file changed

Lines changed: 38 additions & 4 deletions

File tree

crates/ruborist/src/service/http.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//! | | |
1010
//! | true (npmmirror) false (npmjs.org)
1111
//! | | |
12-
//! | fetch_version_manifest resolve_full_manifest
12+
//! | fetch version job fetch full manifest job
1313
//! | GET /{name}/{spec} GET /{name}
1414
//! | Accept: abbreviated Accept: abbreviated
1515
//! | | + If-None-Match: {etag}
@@ -29,7 +29,7 @@
2929
//! v
3030
//! +-----------------------------------------------------------------+
3131
//! | http.rs -- HTTP Client (this file) |
32-
//! | global singleton reqwest::Client (LazyLock) |
32+
//! | global reqwest::Client pool (LazyLock) |
3333
//! | rustls TLS + no_proxy + env proxy + CachingResolver |
3434
//! +-----------------------------------------------------------------+
3535
//! |
@@ -76,6 +76,8 @@
7676
//! WASM targets skip DNS entirely (browser handles it).
7777
7878
use std::sync::LazyLock;
79+
#[cfg(not(target_arch = "wasm32"))]
80+
use std::sync::atomic::{AtomicUsize, Ordering};
7981
use std::time::Duration;
8082

8183
use anyhow::{Context, Result, anyhow};
@@ -85,16 +87,48 @@ use anyhow::{Context, Result, anyhow};
8587
/// error, which silently-stalled sockets never raise.
8688
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
8789

88-
/// Global HTTP client with connection pooling and DNS caching.
90+
/// Number of independent registry HTTP client pools.
91+
///
92+
/// GHA npmjs pcap showed bun spreading resolve traffic across a few
93+
/// Cloudflare edge IPs while a single reqwest pool concentrated requests on
94+
/// one IP. Four pools keeps the model small but gives the resolver enough
95+
/// independent keep-alive pools to fan out when npmjs/full-manifest
96+
/// concurrency is raised.
97+
#[cfg(not(target_arch = "wasm32"))]
98+
const CLIENT_POOL_SIZE: usize = 4;
99+
100+
/// Global HTTP clients with connection pooling and DNS caching.
89101
///
90-
/// Stores `Result<Client, String>` so that proxy-configuration errors are
102+
/// Stores `Result<Vec<Client>, String>` so that proxy-configuration errors are
91103
/// surfaced to callers instead of panicking or calling `process::exit`.
104+
#[cfg(not(target_arch = "wasm32"))]
105+
static HTTP_CLIENTS: LazyLock<Result<Vec<reqwest::Client>, String>> = LazyLock::new(|| {
106+
(0..CLIENT_POOL_SIZE)
107+
.map(|_| client_builder().and_then(|b| b.build().context("Failed to build reqwest client")))
108+
.collect::<Result<Vec<_>>>()
109+
.map_err(|e| e.to_string())
110+
});
111+
112+
#[cfg(not(target_arch = "wasm32"))]
113+
static CLIENT_COUNTER: AtomicUsize = AtomicUsize::new(0);
114+
115+
#[cfg(not(target_arch = "wasm32"))]
116+
pub(crate) fn get_client() -> Result<&'static reqwest::Client> {
117+
let clients = HTTP_CLIENTS.as_ref().map_err(|e| anyhow!("{e}"))?;
118+
let idx = CLIENT_COUNTER.fetch_add(1, Ordering::Relaxed) % clients.len();
119+
Ok(&clients[idx])
120+
}
121+
122+
/// WASM targets retain a single browser-backed client; there is no native TCP
123+
/// connection pool to fan out.
124+
#[cfg(target_arch = "wasm32")]
92125
static HTTP_CLIENT: LazyLock<Result<reqwest::Client, String>> = LazyLock::new(|| {
93126
client_builder()
94127
.and_then(|b| b.build().context("Failed to build reqwest client"))
95128
.map_err(|e| e.to_string())
96129
});
97130

131+
#[cfg(target_arch = "wasm32")]
98132
pub(crate) fn get_client() -> Result<&'static reqwest::Client> {
99133
HTTP_CLIENT.as_ref().map_err(|e| anyhow!("{e}"))
100134
}

0 commit comments

Comments
 (0)