Skip to content

Commit 1fa36fb

Browse files
committed
Utilize reqwest::retry::Builder to implement retries
To replace our ~broken homegrown retry logic, we here make use of `reqwest`'s `retry` functionlity that was introduced with its recent v0.12.23 release. While we can't reproduce our full functionlity from before (namely not retrying some error variants), this will probably still work better as our previous version that did always block on the first request. We set some default values here (intended for the main use in LDK Node), but note that these can always can be overridden by the user when using the `from_client` constructor.
1 parent 26678ed commit 1fa36fb

File tree

4 files changed

+43
-24
lines changed

4 files changed

+43
-24
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/
1818

1919
[dependencies]
2020
prost = "0.11.6"
21-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
21+
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] }
2222
tokio = { version = "1", default-features = false, features = ["time"] }
2323
rand = "0.8.5"
2424
async-trait = "0.1.77"
@@ -33,7 +33,7 @@ chacha20-poly1305 = "0.1.2"
3333

3434
[target.'cfg(genproto)'.build-dependencies]
3535
prost-build = { version = "0.11.3" }
36-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "blocking"] }
36+
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls", "blocking"] }
3737

3838
[dev-dependencies]
3939
mockito = "0.28.0"

src/client.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::types::{
1313
};
1414
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
1515
const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
16+
const DEFAULT_RETRIES: u32 = 10;
1617

