Skip to content

Commit 9aa08ed

Browse files
committed
Retry failed RPC calls on error
Whenever the transport errors with "WouldBlock" we wait a bit and retry the call. Related to #650, should at least fix the errors with RPC
1 parent 9f9ffd0 commit 9aa08ed

File tree

1 file changed

+95
-1
lines changed

1 file changed

+95
-1
lines changed

src/blockchain/rpc.rs

+95-1
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ use bitcoincore_rpc::json::{
4545
ListUnspentResultEntry, ScanningDetails,
4646
};
4747
use bitcoincore_rpc::jsonrpc::serde_json::{json, Value};
48+
use bitcoincore_rpc::jsonrpc::{
49+
self, simple_http::SimpleHttpTransport, Error as JsonRpcError, Request, Response, Transport,
50+
};
4851
use bitcoincore_rpc::Auth as RpcAuth;
4952
use bitcoincore_rpc::{Client, RpcApi};
5053
use log::{debug, info};
5154
use serde::{Deserialize, Serialize};
5255
use std::collections::{HashMap, HashSet};
56+
use std::fmt;
5357
use std::path::PathBuf;
58+
use std::sync::atomic::{AtomicU8, Ordering};
5459
use std::thread;
5560
use std::time::Duration;
5661

@@ -80,6 +85,10 @@ pub struct RpcConfig {
8085
pub wallet_name: String,
8186
/// Sync parameters
8287
pub sync_params: Option<RpcSyncParams>,
88+
/// Max number of attempts before giving up and returning an error
89+
///
90+
/// Set to `0` preserve the old behavior of erroring immediately
91+
pub max_tries: u8,
8392
}
8493

8594
/// Sync parameters for Bitcoin Core RPC.
@@ -195,6 +204,68 @@ impl WalletSync for RpcBlockchain {
195204
}
196205
}
197206

207+
struct SimpleHttpWithRetry {
208+
inner: SimpleHttpTransport,
209+
attempts: AtomicU8,
210+
limit: u8,
211+
}
212+
213+
macro_rules! impl_inner {
214+
($self:expr, $method:ident, $req:expr) => {{
215+
while $self.attempts.load(Ordering::Relaxed) <= $self.limit {
216+
match $self.inner.$method($req.clone()) {
217+
Ok(r) => {
218+
$self.attempts.store(0, Ordering::Relaxed);
219+
return Ok(r);
220+
}
221+
Err(JsonRpcError::Transport(e)) => {
222+
match e.downcast_ref::<jsonrpc::simple_http::Error>() {
223+
Some(jsonrpc::simple_http::Error::SocketError(io))
224+
if io.kind() == std::io::ErrorKind::WouldBlock =>
225+
{
226+
let attempt = $self.attempts.fetch_add(1, Ordering::Relaxed);
227+
let delay = std::cmp::min(1000, 100 << attempt as u64);
228+
229+
debug!(
230+
"Got a WouldBlock error at attempt {}, sleeping for {}ms",
231+
attempt, delay
232+
);
233+
std::thread::sleep(std::time::Duration::from_millis(delay));
234+
235+
continue;
236+
}
237+
_ => {}
238+
}
239+
240+
$self.attempts.store(0, Ordering::Relaxed);
241+
return Err(JsonRpcError::Transport(e));
242+
}
243+
Err(e) => {
244+
$self.attempts.store(0, Ordering::Relaxed);
245+
return Err(e);
246+
}
247+
}
248+
}
249+
250+
$self.attempts.store(0, Ordering::Relaxed);
251+
Err(JsonRpcError::Transport("All attempts errored".into()))
252+
}};
253+
}
254+
255+
impl Transport for SimpleHttpWithRetry {
256+
fn send_request(&self, req: Request) -> Result<Response, JsonRpcError> {
257+
impl_inner!(self, send_request, req)
258+
}
259+
260+
fn send_batch(&self, reqs: &[Request]) -> Result<Vec<Response>, JsonRpcError> {
261+
impl_inner!(self, send_batch, reqs)
262+
}
263+
264+
fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result {
265+
self.inner.fmt_target(f)
266+
}
267+
}
268+
198269
impl ConfigurableBlockchain for RpcBlockchain {
199270
type Config = RpcConfig;
200271

@@ -203,7 +274,23 @@ impl ConfigurableBlockchain for RpcBlockchain {
203274
fn from_config(config: &Self::Config) -> Result<Self, Error> {
204275
let wallet_url = format!("{}/wallet/{}", config.url, &config.wallet_name);
205276

206-
let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?;
277+
let mut builder = SimpleHttpTransport::builder()
278+
.url(&wallet_url)
279+
.map_err(|e| bitcoincore_rpc::Error::JsonRpc(e.into()))?;
280+
281+
let (user, pass) = bitcoincore_rpc::Auth::from(config.auth.clone()).get_user_pass()?;
282+
if let Some(user) = user {
283+
builder = builder.auth(user, pass);
284+
}
285+
286+
let transport = SimpleHttpWithRetry {
287+
inner: builder.build(),
288+
attempts: AtomicU8::new(0),
289+
limit: config.max_tries,
290+
};
291+
let jsonrpc_client = jsonrpc::client::Client::with_transport(transport);
292+
293+
let client = Client::from_jsonrpc(jsonrpc_client);
207294
let rpc_version = client.version()?;
208295

209296
info!("connected to '{}' with auth: {:?}", wallet_url, config.auth);
@@ -835,6 +922,10 @@ pub struct RpcBlockchainFactory {
835922
pub default_skip_blocks: u32,
836923
/// Sync parameters
837924
pub sync_params: Option<RpcSyncParams>,
925+
/// Max number of attempts before giving up and returning an error
926+
///
927+
/// Set to `0` preserve the old behavior of erroring immediately
928+
pub max_tries: u8,
838929
}
839930

840931
impl BlockchainFactory for RpcBlockchainFactory {
@@ -855,6 +946,7 @@ impl BlockchainFactory for RpcBlockchainFactory {
855946
checksum
856947
),
857948
sync_params: self.sync_params.clone(),
949+
max_tries: self.max_tries,
858950
})
859951
}
860952
}
@@ -882,6 +974,7 @@ mod test {
882974
network: Network::Regtest,
883975
wallet_name: format!("client-wallet-test-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() ),
884976
sync_params: None,
977+
max_tries: 5,
885978
};
886979
RpcBlockchain::from_config(&config).unwrap()
887980
}
@@ -899,6 +992,7 @@ mod test {
899992
wallet_name_prefix: Some("prefix-".into()),
900993
default_skip_blocks: 0,
901994
sync_params: None,
995+
max_tries: 3,
902996
};
903997

904998
(test_client, factory)

0 commit comments

Comments
 (0)