@@ -9,6 +9,7 @@ use crate::models::{Crate, VersionDownload};
9
9
use crate :: schema:: * ;
10
10
use crate :: views:: EncodableVersionDownload ;
11
11
use chrono:: { Duration , NaiveDate , Utc } ;
12
+ use dashmap:: mapref:: entry:: Entry ;
12
13
13
14
/// Handles the `GET /crates/:crate_id/:version/download` route.
14
15
/// This returns a URL to the location where the crate is stored.
@@ -18,75 +19,96 @@ pub fn download(req: &mut dyn RequestExt) -> EndpointResult {
18
19
let mut crate_name = req. params ( ) [ "crate_id" ] . clone ( ) ;
19
20
let version = req. params ( ) [ "version" ] . as_str ( ) ;
20
21
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 ( ) ) ,
32
- }
33
- } ;
34
-
35
22
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 {
54
- app. instance_metrics
55
- . downloads_non_canonical_crate_name_total
56
- . inc ( ) ;
57
- log_metadata = Some ( ( "bot" , "dl" ) ) ;
58
- }
59
- crate_name = canonical_crate_name;
60
23
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" ) ) ;
85
- }
24
+ match app
25
+ . version_id_cacher
26
+ . entry ( format ! ( "{}:{}" , crate_name, version) )
27
+ {
28
+ // The version_id is cached. This also means that the provided crate_name is canonical
29
+ // and that no fixup is necessary before redirecting.
30
+ Entry :: Occupied ( entry) => {
31
+ let version_id = * entry. get ( ) ;
32
+
33
+ // The increment does not happen instantly, but it's deferred to be executed in a batch
34
+ // along with other downloads. See crate::downloads_counter for the implementation.
35
+ app. downloads_counter . increment ( version_id) ;
36
+ }
86
37
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) ;
38
+ // The version_id is not cached, so either query the database or fallback to an
39
+ // unconditional redirect if the database pool is unhealthy.
40
+ Entry :: Vacant ( entry) => {
41
+ // When no database connection is ready unconditional redirects will be performed. This could
42
+ // happen if the pool is not healthy or if an operator manually configured the application to
43
+ // always perform unconditional redirects (for example as part of the mitigations for an
44
+ // outage). See the comments below for a description of what unconditional redirects do.
45
+ let conn = if app. config . force_unconditional_redirects {
46
+ None
47
+ } else {
48
+ match req. db_conn ( ) {
49
+ Ok ( conn) => Some ( conn) ,
50
+ Err ( PoolError :: UnhealthyPool ) => None ,
51
+ Err ( err) => return Err ( err. into ( ) ) ,
52
+ }
53
+ } ;
54
+
55
+ if let Some ( conn) = & conn {
56
+ use self :: versions:: dsl:: * ;
57
+
58
+ // Returns the crate name as stored in the database, or an error if we could
59
+ // not load the version ID from the database.
60
+ let ( version_id, canonical_crate_name) = app
61
+ . instance_metrics
62
+ . downloads_select_query_execution_time
63
+ . observe_closure_duration ( || {
64
+ versions
65
+ . inner_join ( crates:: table)
66
+ . select ( ( id, crates:: name) )
67
+ . filter ( Crate :: with_name ( & crate_name) )
68
+ . filter ( num. eq ( version) )
69
+ . first :: < ( i32 , String ) > ( & * * conn)
70
+ } ) ?;
71
+
72
+ if canonical_crate_name != crate_name {
73
+ app. instance_metrics
74
+ . downloads_non_canonical_crate_name_total
75
+ . inc ( ) ;
76
+ log_metadata = Some ( ( "bot" , "dl" ) ) ;
77
+ crate_name = canonical_crate_name;
78
+ } else {
79
+ // The version_id is only cached if the provided crate name was canonical.
80
+ // Non-canonical requests fallback to the "slow" path with a DB query, but
81
+ // we typically only get a few hundred non-canonical requests in a day anyway.
82
+ entry. insert ( version_id) ;
83
+ }
84
+
85
+ // The increment does not happen instantly, but it's deferred to be executed in a batch
86
+ // along with other downloads. See crate::downloads_counter for the implementation.
87
+ app. downloads_counter . increment ( version_id) ;
88
+ } else {
89
+ // The download endpoint is the most critical route in the whole crates.io application,
90
+ // as it's relied upon by users and automations to download crates. Keeping it working
91
+ // is the most important thing for us.
92
+ //
93
+ // The endpoint relies on the database to fetch the canonical crate name (with the
94
+ // right capitalization and hyphenation), but that's only needed to serve clients who
95
+ // don't call the endpoint with the crate's canonical name.
96
+ //
97
+ // Thankfully Cargo always uses the right name when calling the endpoint, and we can
98
+ // keep it working during a full database outage by unconditionally redirecting without
99
+ // checking whether the crate exists or the rigth name is used. Non-Cargo clients might
100
+ // get a 404 response instead of a 500, but that's worth it.
101
+ //
102
+ // Without a working database we also can't count downloads, but that's also less
103
+ // critical than keeping Cargo downloads operational.
104
+
105
+ app. instance_metrics
106
+ . downloads_unconditional_redirects_total
107
+ . inc ( ) ;
108
+ log_metadata = Some ( ( "unconditional_redirect" , "true" ) ) ;
109
+ }
110
+ }
111
+ } ;
90
112
91
113
let redirect_url = req
92
114
. app ( )
0 commit comments