1718
/// Thin-client to access a hosted instance of Versioned Storage Service (VSS).
1819
/// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API.
@@ -25,9 +26,9 @@ pub struct VssClient {
2526

2627
impl VssClient {
2728
/// Constructs a [`VssClient`] using `base_url` as the VSS server endpoint.
28-
pub fn new(base_url: String) -> Self {
29-
let client = build_client();
30-
Self::from_client(base_url, client)
29+
pub fn new(base_url: String) -> Result<Self, VssError> {
30+
let client = build_client(&base_url)?;
31+
Ok(Self::from_client(base_url, client))
3132
}
3233

3334
/// Constructs a [`VssClient`] from a given [`reqwest::Client`], using `base_url` as the VSS server endpoint.
@@ -38,9 +39,11 @@ impl VssClient {
3839
/// Constructs a [`VssClient`] using `base_url` as the VSS server endpoint.
3940
///
4041
/// HTTP headers will be provided by the given `header_provider`.
41-
pub fn new_with_headers(base_url: String, header_provider: Arc<dyn VssHeaderProvider>) -> Self {
42-
let client = build_client();
43-
Self { base_url, client, header_provider }
42+
pub fn new_with_headers(
43+
base_url: String, header_provider: Arc<dyn VssHeaderProvider>,
44+
) -> Result<Self, VssError> {
45+
let client = build_client(&base_url)?;
46+
Ok(Self { base_url, client, header_provider })
4447
}
4548

4649
/// Returns the underlying base URL.
@@ -128,11 +131,19 @@ impl VssClient {
128131
}
129132
}
130133

131-
fn build_client() -> Client {
132-
Client::builder()
134+
fn build_client(base_url: &str) -> Result<Client, VssError> {
135+
let url = reqwest::Url::parse(base_url).map_err(|_| VssError::InvalidUrlError)?;
136+
let host_str = url.host_str().ok_or(VssError::InvalidUrlError)?.to_string();
137+
// TODO: Once the response `payload` is available in `classify_fn`, we should filter out any
138+
// error types for which retrying doesn't make sense (i.e., `NoSuchKeyError`,
139+
// `InvalidRequestError`,`ConflictError`).
140+
let retry = reqwest::retry::for_host(host_str).max_retries_per_request(DEFAULT_RETRIES);
141+
let client = Client::builder()
133142
.timeout(DEFAULT_TIMEOUT)
134143
.connect_timeout(DEFAULT_TIMEOUT)
135144
.read_timeout(DEFAULT_TIMEOUT)
145+
.retry(retry)
136146
.build()
137-
.unwrap()
147+
.unwrap();
148+
Ok(client)
138149
}

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub enum VssError {
2828
/// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error
2929
/// or something else.
3030
InternalError(String),
31+
32+
/// The provided URL is invalid.
33+
InvalidUrlError,
3134
}
3235

3336
impl VssError {
@@ -67,6 +70,9 @@ impl Display for VssError {
6770
VssError::InternalError(message) => {
6871
write!(f, "InternalError: {}", message)
6972
},
73+
VssError::InvalidUrlError => {
74+
write!(f, "The provided URL is invalid")
75+
},
7076
}
7177
}
7278
}

tests/tests.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ mod tests {
4545
.create();
4646

4747
// Create a new VssClient with the mock server URL.
48-
let client = VssClient::new(base_url);
48+
let client = VssClient::new(base_url).unwrap();
4949

5050
let actual_result = client.get_object(&get_request).await.unwrap();
5151

@@ -82,7 +82,7 @@ mod tests {
8282
"headerkey".to_string(),
8383
"headervalue".to_string(),
8484
)])));
85-
let client = VssClient::new_with_headers(base_url, header_provider);
85+
let client = VssClient::new_with_headers(base_url, header_provider).unwrap();
8686

8787
let actual_result = client.get_object(&get_request).await.unwrap();
8888

@@ -120,7 +120,7 @@ mod tests {
120120
.create();
121121

122122
// Create a new VssClient with the mock server URL.
123-
let vss_client = VssClient::new(base_url);
123+
let vss_client = VssClient::new(base_url).unwrap();
124124
let actual_result = vss_client.put_object(&request).await.unwrap();
125125

126126
let expected_result = &mock_response;
@@ -155,7 +155,7 @@ mod tests {
155155
.create();
156156

157157
// Create a new VssClient with the mock server URL.
158-
let vss_client = VssClient::new(base_url);
158+
let vss_client = VssClient::new(base_url).unwrap();
159159
let actual_result = vss_client.delete_object(&request).await.unwrap();
160160

161161
let expected_result = &mock_response;
@@ -196,7 +196,7 @@ mod tests {
196196
.create();
197197

198198
// Create a new VssClient with the mock server URL.
199-
let client = VssClient::new(base_url);
199+
let client = VssClient::new(base_url).unwrap();
200200

201201
let actual_result = client.list_key_versions(&request).await.unwrap();
202202

@@ -210,7 +210,7 @@ mod tests {
210210
#[tokio::test]
211211
async fn test_no_such_key_err_handling() {
212212
let base_url = mockito::server_url();
213-
let vss_client = VssClient::new(base_url);
213+
let vss_client = VssClient::new(base_url).unwrap();
214214

215215
// NoSuchKeyError
216216
let error_response = ErrorResponse {
@@ -237,7 +237,7 @@ mod tests {
237237
#[tokio::test]
238238
async fn test_get_response_without_value() {
239239
let base_url = mockito::server_url();
240-
let vss_client = VssClient::new(base_url);
240+
let vss_client = VssClient::new(base_url).unwrap();
241241

242242
// GetObjectResponse with None value
243243
let mock_response = GetObjectResponse { value: None, ..Default::default() };
@@ -258,7 +258,7 @@ mod tests {
258258
#[tokio::test]
259259
async fn test_invalid_request_err_handling() {
260260
let base_url = mockito::server_url();
261-
let vss_client = VssClient::new(base_url);
261+
let vss_client = VssClient::new(base_url).unwrap();
262262

263263
// Invalid Request Error
264264
let error_response = ErrorResponse {
@@ -318,7 +318,7 @@ mod tests {
318318
#[tokio::test]
319319
async fn test_auth_err_handling() {
320320
let base_url = mockito::server_url();
321-
let vss_client = VssClient::new(base_url);
321+
let vss_client = VssClient::new(base_url).unwrap();
322322

323323
// Invalid Request Error
324324
let error_response = ErrorResponse {
@@ -390,7 +390,9 @@ mod tests {
390390
async fn test_header_provider_error() {
391391
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
392392
let header_provider = Arc::new(FailingHeaderProvider {});
393-
let client = VssClient::new_with_headers("notused".to_string(), header_provider);
393+
let client =
394+
VssClient::new_with_headers("http://localhost/notused".to_string(), header_provider)
395+
.unwrap();
394396
let result = client.get_object(&get_request).await;
395397

396398
assert!(matches!(result, Err(VssError::AuthError { .. })));
@@ -399,7 +401,7 @@ mod tests {
399401
#[tokio::test]
400402
async fn test_conflict_err_handling() {
401403
let base_url = mockito::server_url();
402-
let vss_client = VssClient::new(base_url);
404+
let vss_client = VssClient::new(base_url).unwrap();
403405

404406
// Conflict Error
405407
let error_response = ErrorResponse {
@@ -432,7 +434,7 @@ mod tests {
432434
#[tokio::test]
433435
async fn test_internal_server_err_handling() {
434436
let base_url = mockito::server_url();
435-
let vss_client = VssClient::new(base_url);
437+
let vss_client = VssClient::new(base_url).unwrap();
436438

437439
// Internal Server Error
438440
let error_response = ErrorResponse {
@@ -492,7 +494,7 @@ mod tests {
492494
#[tokio::test]
493495
async fn test_internal_err_handling() {
494496
let base_url = mockito::server_url();
495-
let vss_client = VssClient::new(base_url);
497+
let vss_client = VssClient::new(base_url).unwrap();
496498

497499
let error_response =
498500
ErrorResponse { error_code: 999, message: "UnknownException".to_string() };

0 commit comments

Comments
 (0)