Skip to content

Commit c6f94bc

Browse files
authored
Retry on 5xx, error on 4xx (#465)
* Retry on 5xx, error on 4xx * Onlyy retry on certain error statuses * Only validate responses in the retry policy * Fix inverted logic
1 parent cab39cd commit c6f94bc

File tree

13 files changed

+99
-145
lines changed

13 files changed

+99
-145
lines changed

sdk/core/src/errors.rs

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,8 @@ pub enum StreamError {
124124
pub enum HttpError {
125125
#[error("Failed to serialize request body as json: {0}")]
126126
BodySerializationError(serde_json::Error),
127-
#[error(
128-
"unexpected HTTP result (expected: {:?}, received: {:?}, body: {:?})",
129-
expected,
130-
received,
131-
body
132-
)]
133-
UnexpectedStatusCode {
134-
expected: Vec<StatusCode>,
135-
received: StatusCode,
136-
body: String,
137-
},
127+
#[error("HTTP error status (status: {:?}, body: {:?})", status, body)]
128+
ErrorStatusCode { status: StatusCode, body: String },
138129
#[error("UTF8 conversion error: {0}")]
139130
Utf8Error(#[from] std::str::Utf8Error),
140131
#[error("from UTF8 conversion error: {0}")]
@@ -157,31 +148,6 @@ pub enum HttpError {
157148
StreamResetError(StreamError),
158149
}
159150

160-
impl HttpError {
161-
pub fn new_unexpected_status_code(
162-
expected: StatusCode,
163-
received: StatusCode,
164-
body: &str,
165-
) -> HttpError {
166-
HttpError::UnexpectedStatusCode {
167-
expected: vec![expected],
168-
received,
169-
body: body.to_owned(),
170-
}
171-
}
172-
173-
pub fn new_multiple_unexpected_status_code(
174-
allowed: Vec<StatusCode>,
175-
received: StatusCode,
176-
body: &str,
177-
) -> HttpError {
178-
HttpError::UnexpectedStatusCode {
179-
expected: allowed,
180-
received,
181-
body: body.to_owned(),
182-
}
183-
}
184-
}
185151
#[derive(Debug, PartialEq, thiserror::Error)]
186152
pub enum Not512ByteAlignedError {
187153
#[error("start range not 512-byte aligned: {0}")]

sdk/core/src/http_client.rs

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,45 +41,15 @@ pub trait HttpClient: Send + Sync + std::fmt::Debug {
4141
async fn execute_request_check_status(
4242
&self,
4343
request: Request<Bytes>,
44-
expected_status: StatusCode,
44+
_expected_status: StatusCode,
4545
) -> Result<Response<Bytes>, HttpError> {
4646
let response = self.execute_request(request).await?;
47-
if expected_status != response.status() {
48-
Err(HttpError::new_unexpected_status_code(
49-
expected_status,
50-
response.status(),
51-
std::str::from_utf8(response.body())?,
52-
))
53-
} else {
47+
let status = response.status();
48+
if (200..400).contains(&status.as_u16()) {
5449
Ok(response)
55-
}
56-
}
57-
58-
async fn execute_request_check_statuses(
59-
&self,
60-
request: Request<Bytes>,
61-
expected_statuses: &[StatusCode],
62-
) -> Result<Response<Bytes>, HttpError> {
63-
let response = self.execute_request(request).await?;
64-
if !expected_statuses
65-
.iter()
66-
.any(|expected_status| *expected_status == response.status())
67-
{
68-
if expected_statuses.len() == 1 {
69-
Err(HttpError::new_unexpected_status_code(
70-
expected_statuses[0],
71-
response.status(),
72-
std::str::from_utf8(response.body())?,
73-
))
74-
} else {
75-
Err(HttpError::new_multiple_unexpected_status_code(
76-
expected_statuses.to_vec(),
77-
response.status(),
78-
std::str::from_utf8(response.body())?,
79-
))
80-
}
8150
} else {
82-
Ok(response)
51+
let body = std::str::from_utf8(response.body())?.to_owned();
52+
Err(crate::HttpError::ErrorStatusCode { status, body })
8353
}
8454
}
8555
}
Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
use crate::policies::{Policy, PolicyResult, Request, Response};
22
use crate::sleep::sleep;
3-
use crate::PipelineContext;
3+
use crate::{HttpError, PipelineContext};
44
use chrono::{DateTime, Local};
5+
use http::StatusCode;
56
use std::sync::Arc;
67
use std::time::Duration;
78

9+
/// A retry policy.
10+
///
11+
/// All retry policies follow a similar pattern only differing in how
12+
/// they determine if the retry has expired and for how long they should
13+
/// sleep between retries.
814
pub trait RetryPolicy {
15+
/// Determine if no more retries should be performed.
16+
///
17+
/// Must return true if no more retries should be attempted.
918
fn is_expired(&self, first_retry_time: &mut Option<DateTime<Local>>, retry_count: u32) -> bool;
19+
/// Determine how long before the next retry should be attempted.
1020
fn sleep_duration(&self, retry_count: u32) -> Duration;
1121
}
1222

23+
/// The status codes where a retry should be attempted.
24+
///
25+
/// On all other 4xx and 5xx status codes no retry is attempted.
26+
const RETRY_STATUSES: &[StatusCode] = &[
27+
StatusCode::REQUEST_TIMEOUT,
28+
StatusCode::TOO_MANY_REQUESTS,
29+
StatusCode::INTERNAL_SERVER_ERROR,
30+
StatusCode::BAD_GATEWAY,
31+
StatusCode::SERVICE_UNAVAILABLE,
32+
StatusCode::GATEWAY_TIMEOUT,
33+
];
34+
1335
#[async_trait::async_trait]
1436
impl<T, C> Policy<C> for T
1537
where
@@ -26,19 +48,50 @@ where
2648
let mut retry_count = 0;
2749

2850
loop {
29-
match next[0].send(ctx, request, &next[1..]).await {
30-
Ok(response) => return Ok(response),
31-
Err(error) => {
32-
log::error!("Error occurred when making request: {}", error);
33-
if self.is_expired(&mut first_retry_time, retry_count) {
51+
let error = match next[0].send(ctx, request, &next[1..]).await {
52+
Ok(response) if (200..400).contains(&response.status().as_u16()) => {
53+
log::trace!(
54+
"Succesful response. Request={:?} response={:?}",
55+
request,
56+
response
57+
);
58+
// Successful status code
59+
return Ok(response);
60+
}
61+
Ok(response) => {
62+
// Error status code
63+
let status = response.status();
64+
let body = response.into_body_string().await;
65+
let error = Box::new(HttpError::ErrorStatusCode { status, body });
66+
if !RETRY_STATUSES.contains(&status) {
67+
log::error!(
68+
"server returned error status which will not be retried: {}",
69+
status
70+
);
71+
// Server didn't return a status we retry on so return early
3472
return Err(error);
35-
} else {
36-
retry_count += 1;
37-
38-
sleep(self.sleep_duration(retry_count)).await;
3973
}
74+
log::debug!(
75+
"server returned error status which requires retry: {}",
76+
status
77+
);
78+
error
4079
}
80+
Err(error) => {
81+
log::debug!(
82+
"error occurred when making request which will be retried: {}",
83+
error
84+
);
85+
error
86+
}
87+
};
88+
89+
if self.is_expired(&mut first_retry_time, retry_count) {
90+
return Err(error);
4191
}
92+
retry_count += 1;
93+
94+
sleep(self.sleep_duration(retry_count)).await;
4295
}
4396
}
4497
}

sdk/core/src/response.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl ResponseBuilder {
3636
}
3737
}
3838

39+
// An HTTP Response
3940
pub struct Response {
4041
status: StatusCode,
4142
headers: HeaderMap,
@@ -63,20 +64,18 @@ impl Response {
6364
(self.status, self.headers, self.body)
6465
}
6566

66-
pub async fn validate(self, expected_status: StatusCode) -> Result<Self, crate::HttpError> {
67-
let status = self.status();
68-
if expected_status != status {
69-
let body = collect_pinned_stream(self.body)
70-
.await
71-
.unwrap_or_else(|_| Bytes::from_static("<INVALID BODY>".as_bytes()));
72-
Err(crate::HttpError::new_unexpected_status_code(
73-
expected_status,
74-
status,
75-
std::str::from_utf8(&body as &[u8]).unwrap_or("<NON-UTF8 BODY>"),
76-
))
77-
} else {
78-
Ok(self)
79-
}
67+
pub async fn into_body_string(self) -> String {
68+
pinned_stream_into_utf8_string(self.body).await
69+
}
70+
}
71+
72+
impl std::fmt::Debug for Response {
73+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74+
f.debug_struct("Response")
75+
.field("status", &self.status)
76+
.field("headers", &self.headers)
77+
.field("body", &"<BODY>")
78+
.finish()
8079
}
8180
}
8281

@@ -92,6 +91,20 @@ pub async fn collect_pinned_stream(mut pinned_stream: PinnedStream) -> Result<By
9291
Ok(final_result.into())
9392
}
9493

94+
/// Collects a `PinnedStream` into a utf8 String
95+
///
96+
/// If the stream cannot be collected or is not utf8, a placeholder string
97+
/// will be returned.
98+
pub async fn pinned_stream_into_utf8_string(stream: PinnedStream) -> String {
99+
let body = collect_pinned_stream(stream)
100+
.await
101+
.unwrap_or_else(|_| Bytes::from_static("<INVALID BODY>".as_bytes()));
102+
let body = std::str::from_utf8(&body)
103+
.unwrap_or("<NON-UTF8 BODY>")
104+
.to_owned();
105+
body
106+
}
107+
95108
impl From<BytesResponse> for Response {
96109
fn from(bytes_response: BytesResponse) -> Self {
97110
let (status, headers, body) = bytes_response.deconstruct();

sdk/cosmos/src/clients/collection_client.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ impl CollectionClient {
5858
let response = self
5959
.pipeline()
6060
.send(&mut pipeline_context, &mut request)
61-
.await?
62-
.validate(http::StatusCode::OK)
6361
.await?;
6462

6563
Ok(GetCollectionResponse::try_from(response).await?)
@@ -80,8 +78,6 @@ impl CollectionClient {
8078
let response = self
8179
.pipeline()
8280
.send(&mut pipeline_context, &mut request)
83-
.await?
84-
.validate(http::StatusCode::NO_CONTENT)
8581
.await?;
8682

8783
Ok(DeleteCollectionResponse::try_from(response).await?)
@@ -102,8 +98,6 @@ impl CollectionClient {
10298
let response = self
10399
.pipeline()
104100
.send(&mut pipeline_context, &mut request)
105-
.await?
106-
.validate(http::StatusCode::OK)
107101
.await?;
108102

109103
Ok(ReplaceCollectionResponse::try_from(response).await?)
@@ -128,8 +122,6 @@ impl CollectionClient {
128122
let response = self
129123
.pipeline()
130124
.send(&mut pipeline_context, &mut request)
131-
.await?
132-
.validate(http::StatusCode::CREATED)
133125
.await?;
134126

135127
Ok(CreateDocumentResponse::try_from(response).await?)

sdk/cosmos/src/clients/cosmos_client.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,6 @@ impl CosmosClient {
191191
let response = self
192192
.pipeline()
193193
.send(&mut pipeline_context, &mut request)
194-
.await?
195-
.validate(http::StatusCode::CREATED)
196194
.await?;
197195

198196
Ok(CreateDatabaseResponse::try_from(response).await?)
@@ -239,7 +237,6 @@ impl CosmosClient {
239237
.send(&mut pipeline_context, &mut request)
240238
.await
241239
);
242-
let response = r#try!(response.validate(http::StatusCode::OK).await);
243240

244241
ListDatabasesResponse::try_from(response).await
245242
}
@@ -256,7 +253,6 @@ impl CosmosClient {
256253
.send(&mut pipeline_context, &mut request)
257254
.await
258255
);
259-
let response = r#try!(response.validate(http::StatusCode::OK).await);
260256
ListDatabasesResponse::try_from(response).await
261257
}
262258
State::Done => return None,

sdk/cosmos/src/clients/database_client.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ impl DatabaseClient {
7272
let response = self
7373
.pipeline()
7474
.send(&mut pipeline_context, &mut request)
75-
.await?
76-
.validate(http::StatusCode::OK)
7775
.await?;
7876

7977
Ok(GetDatabaseResponse::try_from(response).await?)
@@ -94,8 +92,6 @@ impl DatabaseClient {
9492
let response = self
9593
.pipeline()
9694
.send(&mut pipeline_context, &mut request)
97-
.await?
98-
.validate(http::StatusCode::OK)
9995
.await?;
10096

10197
Ok(DeleteDatabaseResponse::try_from(response).await?)
@@ -127,7 +123,6 @@ impl DatabaseClient {
127123
.send(&mut pipeline_context, &mut request)
128124
.await
129125
);
130-
let response = r#try!(response.validate(http::StatusCode::OK).await);
131126
ListCollectionsResponse::try_from(response).await
132127
}
133128
State::Continuation(continuation_token) => {
@@ -146,7 +141,6 @@ impl DatabaseClient {
146141
.send(&mut pipeline_context, &mut request)
147142
.await
148143
);
149-
let response = r#try!(response.validate(http::StatusCode::OK).await);
150144
ListCollectionsResponse::try_from(response).await
151145
}
152146
State::Done => return None,
@@ -182,8 +176,6 @@ impl DatabaseClient {
182176
let response = self
183177
.pipeline()
184178
.send(&mut pipeline_context, &mut request)
185-
.await?
186-
.validate(http::StatusCode::CREATED)
187179
.await?;
188180

189181
Ok(CreateCollectionResponse::try_from(response).await?)
@@ -215,7 +207,6 @@ impl DatabaseClient {
215207
.send(&mut pipeline_context, &mut request)
216208
.await
217209
);
218-
let response = r#try!(response.validate(http::StatusCode::OK).await);
219210
ListUsersResponse::try_from(response).await
220211
}
221212
State::Continuation(continuation_token) => {
@@ -234,7 +225,6 @@ impl DatabaseClient {
234225
.send(&mut pipeline_context, &mut request)
235226
.await
236227
);
237-
let response = r#try!(response.validate(http::StatusCode::OK).await);
238228
ListUsersResponse::try_from(response).await
239229
}
240230
State::Done => return None,

sdk/cosmos/src/clients/document_client.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ impl DocumentClient {
8080
.cosmos_client()
8181
.pipeline()
8282
.send(&mut pipeline_context, &mut request)
83-
.await?
84-
.validate(http::StatusCode::OK)
8583
.await?;
8684

8785
GetDocumentResponse::try_from(response).await

0 commit comments

Comments
 (0)