8
8
//! we should avoid dropping download requests even if that means rejecting some legitimate
9
9
//! requests to other endpoints.
10
10
11
+ use std:: env;
11
12
use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
12
13
13
14
use super :: prelude:: * ;
@@ -17,7 +18,8 @@ use conduit::{RequestExt, StatusCode};
17
18
pub ( super ) struct BalanceCapacity {
18
19
handler : Option < Box < dyn Handler > > ,
19
20
capacity : usize ,
20
- in_flight_requests : AtomicUsize ,
21
+ in_flight_non_dl_requests : AtomicUsize ,
22
+ report_only : bool ,
21
23
log_at_percentage : usize ,
22
24
throttle_at_percentage : usize ,
23
25
dl_only_at_percentage : usize ,
@@ -28,12 +30,39 @@ impl BalanceCapacity {
28
30
Self {
29
31
handler : None ,
30
32
capacity,
31
- in_flight_requests : AtomicUsize :: new ( 0 ) ,
32
- log_at_percentage : read_env_percentage ( "WEB_CAPACITY_LOG_PCT" , 20 ) ,
33
+ in_flight_non_dl_requests : AtomicUsize :: new ( 0 ) ,
34
+ report_only : env:: var ( "WEB_CAPACITY_REPORT_ONLY" ) . ok ( ) . is_some ( ) ,
35
+ log_at_percentage : read_env_percentage ( "WEB_CAPACITY_LOG_PCT" , 50 ) ,
33
36
throttle_at_percentage : read_env_percentage ( "WEB_CAPACITY_THROTTLE_PCT" , 70 ) ,
34
37
dl_only_at_percentage : read_env_percentage ( "WEB_CAPACITY_DL_ONLY_PCT" , 80 ) ,
35
38
}
36
39
}
40
+
41
+ /// Handle a request normally
42
+ fn handle ( & self , request : & mut dyn RequestExt ) -> AfterResult {
43
+ self . handler . as_ref ( ) . unwrap ( ) . call ( request)
44
+ }
45
+
46
+ /// Handle a request when load exceeds a threshold
47
+ ///
48
+ /// In report-only mode, log metadata is added but the request is still served. Otherwise,
49
+ /// the request is rejected with a service unavailable response.
50
+ fn handle_high_load ( & self , request : & mut dyn RequestExt , note : & str ) -> AfterResult {
51
+ if self . report_only {
52
+ // In report-only mode we serve all requests but add log metadata
53
+ super :: log_request:: add_custom_metadata ( request, "would_reject" , note) ;
54
+ self . handle ( request)
55
+ } else {
56
+ // Reject the request
57
+ super :: log_request:: add_custom_metadata ( request, "cause" , note) ;
58
+ let body = "Service temporarily unavailable" ;
59
+ Response :: builder ( )
60
+ . status ( StatusCode :: SERVICE_UNAVAILABLE )
61
+ . header ( header:: CONTENT_LENGTH , body. len ( ) )
62
+ . body ( Body :: from_static ( body. as_bytes ( ) ) )
63
+ . map_err ( box_error)
64
+ }
65
+ }
37
66
}
38
67
39
68
impl AroundMiddleware for BalanceCapacity {
@@ -44,58 +73,43 @@ impl AroundMiddleware for BalanceCapacity {
44
73
45
74
impl Handler for BalanceCapacity {
46
75
fn call ( & self , request : & mut dyn RequestExt ) -> AfterResult {
76
+ // Download requests are always accepted and do not affect the request count
77
+ if request. path ( ) . starts_with ( "/api/v1/crates/" ) && request. path ( ) . ends_with ( "/download" ) {
78
+ return self . handle ( request) ;
79
+ }
80
+
47
81
// The _drop_on_exit ensures the counter is decremented for all exit paths (including panics)
48
- let ( _drop_on_exit, count) = RequestCounter :: add_one ( & self . in_flight_requests ) ;
49
- let handler = self . handler . as_ref ( ) . unwrap ( ) ;
82
+ let ( _drop_on_exit, count) = RequestCounter :: add_one ( & self . in_flight_non_dl_requests ) ;
50
83
let load = 100 * count / self . capacity ;
51
84
52
85
// Begin logging request count so early stages of load increase can be located
53
86
if load >= self . log_at_percentage {
54
- super :: log_request:: add_custom_metadata ( request, "in_flight_requests" , count) ;
55
- }
56
-
57
- // Download requests are always accepted
58
- if request. path ( ) . starts_with ( "/api/v1/crates/" ) && request. path ( ) . ends_with ( "/download" ) {
59
- return handler. call ( request) ;
87
+ super :: log_request:: add_custom_metadata ( request, "in_flight_non_dl_requests" , count) ;
60
88
}
61
89
62
90
// Reject read-only requests as load nears capacity. Bots are likely to send only safe
63
91
// requests and this helps prioritize requests that users may be reluctant to retry.
64
92
if load >= self . throttle_at_percentage && request. method ( ) . is_safe ( ) {
65
- return over_capacity_response ( request) ;
93
+ return self . handle_high_load ( request, "over capacity (throttle)" ) ;
66
94
}
67
95
68
96
// As load reaches capacity, all non-download requests are rejected
69
97
if load >= self . dl_only_at_percentage {
70
- return over_capacity_response ( request) ;
98
+ return self . handle_high_load ( request, "over capacity (download only)" ) ;
71
99
}
72
100
73
- handler . call ( request)
101
+ self . handle ( request)
74
102
}
75
103
}
76
104
77
- fn over_capacity_response ( request : & mut dyn RequestExt ) -> AfterResult {
78
- // TODO: Generate an alert so we can investigate
79
- super :: log_request:: add_custom_metadata ( request, "cause" , "over capacity" ) ;
80
- let body = "Service temporarily unavailable" ;
81
- Response :: builder ( )
82
- . status ( StatusCode :: SERVICE_UNAVAILABLE )
83
- . header ( header:: CONTENT_LENGTH , body. len ( ) )
84
- . body ( Body :: from_static ( body. as_bytes ( ) ) )
85
- . map_err ( box_error)
86
- }
87
-
88
105
fn read_env_percentage ( name : & str , default : usize ) -> usize {
89
- if let Ok ( value) = std :: env:: var ( name) {
106
+ if let Ok ( value) = env:: var ( name) {
90
107
value. parse ( ) . unwrap_or ( default)
91
108
} else {
92
109
default
93
110
}
94
111
}
95
112
96
- // FIXME(JTG): I've copied the following from my `conduit-hyper` crate. Once we transition from
97
- // `civet`, we could pass the in_flight_request count from `condut-hyper` via a request extension.
98
-
99
113
/// A struct that stores a reference to an atomic counter so it can be decremented when dropped
100
114
struct RequestCounter < ' a > {
101
115
counter : & ' a AtomicUsize ,
0 commit comments