Skip to content

Commit 96c8559

Browse files
committed
Initialize a single reqwest::Client for the server process
A `reqwest::Client` represents a pool of connections and should be reused. For the `server` and `render-readme` binaries, a single pool is now used for all requests. This should reduce allocation activity in production for routes that make an outgoing HTTP connection. Each initialization costs 30,255 allocations and 2.0 MB of allocated data. This work is also done on a new thread which will map it to a random jemalloc arena. The initialization is now done once when the server is booted and any remaining per-request allocations will occur on a smaller thread pool. This also cleans up how proxying is implemented when running tests. Previously, the configuration was stored in the S3 `Uploader` but this resulted in test failures when calling functions (i.e. `crate_location` and `readme_location`) that do not make an HTTP request.
1 parent b84b91a commit 96c8559

File tree

10 files changed

+75
-81
lines changed

10 files changed

+75
-81
lines changed

src/app.rs

+24-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Application-wide components in a struct accessible from each request
22
3-
use crate::{db, util::CargoResult, Config, Env};
3+
use crate::{db, Config, Env};
44
use std::{path::PathBuf, sync::Arc, time::Duration};
55

66
use diesel::r2d2;
7+
use reqwest::Client;
78
use scheduled_thread_pool::ScheduledThreadPool;
89

910
/// The `App` struct holds the main components of the application like
@@ -26,17 +27,24 @@ pub struct App {
2627

2728
/// The server configuration
2829
pub config: Config,
30+
31+
/// A configured client for outgoing HTTP requests
32+
///
33+
/// In production this shares a single connection pool across requests. In tests
34+
/// this is either None (in which case any attempt to create an outgoing connection
35+
/// will panic) or a `Client` configured with a per-test replay proxy.
36+
http_client: Option<Client>,
2937
}
3038

