Skip to content

Commit dd911a8

Browse files
committed
feat(async)!: add Sleeper trait to allow custom runtime
1 parent db8acc0 commit dd911a8

File tree

3 files changed

+71
-7
lines changed

3 files changed

+71
-7
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ serde_json = "1.0"
3030
tokio = { version = "1.41.0", features = ["full"] }
3131
electrsd = { version = "0.28.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }
3232
lazy_static = "1.4.0"
33+
async-std = { version = "1.13.0"}
3334

3435
[features]
3536
default = ["blocking", "async", "async-https", "tokio"]

src/async.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//! Esplora by way of `reqwest` HTTP client.
1313
1414
use std::collections::HashMap;
15+
use std::marker::PhantomData;
1516
use std::str::FromStr;
1617

1718
use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
@@ -32,16 +33,17 @@ use crate::{
3233
};
3334

3435
#[derive(Debug, Clone)]
35-
pub struct AsyncClient {
36+
pub struct AsyncClient<S = DefaultSleeper> {
3637
/// The URL of the Esplora Server.
3738
url: String,
3839
/// The inner [`reqwest::Client`] to make HTTP requests.
3940
client: Client,
4041
/// Number of times to retry a request
4142
max_retries: usize,
43+
sleep_fn: PhantomData<S>,
4244
}
4345

44-
impl AsyncClient {
46+
impl<S: Sleeper> AsyncClient<S> {
4547
/// Build an async client from a builder
4648
pub fn from_builder(builder: Builder) -> Result<Self, Error> {
4749
let mut client_builder = Client::builder();
@@ -72,6 +74,7 @@ impl AsyncClient {
7274
url: builder.base_url,
7375
client: client_builder.build()?,
7476
max_retries: builder.max_retries,
77+
sleep_fn: PhantomData,
7578
})
7679
}
7780

@@ -81,6 +84,7 @@ impl AsyncClient {
8184
url,
8285
client,
8386
max_retries: crate::DEFAULT_MAX_RETRIES,
87+
sleep_fn: PhantomData,
8488
}
8589
}
8690

@@ -433,8 +437,7 @@ impl AsyncClient {
433437
loop {
434438
match self.client.get(url).send().await? {
435439
resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
436-
tokio::time::sleep(delay).await;
437-
// FIXME: use an sleeper_fn passed as parameter.
440+
S::sleep(delay).await;
438441
attempts += 1;
439442
delay *= 2;
440443
}
@@ -447,3 +450,21 @@ impl AsyncClient {
447450
fn is_status_retryable(status: reqwest::StatusCode) -> bool {
448451
RETRYABLE_ERROR_CODES.contains(&status.as_u16())
449452
}
453+
454+
pub trait Sleeper: 'static {
455+
type Sleep: std::future::Future<Output = ()>;
456+
457+
fn sleep(dur: std::time::Duration) -> Self::Sleep;
458+
}
459+
460+
#[derive(Default)]
461+
pub struct DefaultSleeper;
462+
463+
#[cfg(any(test, feature = "tokio"))]
464+
impl Sleeper for DefaultSleeper {
465+
type Sleep = tokio::time::Sleep;
466+
467+
fn sleep(dur: std::time::Duration) -> Self::Sleep {
468+
tokio::time::sleep(dur)
469+
}
470+
}

src/lib.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
//! Here is an example of how to create an asynchronous client.
2727
//!
2828
//! ```no_run
29-
//! # #[cfg(feature = "async")]
29+
//! # #[cfg(all(feature = "async", feature = "tokio"))]
3030
//! # {
3131
//! use esplora_client::Builder;
3232
//! let builder = Builder::new("https://blockstream.info/testnet/api");
@@ -178,11 +178,17 @@ impl Builder {
178178
BlockingClient::from_builder(self)
179179
}
180180

181-
// Build an asynchronous client from builder
182-
#[cfg(feature = "async")]
181+
/// Build an asynchronous client from builder
182+
#[cfg(all(feature = "async", feature = "tokio"))]
183183
pub fn build_async(self) -> Result<AsyncClient, Error> {
184184
AsyncClient::from_builder(self)
185185
}
186+
187+
/// Build an asynchronous client from builder
188+
#[cfg(feature = "async")]
189+
pub fn build_async_with_sleeper<S: r#async::Sleeper>(self) -> Result<AsyncClient<S>, Error> {
190+
AsyncClient::from_builder(self)
191+
}
186192
}
187193

188194
/// Errors that can happen during a request to `Esplora` servers.
@@ -253,6 +259,8 @@ mod test {
253259
use electrsd::{bitcoind, bitcoind::BitcoinD, ElectrsD};
254260
use lazy_static::lazy_static;
255261
use std::env;
262+
#[cfg(all(feature = "async", not(feature = "tokio")))]
263+
use std::{future::Future, pin::Pin};
256264
use tokio::sync::Mutex;
257265
#[cfg(all(feature = "blocking", feature = "async"))]
258266
use {
@@ -320,8 +328,15 @@ mod test {
320328
let blocking_client = builder.build_blocking();
321329

322330
let builder_async = Builder::new(&format!("http://{}", esplora_url));
331+
332+
#[cfg(feature = "tokio")]
323333
let async_client = builder_async.build_async().unwrap();
324334

335+
#[cfg(not(feature = "tokio"))]
336+
let async_client = builder_async
337+
.build_async_with_sleeper::<r#async::DefaultSleeper>()
338+
.unwrap();
339+
325340
(blocking_client, async_client)
326341
}
327342

@@ -992,4 +1007,31 @@ mod test {
9921007
let tx_async = async_client.get_tx(&txid).await.unwrap();
9931008
assert_eq!(tx, tx_async);
9941009
}
1010+
1011+
#[cfg(all(feature = "async", feature = "tokio"))]
1012+
#[test]
1013+
fn test_default_tokio_sleeper() {
1014+
let builder = Builder::new("https://blockstream.info/testnet/api");
1015+
let client = builder.build_async();
1016+
assert!(client.is_ok());
1017+
}
1018+
#[cfg(all(feature = "async", not(feature = "tokio")))]
1019+
struct CustomRuntime;
1020+
1021+
#[cfg(all(feature = "async", not(feature = "tokio")))]
1022+
impl r#async::Sleeper for CustomRuntime {
1023+
type Sleep = Pin<Box<dyn Future<Output = ()>>>;
1024+
1025+
fn sleep(dur: std::time::Duration) -> Self::Sleep {
1026+
Box::pin(async_std::task::sleep(dur))
1027+
}
1028+
}
1029+
1030+
#[cfg(all(feature = "async", not(feature = "tokio")))]
1031+
#[test]
1032+
fn test_custom_runtime() {
1033+
let builder = Builder::new("https://blockstream.info/testnet/api");
1034+
let client = builder.build_async_with_sleeper::<CustomRuntime>();
1035+
assert!(client.is_ok());
1036+
}
9951037
}

0 commit comments

Comments
 (0)