@@ -8,22 +8,10 @@ use aws_smithy_runtime_api::client::retries::classifiers::{
8
8
} ;
9
9
use tracing:: debug;
10
10
11
- /// Error marker for monthly limit exceeded errors
12
11
const MONTHLY_LIMIT_ERROR_MARKER : & str = "MONTHLY_REQUEST_COUNT" ;
13
-
14
- /// Error message for high load conditions that should be retried
15
12
const HIGH_LOAD_ERROR_MESSAGE : & str =
16
13
"Encountered unexpectedly high load when processing the request, please try again." ;
17
-
18
- /// Error message for insufficient model capacity that should be retried
19
- const INSUFFICIENT_MODEL_CAPACITY_MESSAGE : & str = "I am experiencing high traffic, please try again shortly." ;
20
-
21
- /// Status codes that indicate service overload/unavailability and should be retried
22
- const SERVICE_OVERLOAD_STATUS_CODES : & [ u16 ] = & [
23
- 429 , // Too Many Requests - throttling with insufficient model capacity
24
- 500 , // Internal Server Error - requires specific message check for high load conditions
25
- 503 , // Service Unavailable - server is temporarily overloaded or under maintenance
26
- ] ;
14
+ const SERVICE_UNAVAILABLE_EXCEPTION : & str = "ServiceUnavailableException" ;
27
15
28
16
#[ derive( Debug , Default ) ]
29
17
pub struct QCliRetryClassifier ;
@@ -33,112 +21,58 @@ impl QCliRetryClassifier {
33
21
Self
34
22
}
35
23
36
- /// Return the priority of this retry classifier.
37
- ///
38
- /// We want this to run after the standard classifiers but with high priority
39
- /// to override their decisions for our specific error cases.
40
- ///
41
- /// # Returns
42
- /// A priority that runs after the transient error classifier but can override its decisions.
43
24
pub fn priority ( ) -> RetryClassifierPriority {
44
25
RetryClassifierPriority :: run_after ( RetryClassifierPriority :: transient_error_classifier ( ) )
45
26
}
46
27
47
- /// Check if the error indicates a monthly limit has been reached
48
- fn is_monthly_limit_error ( ctx : & InterceptorContext ) -> bool {
49
- let Some ( resp) = ctx. response ( ) else {
50
- return false ;
51
- } ;
52
-
53
- // Check status code first - monthly limit errors typically return 429 (Too Many Requests)
54
- let status_code = resp. status ( ) . as_u16 ( ) ;
55
- if status_code != 429 {
56
- return false ;
57
- }
58
-
59
- let Some ( bytes) = resp. body ( ) . bytes ( ) else {
60
- return false ;
61
- } ;
62
-
63
- let is_monthly_limit = match std:: str:: from_utf8 ( bytes) {
64
- Ok ( body_str) => body_str. contains ( MONTHLY_LIMIT_ERROR_MARKER ) ,
65
- Err ( _) => false ,
66
- } ;
28
+ fn extract_response_body ( ctx : & InterceptorContext ) -> Option < & str > {
29
+ let bytes = ctx. response ( ) ?. body ( ) . bytes ( ) ?;
30
+ std:: str:: from_utf8 ( bytes) . ok ( )
31
+ }
67
32
33
+ fn is_monthly_limit_error ( body_str : & str ) -> bool {
34
+ let is_monthly_limit = body_str. contains ( MONTHLY_LIMIT_ERROR_MARKER ) ;
68
35
debug ! (
69
36
"QCliRetryClassifier: Monthly limit error detected: {}" ,
70
37
is_monthly_limit
71
38
) ;
72
39
is_monthly_limit
73
40
}
74
41
75
- /// Check if the error indicates a model is unavailable due to high load
76
- fn is_service_overloaded_error ( ctx : & InterceptorContext ) -> bool {
42
+ fn is_service_overloaded_error ( ctx : & InterceptorContext , body_str : & str ) -> bool {
77
43
let Some ( resp) = ctx. response ( ) else {
78
44
return false ;
79
45
} ;
80
46
81
- let status_code = resp. status ( ) . as_u16 ( ) ;
82
-
83
- // Fail fast: if status code is not in our list, return false immediately
84
- if !SERVICE_OVERLOAD_STATUS_CODES . contains ( & status_code) {
47
+ if resp. status ( ) . as_u16 ( ) != 500 {
85
48
return false ;
86
49
}
87
50
88
- let is_overloaded = match status_code {
89
- 429 => {
90
- // For 429 errors, check if the response body contains the insufficient model capacity message
91
- let Some ( bytes) = resp. body ( ) . bytes ( ) else {
92
- return false ;
93
- } ;
94
-
95
- match std:: str:: from_utf8 ( bytes) {
96
- Ok ( body_str) => body_str. contains ( INSUFFICIENT_MODEL_CAPACITY_MESSAGE ) ,
97
- Err ( _) => false ,
98
- }
99
- } ,
100
- 500 => {
101
- // For 500 errors, check if the response body contains the specific high load message
102
- let Some ( bytes) = resp. body ( ) . bytes ( ) else {
103
- return false ;
104
- } ;
105
-
106
- match std:: str:: from_utf8 ( bytes) {
107
- Ok ( body_str) => body_str. contains ( HIGH_LOAD_ERROR_MESSAGE ) ,
108
- Err ( _) => false ,
109
- }
110
- } ,
111
- 503 => {
112
- // For 503 Service Unavailable, always retry (no additional checks needed)
113
- true
114
- } ,
115
- _ => {
116
- // This shouldn't happen given our fail-fast check above, but handle gracefully
117
- false
118
- } ,
119
- } ;
51
+ let is_overloaded =
52
+ body_str. contains ( HIGH_LOAD_ERROR_MESSAGE ) || body_str. contains ( SERVICE_UNAVAILABLE_EXCEPTION ) ;
120
53
121
54
debug ! (
122
- "QCliRetryClassifier: Service overloaded error detected (status {} ): {}" ,
123
- status_code , is_overloaded
55
+ "QCliRetryClassifier: Service overloaded error detected (status 500 ): {}" ,
56
+ is_overloaded
124
57
) ;
125
58
is_overloaded
126
59
}
127
60
}
128
61
129
62
impl ClassifyRetry for QCliRetryClassifier {
130
63
fn classify_retry ( & self , ctx : & InterceptorContext ) -> RetryAction {
131
- // Check for monthly limit error first - this should never be retried
132
- if Self :: is_monthly_limit_error ( ctx) {
64
+ let Some ( body_str) = Self :: extract_response_body ( ctx) else {
65
+ return RetryAction :: NoActionIndicated ;
66
+ } ;
67
+
68
+ if Self :: is_monthly_limit_error ( body_str) {
133
69
return RetryAction :: RetryForbidden ;
134
70
}
135
71
136
- // Check for service overloaded error - this should be treated as throttling
137
- if Self :: is_service_overloaded_error ( ctx) {
72
+ if Self :: is_service_overloaded_error ( ctx, body_str) {
138
73
return RetryAction :: throttling_error ( ) ;
139
74
}
140
75
141
- // No specific action for other errors
142
76
RetryAction :: NoActionIndicated
143
77
}
144
78
@@ -173,10 +107,9 @@ mod tests {
173
107
let classifier = QCliRetryClassifier :: new ( ) ;
174
108
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
175
109
176
- // Create a response with MONTHLY_REQUEST_COUNT in the body
177
- let response_body = r#"{"error": "MONTHLY_REQUEST_COUNT exceeded"}"# ;
110
+ let response_body = r#"{"__type":"ThrottlingException","message":"Maximum Request reached for this month.","reason":"MONTHLY_REQUEST_COUNT"}"# ;
178
111
let response = Response :: builder ( )
179
- . status ( 429 )
112
+ . status ( 400 )
180
113
. body ( response_body)
181
114
. unwrap ( )
182
115
. map ( SdkBody :: from) ;
@@ -188,15 +121,13 @@ mod tests {
188
121
}
189
122
190
123
#[ test]
191
- fn test_insufficient_model_capacity_error_classification ( ) {
124
+ fn test_service_unavailable_exception_classification ( ) {
192
125
let classifier = QCliRetryClassifier :: new ( ) ;
193
126
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
194
127
195
- // Create a 429 response with the insufficient model capacity message - should be treated as service
196
- // overloaded
197
- let response_body = r#"{"error": "I am experiencing high traffic, please try again shortly."}"# ;
128
+ let response_body = r#"{"__type":"ServiceUnavailableException","message":"The service is temporarily unavailable. Please try again later."}"# ;
198
129
let response = Response :: builder ( )
199
- . status ( 429 )
130
+ . status ( 500 )
200
131
. body ( response_body)
201
132
. unwrap ( )
202
133
. map ( SdkBody :: from) ;
@@ -208,32 +139,10 @@ mod tests {
208
139
}
209
140
210
141
#[ test]
211
- fn test_429_error_without_insufficient_capacity_message_not_retried ( ) {
212
- let classifier = QCliRetryClassifier :: new ( ) ;
213
- let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
214
-
215
- // Create a 429 response without the specific insufficient model capacity message - should NOT be
216
- // retried
217
- let response_body = "Too Many Requests - some other error" ;
218
- let response = Response :: builder ( )
219
- . status ( 429 )
220
- . body ( response_body)
221
- . unwrap ( )
222
- . map ( SdkBody :: from) ;
223
-
224
- ctx. set_response ( response. try_into ( ) . unwrap ( ) ) ;
225
-
226
- let result = classifier. classify_retry ( & ctx) ;
227
- assert_eq ! ( result, RetryAction :: NoActionIndicated ) ;
228
- }
229
-
230
- #[ test]
231
- fn test_service_overloaded_error_classification ( ) {
142
+ fn test_high_load_error_classification ( ) {
232
143
let classifier = QCliRetryClassifier :: new ( ) ;
233
144
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
234
145
235
- // Create a 500 response with the specific high load message - should be treated as service
236
- // overloaded
237
146
let response_body =
238
147
r#"{"error": "Encountered unexpectedly high load when processing the request, please try again."}"# ;
239
148
let response = Response :: builder ( )
@@ -249,12 +158,11 @@ mod tests {
249
158
}
250
159
251
160
#[ test]
252
- fn test_500_error_without_high_load_message_not_retried ( ) {
161
+ fn test_500_error_without_specific_message_not_retried ( ) {
253
162
let classifier = QCliRetryClassifier :: new ( ) ;
254
163
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
255
164
256
- // Create a 500 response without the specific high load message - should NOT be retried
257
- let response_body = "Internal Server Error - some other error" ;
165
+ let response_body = r#"{"__type":"InternalServerException","message":"Some other error"}"# ;
258
166
let response = Response :: builder ( )
259
167
. status ( 500 )
260
168
. body ( response_body)
@@ -268,30 +176,10 @@ mod tests {
268
176
}
269
177
270
178
#[ test]
271
- fn test_service_unavailable_error_classification ( ) {
179
+ fn test_no_action_for_other_status_codes ( ) {
272
180
let classifier = QCliRetryClassifier :: new ( ) ;
273
181
let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
274
182
275
- // Create a 503 response - should be treated as service overloaded
276
- let response_body = "Service Unavailable" ;
277
- let response = Response :: builder ( )
278
- . status ( 503 )
279
- . body ( response_body)
280
- . unwrap ( )
281
- . map ( SdkBody :: from) ;
282
-
283
- ctx. set_response ( response. try_into ( ) . unwrap ( ) ) ;
284
-
285
- let result = classifier. classify_retry ( & ctx) ;
286
- assert_eq ! ( result, RetryAction :: throttling_error( ) ) ;
287
- }
288
-
289
- #[ test]
290
- fn test_no_action_for_non_overload_errors ( ) {
291
- let classifier = QCliRetryClassifier :: new ( ) ;
292
- let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
293
-
294
- // Create a 400 response - should not be treated as service overloaded
295
183
let response = Response :: builder ( )
296
184
. status ( 400 )
297
185
. body ( "Bad Request" )
@@ -303,53 +191,4 @@ mod tests {
303
191
let result = classifier. classify_retry ( & ctx) ;
304
192
assert_eq ! ( result, RetryAction :: NoActionIndicated ) ;
305
193
}
306
-
307
- #[ test]
308
- fn test_fail_fast_for_non_service_overload_status_codes ( ) {
309
- let classifier = QCliRetryClassifier :: new ( ) ;
310
- let mut ctx = InterceptorContext :: new ( Input :: doesnt_matter ( ) ) ;
311
-
312
- // Test various status codes that are not in SERVICE_OVERLOAD_STATUS_CODES
313
- let test_cases = vec ! [
314
- ( 200 , "OK" ) ,
315
- ( 400 , "Bad Request" ) ,
316
- ( 401 , "Unauthorized" ) ,
317
- ( 403 , "Forbidden" ) ,
318
- ( 404 , "Not Found" ) ,
319
- ( 502 , "Bad Gateway" ) ,
320
- ] ;
321
-
322
- for ( status_code, body) in test_cases {
323
- let response = Response :: builder ( )
324
- . status ( status_code)
325
- . body ( body)
326
- . unwrap ( )
327
- . map ( SdkBody :: from) ;
328
-
329
- ctx. set_response ( response. try_into ( ) . unwrap ( ) ) ;
330
-
331
- let result = classifier. classify_retry ( & ctx) ;
332
- assert_eq ! (
333
- result,
334
- RetryAction :: NoActionIndicated ,
335
- "Status code {} should return NoActionIndicated" ,
336
- status_code
337
- ) ;
338
- }
339
- }
340
-
341
- #[ test]
342
- fn test_classifier_priority ( ) {
343
- let priority = QCliRetryClassifier :: priority ( ) ;
344
- let transient_priority = RetryClassifierPriority :: transient_error_classifier ( ) ;
345
-
346
- // Our classifier should have higher priority than the transient error classifier
347
- assert ! ( priority > transient_priority) ;
348
- }
349
-
350
- #[ test]
351
- fn test_classifier_name ( ) {
352
- let classifier = QCliRetryClassifier :: new ( ) ;
353
- assert_eq ! ( classifier. name( ) , "Q CLI Custom Retry Classifier" ) ;
354
- }
355
194
}
0 commit comments