From 00549beae6ccff6ffc505662c95172782437b8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sat, 17 Jan 2026 21:02:12 +0100 Subject: [PATCH 1/5] refactor: lift most of impl_inner_call! to function template --- src/client.rs | 119 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 48 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7e4e626..4380182 100644 --- a/src/client.rs +++ b/src/client.rs @@ -38,62 +38,85 @@ pub struct Client { macro_rules! impl_inner_call { ( $self:expr, $name:ident $(, $args:expr)* ) => { { - let mut errors = vec![]; - loop { - let read_client = $self.client_type.read().unwrap(); - let res = match &*read_client { + impl_inner_call_impl($self, || { + match &*$self.client_type.read().unwrap() { ClientType::TCP(inner) => inner.$name( $($args, )* ), ClientType::SSL(inner) => inner.$name( $($args, )* ), ClientType::Socks5(inner) => inner.$name( $($args, )* ), - }; - drop(read_client); - match res { - Ok(val) => return Ok(val), - Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => { - return res; - }, - Err(e) => { - let failed_attempts = errors.len() + 1; - - if retries_exhausted(failed_attempts, $self.config.retry()) { - warn!("call '{}' failed after {} attempts", stringify!($name), failed_attempts); - return Err(Error::AllAttemptsErrored(errors)); - } + } + }) + }} +} - warn!("call '{}' failed with {}, retry: {}/{}", stringify!($name), e, failed_attempts, $self.config.retry()); - - errors.push(e); - - // Only one thread will try to recreate the client getting the write lock, - // other eventual threads will get Err and will block at the beginning of - // previous loop when trying to read() - if let Ok(mut write_client) = $self.client_type.try_write() { - loop { - std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64)); - match ClientType::from_config(&$self.url, &$self.config) { - Ok(new_client) => { - info!("Succesfully created new client"); - *write_client = new_client; - break; - }, - Err(e) => { - let failed_attempts = errors.len() + 1; - - if retries_exhausted(failed_attempts, $self.config.retry()) { - warn!("re-creating client failed after {} attempts", failed_attempts); - return Err(Error::AllAttemptsErrored(errors)); - } - - warn!("re-creating client failed with {}, retry: {}/{}", e, failed_attempts, $self.config.retry()); - - errors.push(e); +fn impl_inner_call_impl(self_: &Client, f: impl Fn() -> Result) -> Result { + let mut errors = vec![]; + loop { + match f() { + Ok(val) => return Ok(val), + res @ Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => { + return res; + } + Err(e) => { + let failed_attempts = errors.len() + 1; + + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "call '{}' failed after {} attempts", + stringify!($name), + failed_attempts + ); + return Err(Error::AllAttemptsErrored(errors)); + } + + warn!( + "call '{}' failed with {}, retry: {}/{}", + stringify!($name), + e, + failed_attempts, + self_.config.retry() + ); + + errors.push(e); + + // Only one thread will try to recreate the client getting the write lock, + // other eventual threads will get Err and will block at the beginning of + // previous loop when trying to read() + if let Ok(mut write_client) = self_.client_type.try_write() { + loop { + std::thread::sleep(std::time::Duration::from_secs( + (1 << errors.len()).min(30) as u64, + )); + match ClientType::from_config(&self_.url, &self_.config) { + Ok(new_client) => { + info!("Succesfully created new client"); + *write_client = new_client; + break; + } + Err(e) => { + let failed_attempts = errors.len() + 1; + + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "re-creating client failed after {} attempts", + failed_attempts + ); + return Err(Error::AllAttemptsErrored(errors)); } + + warn!( + "re-creating client failed with {}, retry: {}/{}", + e, + failed_attempts, + self_.config.retry() + ); + + errors.push(e); } } } - }, + } } - }} + } } } @@ -178,7 +201,7 @@ impl ElectrumApi for Client { // `RawClient::internal_raw_call_with_vec` method. let vec = params.into_iter().collect::>(); - impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone()); + impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone()) } #[inline] From 91c9961f9e9cccf49fcc4c2d0d4f1d1a1a62f2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sat, 17 Jan 2026 21:07:55 +0100 Subject: [PATCH 2/5] refactor: lift impl_inner_call_impl error handler to non-generic function --- src/client.rs | 116 +++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4380182..1acbebf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -56,68 +56,76 @@ fn impl_inner_call_impl(self_: &Client, f: impl Fn() -> Result) -> res @ Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => { return res; } - Err(e) => { - let failed_attempts = errors.len() + 1; + Err(e) => impl_inner_call_impl_err(self_, &mut errors, e)?, + } + } +} + +fn impl_inner_call_impl_err( + self_: &Client, + errors: &mut Vec, + e: Error, +) -> Result<(), Error> { + let failed_attempts = errors.len() + 1; + + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "call '{}' failed after {} attempts", + stringify!($name), + failed_attempts + ); + return Err(Error::AllAttemptsErrored(std::mem::take(errors))); + } + + warn!( + "call '{}' failed with {}, retry: {}/{}", + stringify!($name), + e, + failed_attempts, + self_.config.retry() + ); + + errors.push(e); + + // Only one thread will try to recreate the client getting the write lock, + // other eventual threads will get Err and will block at the beginning of + // previous loop when trying to read() + if let Ok(mut write_client) = self_.client_type.try_write() { + loop { + std::thread::sleep(std::time::Duration::from_secs( + (1 << errors.len()).min(30) as u64 + )); + match ClientType::from_config(&self_.url, &self_.config) { + Ok(new_client) => { + info!("Succesfully created new client"); + *write_client = new_client; + break; + } + Err(e) => { + let failed_attempts = errors.len() + 1; + + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "re-creating client failed after {} attempts", + failed_attempts + ); + return Err(Error::AllAttemptsErrored(std::mem::take(errors))); + } - if retries_exhausted(failed_attempts, self_.config.retry()) { warn!( - "call '{}' failed after {} attempts", - stringify!($name), - failed_attempts + "re-creating client failed with {}, retry: {}/{}", + e, + failed_attempts, + self_.config.retry() ); - return Err(Error::AllAttemptsErrored(errors)); - } - warn!( - "call '{}' failed with {}, retry: {}/{}", - stringify!($name), - e, - failed_attempts, - self_.config.retry() - ); - - errors.push(e); - - // Only one thread will try to recreate the client getting the write lock, - // other eventual threads will get Err and will block at the beginning of - // previous loop when trying to read() - if let Ok(mut write_client) = self_.client_type.try_write() { - loop { - std::thread::sleep(std::time::Duration::from_secs( - (1 << errors.len()).min(30) as u64, - )); - match ClientType::from_config(&self_.url, &self_.config) { - Ok(new_client) => { - info!("Succesfully created new client"); - *write_client = new_client; - break; - } - Err(e) => { - let failed_attempts = errors.len() + 1; - - if retries_exhausted(failed_attempts, self_.config.retry()) { - warn!( - "re-creating client failed after {} attempts", - failed_attempts - ); - return Err(Error::AllAttemptsErrored(errors)); - } - - warn!( - "re-creating client failed with {}, retry: {}/{}", - e, - failed_attempts, - self_.config.retry() - ); - - errors.push(e); - } - } - } + errors.push(e); } } } } + + Ok(()) } fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool { From f752c980f58540078e2c0b018cabf9a2228678b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sat, 17 Jan 2026 21:36:39 +0100 Subject: [PATCH 3/5] Test impl_inner_call!() retry behaviour Test fails at this point --- src/client.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 1acbebf..4717ae2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -48,7 +48,10 @@ macro_rules! impl_inner_call { }} } -fn impl_inner_call_impl(self_: &Client, f: impl Fn() -> Result) -> Result { +fn impl_inner_call_impl( + self_: &Client, + mut f: impl FnMut() -> Result, +) -> Result { let mut errors = vec![]; loop { match f() { @@ -488,4 +491,42 @@ mod tests { assert!(client.is_ok()); assert!(elapsed > Duration::from_secs(2)); } + + #[test] + fn impl_inner_call_all_attempts_has_all_errors() { + use std::net::TcpListener; + use std::time::Duration; + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + std::thread::spawn(move || { + for _stream in listener.incoming() { + std::thread::sleep(Duration::from_secs(2)); + } + }); + + let client = Client::from_config( + &format!("127.0.0.1:{}", port), + crate::config::ConfigBuilder::new().retry(3).build(), + ) + .unwrap(); + let msg = |n| format!("error #{}", n); + + let mut n = 0; + let res: Result<(), _> = impl_inner_call_impl(&client, || { + n = n + 1; + Err(Error::Message(msg(n))) + }); + assert_eq!(n, 4); + + let err = res.unwrap_err(); + let Error::AllAttemptsErrored(vec) = err else { + panic!() + }; + assert_eq!(vec.len(), n); + for (i, err) in vec.into_iter().enumerate() { + let Error::Message(m) = err else { panic!() }; + assert_eq!(m, msg(i + 1)); + } + } } From 0339d62c1e780e5698ff61d95dab36dd82c2f5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 3 Dec 2025 23:39:09 +0100 Subject: [PATCH 4/5] fix: include latest error in `AllAttemptsErrored` Closes #186 --- src/client.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4717ae2..5504024 100644 --- a/src/client.rs +++ b/src/client.rs @@ -71,15 +71,6 @@ fn impl_inner_call_impl_err( ) -> Result<(), Error> { let failed_attempts = errors.len() + 1; - if retries_exhausted(failed_attempts, self_.config.retry()) { - warn!( - "call '{}' failed after {} attempts", - stringify!($name), - failed_attempts - ); - return Err(Error::AllAttemptsErrored(std::mem::take(errors))); - } - warn!( "call '{}' failed with {}, retry: {}/{}", stringify!($name), @@ -90,6 +81,15 @@ fn impl_inner_call_impl_err( errors.push(e); + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "call '{}' failed after {} attempts", + stringify!($name), + failed_attempts + ); + return Err(Error::AllAttemptsErrored(std::mem::take(errors))); + } + // Only one thread will try to recreate the client getting the write lock, // other eventual threads will get Err and will block at the beginning of // previous loop when trying to read() @@ -107,14 +107,6 @@ fn impl_inner_call_impl_err( Err(e) => { let failed_attempts = errors.len() + 1; - if retries_exhausted(failed_attempts, self_.config.retry()) { - warn!( - "re-creating client failed after {} attempts", - failed_attempts - ); - return Err(Error::AllAttemptsErrored(std::mem::take(errors))); - } - warn!( "re-creating client failed with {}, retry: {}/{}", e, @@ -123,6 +115,14 @@ fn impl_inner_call_impl_err( ); errors.push(e); + + if retries_exhausted(failed_attempts, self_.config.retry()) { + warn!( + "re-creating client failed after {} attempts", + failed_attempts + ); + return Err(Error::AllAttemptsErrored(std::mem::take(errors))); + } } } } From 283d0cbce17b993c03b1ab49be0b59992bad9a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Sat, 17 Jan 2026 21:41:45 +0100 Subject: [PATCH 5/5] refactor: bind to 127.0.0.1:60000 synchronously instead of synchronising around it --- src/client.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5504024..939b9ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -459,26 +459,18 @@ mod tests { // try. use std::net::TcpListener; - use std::sync::mpsc::channel; use std::time::{Duration, Instant}; let endpoint = std::env::var("TEST_ELECTRUM_TIMEOUT_PORT").unwrap_or("localhost:60000".into()); - let (sender, receiver) = channel(); + let listener = TcpListener::bind("127.0.0.1:60000").unwrap(); std::thread::spawn(move || { - let listener = TcpListener::bind("127.0.0.1:60000").unwrap(); - sender.send(()).unwrap(); - for _stream in listener.incoming() { std::thread::sleep(Duration::from_secs(60)) } }); - receiver - .recv_timeout(Duration::from_secs(5)) - .expect("Can't start local listener"); - let now = Instant::now(); let client = Client::from_config( &endpoint,