3139
impl App {
32-
/// Creates a new `App` with a given `Config`
40+
/// Creates a new `App` with a given `Config` and an optional HTTP `Client`
3341
///
3442
/// Configures and sets up:
3543
///
3644
/// - GitHub OAuth
3745
/// - Database connection pools
38-
/// - A `git2::Repository` instance from the index repo checkout (that server.rs ensures exists)
39-
pub fn new(config: &Config) -> App {
46+
/// - Holds an HTTP `Client` and associated connection pool, if provided
47+
pub fn new(config: &Config, http_client: Option<Client>) -> App {
4048
let mut github = oauth2::Config::new(
4149
&config.gh_client_id,
4250
&config.gh_client_secret,
@@ -90,19 +98,22 @@ impl App {
9098
session_key: config.session_key.clone(),
9199
git_repo_checkout: config.git_repo_checkout.clone(),
92100
config: config.clone(),
101+
http_client,
93102
}
94103
}
95104

96105
/// Returns a client for making HTTP requests to upload crate files.
97106
///
98-
/// The handle will go through a proxy if the uploader being used has specified one, which
99-
/// is only done in tests with `TestApp::with_proxy()` in order to be able to record and
100-
/// inspect the HTTP requests that tests make.
101-
pub fn http_client(&self) -> CargoResult<reqwest::Client> {
102-
let mut builder = reqwest::Client::builder();
103-
if let Some(proxy) = self.config.uploader.proxy() {
104-
builder = builder.proxy(reqwest::Proxy::all(proxy)?);
105-
}
106-
Ok(builder.build()?)
107+
/// The client will go through a proxy if the application was configured via
108+
/// `TestApp::with_proxy()`.
109+
///
110+
/// # Panics
111+
///
112+
/// Panics if the application was not initialized with a client. This should only occur in
113+
/// tests that were not properly initialized.
114+
pub fn http_client(&self) -> &Client {
115+
self.http_client
116+
.as_ref()
117+
.expect("No HTTP client is configured. In tests, use `TestApp::with_proxy()`.")
107118
}
108119
}

src/bin/render-readmes.rs

+14-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use chrono::{TimeZone, Utc};
2121
use diesel::{dsl::any, prelude::*};
2222
use docopt::Docopt;
2323
use flate2::read::GzDecoder;
24+
use reqwest::Client;
2425
use tar::{self, Archive};
2526

2627
const DEFAULT_PAGE_SIZE: usize = 25;
@@ -94,6 +95,8 @@ fn main() {
9495
total_pages + 1
9596
};
9697

98+
let client = Client::new();
99+
97100
for (page_num, version_ids_chunk) in version_ids.chunks(page_size).enumerate() {
98101
println!(
99102
"= Page {} of {} ==================================",
@@ -117,22 +120,18 @@ fn main() {
117120
krate_name, version.num
118121
)
119122
});
123+
let client = client.clone();
120124
let handle = thread::spawn(move || {
121125
println!("[{}-{}] Rendering README...", krate_name, version.num);
122-
let readme = get_readme(&config, &version, &krate_name);
126+
let readme = get_readme(&config, &client, &version, &krate_name);
123127
if readme.is_none() {
124128
return;
125129
}
126130
let readme = readme.unwrap();
127131
let readme_path = format!("readmes/{0}/{0}-{1}.html", krate_name, version.num);
128132
config
129133
.uploader
130-
.upload(
131-
&reqwest::Client::new(),
132-
&readme_path,
133-
readme.into_bytes(),
134-
"text/html",
135-
)
134+
.upload(&client, &readme_path, readme.into_bytes(), "text/html")
136135
.unwrap_or_else(|_| {
137136
panic!(
138137
"[{}-{}] Couldn't upload file to S3",
@@ -151,12 +150,17 @@ fn main() {
151150
}
152151

153152
/// Renders the readme of an uploaded crate version.
154-
fn get_readme(config: &Config, version: &Version, krate_name: &str) -> Option<String> {
153+
fn get_readme(
154+
config: &Config,
155+
client: &Client,
156+
version: &Version,
157+
krate_name: &str,
158+
) -> Option<String> {
155159
let location = config
156160
.uploader
157-
.crate_location(krate_name, &version.num.to_string())?;
161+
.crate_location(krate_name, &version.num.to_string());
158162

159-
let mut response = match reqwest::get(&location) {
163+
let mut response = match client.get(&location).send() {
160164
Ok(r) => r,
161165
Err(err) => {
162166
println!(

src/bin/server.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99

1010
use civet::Server as CivetServer;
1111
use conduit_hyper::Service as HyperService;
12+
use reqwest::Client;
1213

1314
enum Server {
1415
Civet(CivetServer),
@@ -24,7 +25,9 @@ fn main() {
2425
env_logger::init();
2526

2627
let config = cargo_registry::Config::default();
27-
let app = App::new(&config);
28+
let client = Client::new();
29+
30+
let app = App::new(&config, Some(client));
2831
let app = cargo_registry::build_handler(Arc::new(app));
2932

3033
// On every server restart, ensure the categories available in the database match

src/config.rs

-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ impl Default for Config {
6868
&api_protocol,
6969
),
7070
cdn: dotenv::var("S3_CDN").ok(),
71-
proxy: None,
7271
}
7372
}
7473
(Env::Production, Replica::ReadOnlyMirror) => {
@@ -89,7 +88,6 @@ impl Default for Config {
8988
&api_protocol,
9089
),
9190
cdn: dotenv::var("S3_CDN").ok(),
92-
proxy: None,
9391
}
9492
}
9593
// In Development mode, either running as a primary instance or a read-only mirror
@@ -109,7 +107,6 @@ impl Default for Config {
109107
&api_protocol,
110108
),
111109
cdn: dotenv::var("S3_CDN").ok(),
112-
proxy: None,
113110
}
114111
} else {
115112
// If we don't set the `S3_BUCKET` variable, we'll use a development-only

src/controllers/krate/metadata.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ pub fn readme(req: &mut dyn Request) -> CargoResult<Response> {
169169
.app()
170170
.config
171171
.uploader
172-
.readme_location(crate_name, version)
173-
.ok_or_else(|| human("crate readme not found"))?;
172+
.readme_location(crate_name, version);
174173

175174
if req.wants_json() {
176175
#[derive(Serialize)]

src/controllers/version/downloads.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ pub fn download(req: &mut dyn Request) -> CargoResult<Response> {
2424
.app()
2525
.config
2626
.uploader
27-
.crate_location(crate_name, version)
28-
.ok_or_else(|| human("crate files not found"))?;
27+
.crate_location(crate_name, version);
2928

3029
if req.wants_json() {
3130
#[derive(Serialize)]

src/github.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ where
2020
let url = format!("{}://api.github.com{}", app.config.api_protocol, url);
2121
info!("GITHUB HTTP: {}", url);
2222

23-
let client = app.http_client()?;
24-
client
23+
app.http_client()
2524
.get(&url)
2625
.header(header::ACCEPT, "application/vnd.github.v3+json")
2726
.header(

src/tests/all.rs

+19-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::{
2929
use conduit::Request;
3030
use conduit_test::MockRequest;
3131
use diesel::prelude::*;
32+
use reqwest::{Client, Proxy};
3233
use url::Url;
3334

3435
macro_rules! t {
@@ -113,6 +114,13 @@ fn app() -> (
113114
conduit_middleware::MiddlewareBuilder,
114115
) {
115116
let (proxy, bomb) = record::proxy();
117+
let (app, handler) = simple_app(Some(proxy));
118+
(bomb, app, handler)
119+
}
120+
121+
fn simple_app(proxy: Option<String>) -> (Arc<App>, conduit_middleware::MiddlewareBuilder) {
122+
git::init();
123+
116124
let uploader = Uploader::S3 {
117125
bucket: s3::Bucket::new(
118126
String::from("alexcrichton-test"),
@@ -123,16 +131,9 @@ fn app() -> (
123131
// sniff/record it, but everywhere else we use https
124132
"http",
125133
),
126-
proxy: Some(proxy),
127134
cdn: None,
128135
};
129136

130-
let (app, handler) = simple_app(uploader);
131-
(bomb, app, handler)
132-
}
133-
134-
fn simple_app(uploader: Uploader) -> (Arc<App>, conduit_middleware::MiddlewareBuilder) {
135-
git::init();
136137
let config = Config {
137138
uploader,
138139
session_key: "test this has to be over 32 bytes long".to_string(),
@@ -149,7 +150,17 @@ fn simple_app(uploader: Uploader) -> (Arc<App>, conduit_middleware::MiddlewareBu
149150
// sniff/record it, but everywhere else we use https
150151
api_protocol: String::from("http"),
151152
};
152-
let app = App::new(&config);
153+
154+
let client = if let Some(proxy) = proxy {
155+
let mut builder = Client::builder();
156+
builder = builder
157+
.proxy(Proxy::all(&proxy).expect("Unable to configure proxy with the provided URL"));
158+
Some(builder.build().expect("TLS backend cannot be initialized"))
159+
} else {
160+
None
161+
};
162+
163+
let app = App::new(&config, client);
153164
t!(t!(app.diesel_database.get()).begin_test_transaction());
154165
let app = Arc::new(app);
155166
let handler = cargo_registry::build_handler(Arc::clone(&app));

src/tests/util.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use crate::{
2626
use cargo_registry::{
2727
middleware::current_user::AuthenticationSource,
2828
models::{ApiToken, User},
29-
uploaders::Uploader,
3029
App,
3130
};
3231
use diesel::PgConnection;
@@ -48,7 +47,7 @@ pub struct TestApp(Rc<TestAppInner>);
4847
impl TestApp {
4948
/// Initialize an application with an `Uploader` that panics
5049
pub fn init() -> TestAppBuilder {
51-
let (app, middle) = crate::simple_app(Uploader::Panic);
50+
let (app, middle) = crate::simple_app(None);
5251
let inner = Rc::new(TestAppInner {
5352
app,
5453
_bomb: None,

0 commit comments

Comments
 (0)