Skip to content

Commit 6da09d3

Browse files
committed
add FORCE_UNCONDITIONAL_REDIRECTS environment variable
1 parent 5728a99 commit 6da09d3

File tree

4 files changed

+99
-49
lines changed

4 files changed

+99
-49
lines changed

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct Server {
2727
pub metrics_authorization_token: Option<String>,
2828
pub use_test_database_pool: bool,
2929
pub instance_metrics_log_every_seconds: Option<u64>,
30+
pub force_unconditional_redirects: bool,
3031
}
3132

3233
impl Default for Server {
@@ -55,6 +56,8 @@ impl Default for Server {
5556
/// will occur.
5657
/// - `INSTANCE_METRICS_LOG_EVERY_SECONDS`: How frequently should instance metrics be logged.
5758
/// If the environment variable is not present instance metrics are not logged.
59+
/// - `FORCE_UNCONDITIONAL_REDIRECTS`: Whether to force unconditional redirects in the download
60+
/// endpoint even with a healthy database pool.
5861
///
5962
/// # Panics
6063
///
@@ -96,6 +99,7 @@ impl Default for Server {
9699
metrics_authorization_token: dotenv::var("METRICS_AUTHORIZATION_TOKEN").ok(),
97100
use_test_database_pool: false,
98101
instance_metrics_log_every_seconds: env_optional("INSTANCE_METRICS_LOG_EVERY_SECONDS"),
102+
force_unconditional_redirects: dotenv::var("FORCE_UNCONDITIONAL_REDIRECTS").is_ok(),
99103
}
100104
}
101105
}

src/controllers/version/downloads.rs

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,62 +18,76 @@ pub fn download(req: &mut dyn RequestExt) -> EndpointResult {
1818
let mut crate_name = req.params()["crate_id"].clone();
1919
let version = req.params()["version"].as_str();
2020

21-
let mut log_metadata = None;
22-
match req.db_conn() {
23-
Ok(conn) => {
24-
use self::versions::dsl::*;
25-
26-
// Returns the crate name as stored in the database, or an error if we could
27-
// not load the version ID from the database.
28-
let (version_id, canonical_crate_name) = app
29-
.instance_metrics
30-
.downloads_select_query_execution_time
31-
.observe_closure_duration(|| {
32-
versions
33-
.inner_join(crates::table)
34-
.select((id, crates::name))
35-
.filter(Crate::with_name(&crate_name))
36-
.filter(num.eq(version))
37-
.first::<(i32, String)>(&*conn)
38-
})?;
39-
40-
if canonical_crate_name != crate_name {
41-
app.instance_metrics
42-
.downloads_non_canonical_crate_name_total
43-
.inc();
44-
log_metadata = Some(("bot", "dl"));
45-
}
46-
crate_name = canonical_crate_name;
47-
48-
// The increment does not happen instantly, but it's deferred to be executed in a batch
49-
// along with other downloads. See crate::downloads_counter for the implementation.
50-
app.downloads_counter.increment(version_id);
21+
// When no database connection is ready unconditional redirects will be performed. This could
22+
// happen if the pool is not healthy or if an operator manually configured the application to
23+
// always perform unconditional redirects (for example as part of the mitigations for an
24+
// outage). See the comments below for a description of what unconditional redirects do.
25+
let conn = if app.config.force_unconditional_redirects {
26+
None
27+
} else {
28+
match req.db_conn() {
29+
Ok(conn) => Some(conn),
30+
Err(PoolError::UnhealthyPool) => None,
31+
Err(err) => return Err(err.into()),
5132
}
52-
Err(PoolError::UnhealthyPool) => {
53-
// The download endpoint is the most critical route in the whole crates.io application,
54-
// as it's relied upon by users and automations to download crates. Keeping it working
55-
// is the most important thing for us.
56-
//
57-
// The endpoint relies on the database to fetch the canonical crate name (with the
58-
// right capitalization and hyphenation), but that's only needed to serve clients who
59-
// don't call the endpoint with the crate's canonical name.
60-
//
61-
// Thankfully Cargo always uses the right name when calling the endpoint, and we can
62-
// keep it working during a full database outage by unconditionally redirecting without
63-
// checking whether the crate exists or the rigth name is used. Non-Cargo clients might
64-
// get a 404 response instead of a 500, but that's worth it.
65-
//
66-
// Without a working database we also can't count downloads, but that's also less
67-
// critical than keeping Cargo downloads operational.
33+
};
6834

35+
let mut log_metadata = None;
36+
if let Some(conn) = &conn {
37+
use self::versions::dsl::*;
38+
39+
// Returns the crate name as stored in the database, or an error if we could
40+
// not load the version ID from the database.
41+
let (version_id, canonical_crate_name) = app
42+
.instance_metrics
43+
.downloads_select_query_execution_time
44+
.observe_closure_duration(|| {
45+
versions
46+
.inner_join(crates::table)
47+
.select((id, crates::name))
48+
.filter(Crate::with_name(&crate_name))
49+
.filter(num.eq(version))
50+
.first::<(i32, String)>(&**conn)
51+
})?;
52+
53+
if canonical_crate_name != crate_name {
6954
app.instance_metrics
70-
.downloads_unconditional_redirects_total
55+
.downloads_non_canonical_crate_name_total
7156
.inc();
72-
log_metadata = Some(("unconditional_redirect", "true"));
57+
log_metadata = Some(("bot", "dl"));
7358
}
74-
Err(err) => return Err(err.into()),
59+
crate_name = canonical_crate_name;
60+
61+
// The increment does not happen instantly, but it's deferred to be executed in a batch
62+
// along with other downloads. See crate::downloads_counter for the implementation.
63+
app.downloads_counter.increment(version_id);
64+
} else {
65+
// The download endpoint is the most critical route in the whole crates.io application,
66+
// as it's relied upon by users and automations to download crates. Keeping it working
67+
// is the most important thing for us.
68+
//
69+
// The endpoint relies on the database to fetch the canonical crate name (with the
70+
// right capitalization and hyphenation), but that's only needed to serve clients who
71+
// don't call the endpoint with the crate's canonical name.
72+
//
73+
// Thankfully Cargo always uses the right name when calling the endpoint, and we can
74+
// keep it working during a full database outage by unconditionally redirecting without
75+
// checking whether the crate exists or the rigth name is used. Non-Cargo clients might
76+
// get a 404 response instead of a 500, but that's worth it.
77+
//
78+
// Without a working database we also can't count downloads, but that's also less
79+
// critical than keeping Cargo downloads operational.
80+
81+
app.instance_metrics
82+
.downloads_unconditional_redirects_total
83+
.inc();
84+
log_metadata = Some(("unconditional_redirect", "true"));
7585
}
7686

87+
// Ensure the connection is released to the pool as soon as possible, as the download endpoint
88+
// covers the majority of our traffic and we don't want it to starve other requests.
89+
drop(conn);
90+
7791
let redirect_url = req
7892
.app()
7993
.config

src/tests/krate/downloads.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,34 @@ fn download_noncanonical_crate_name() {
105105
anon.get::<()>("/api/v1/crates/foo-download/1.0.0/download")
106106
.assert_redirect_ends_with("/crates/foo_download/foo_download-1.0.0.crate");
107107
}
108+
109+
#[test]
110+
fn force_unconditional_redirect() {
111+
let (app, anon, user) = TestApp::init()
112+
.with_config(|config| {
113+
config.force_unconditional_redirects = true;
114+
})
115+
.with_user();
116+
117+
app.db(|conn| {
118+
CrateBuilder::new("foo-download", user.as_model().id)
119+
.version(VersionBuilder::new("1.0.0"))
120+
.expect_build(conn);
121+
});
122+
123+
// Any redirect to an existing crate and version works correctly.
124+
anon.get::<()>("/api/v1/crates/foo-download/1.0.0/download")
125+
.assert_redirect_ends_with("/crates/foo-download/foo-download-1.0.0.crate");
126+
127+
// Redirects to crates with wrong capitalization are performed unconditionally.
128+
anon.get::<()>("/api/v1/crates/Foo_downloaD/1.0.0/download")
129+
.assert_redirect_ends_with("/crates/Foo_downloaD/Foo_downloaD-1.0.0.crate");
130+
131+
// Redirects to missing versions are performed unconditionally.
132+
anon.get::<()>("/api/v1/crates/foo-download/2.0.0/download")
133+
.assert_redirect_ends_with("/crates/foo-download/foo-download-2.0.0.crate");
134+
135+
// Redirects to missing crates are performed unconditionally.
136+
anon.get::<()>("/api/v1/crates/bar-download/1.0.0/download")
137+
.assert_redirect_ends_with("/crates/bar-download/bar-download-1.0.0.crate");
138+
}

src/tests/util/test_app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ fn simple_config() -> config::Server {
334334
metrics_authorization_token: None,
335335
use_test_database_pool: true,
336336
instance_metrics_log_every_seconds: None,
337+
force_unconditional_redirects: false,
337338
}
338339
}
339340

0 commit comments

Comments
 (0)