Skip to content

Commit b0fa337

Browse files
authored
feat(sdk): detect stale nodes (#2254)
1 parent e7ad4f3 commit b0fa337

File tree

6 files changed

+400
-41
lines changed

6 files changed

+400
-41
lines changed

packages/rs-dapi-client/src/dapi_client.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ pub enum DapiClientError<TE: Mockable> {
3939
}
4040

4141
impl<TE: CanRetry + Mockable> CanRetry for DapiClientError<TE> {
42-
fn is_node_failure(&self) -> bool {
42+
fn can_retry(&self) -> bool {
4343
use DapiClientError::*;
4444
match self {
4545
NoAvailableAddresses => false,
46-
Transport(transport_error, _) => transport_error.is_node_failure(),
46+
Transport(transport_error, _) => transport_error.can_retry(),
4747
AddressList(_) => false,
4848
#[cfg(feature = "mocks")]
4949
Mock(_) => false,
@@ -233,7 +233,7 @@ impl DapiRequestExecutor for DapiClient {
233233
tracing::trace!(?response, "received {} response", response_name);
234234
}
235235
Err(error) => {
236-
if error.is_node_failure() {
236+
if !error.can_retry() {
237237
if applied_settings.ban_failed_address {
238238
let mut address_list = self
239239
.address_list
@@ -264,12 +264,12 @@ impl DapiRequestExecutor for DapiClient {
264264
duration.as_secs_f32()
265265
)
266266
})
267-
.when(|e| e.is_node_failure())
267+
.when(|e| e.can_retry())
268268
.instrument(tracing::info_span!("request routine"))
269269
.await;
270270

271271
if let Err(error) = &result {
272-
if error.is_node_failure() {
272+
if !error.can_retry() {
273273
tracing::error!(?error, "request failed");
274274
}
275275
}

packages/rs-dapi-client/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,16 @@ impl<T: transport::TransportRequest + Send> DapiRequest for T {
7171
}
7272
}
7373

74-
/// Allows to flag the transport error variant how tolerant we are of it and whether we can
75-
/// try to do a request again.
74+
/// Returns true if the operation can be retried.
7675
pub trait CanRetry {
76+
/// Returns true if the operation can be retried safely.
77+
fn can_retry(&self) -> bool;
78+
7779
/// Get boolean flag that indicates if the error is retryable.
78-
fn is_node_failure(&self) -> bool;
80+
///
81+
/// Depreacted in favor of [CanRetry::can_retry].
82+
#[deprecated = "Use !can_retry() instead"]
83+
fn is_node_failure(&self) -> bool {
84+
!self.can_retry()
85+
}
7986
}

packages/rs-dapi-client/src/transport/grpc.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@ impl TransportClient for CoreGrpcClient {
117117
}
118118

119119
impl CanRetry for dapi_grpc::tonic::Status {
120-
fn is_node_failure(&self) -> bool {
120+
fn can_retry(&self) -> bool {
121121
let code = self.code();
122122

123123
use dapi_grpc::tonic::Code::*;
124-
matches!(
124+
125+
!matches!(
125126
code,
126127
Ok | DataLoss
127128
| Cancelled

packages/rs-sdk/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2021"
66
[dependencies]
77

88
arc-swap = { version = "1.7.1" }
9+
chrono = { version = "0.4.38" }
910
dpp = { path = "../rs-dpp", default-features = false, features = [
1011
"dash-sdk-features",
1112
] }
@@ -52,7 +53,6 @@ data-contracts = { path = "../data-contracts" }
5253
tokio-test = { version = "0.4.4" }
5354
clap = { version = "4.5.4", features = ["derive"] }
5455
sanitize-filename = { version = "0.5.0" }
55-
chrono = { version = "0.4.38" }
5656
test-case = { version = "3.3.1" }
5757

5858
[features]

packages/rs-sdk/src/error.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::time::Duration;
55
use dapi_grpc::mock::Mockable;
66
use dpp::version::PlatformVersionError;
77
use dpp::ProtocolError;
8-
use rs_dapi_client::DapiClientError;
8+
use rs_dapi_client::{CanRetry, DapiClientError};
99

1010
pub use drive_proof_verifier::error::ContextProviderError;
1111

@@ -67,6 +67,10 @@ pub enum Error {
6767
/// Operation cancelled - cancel token was triggered, timeout, etc.
6868
#[error("Operation cancelled: {0}")]
6969
Cancelled(String),
70+
71+
/// Remote node is stale; try another server
72+
#[error(transparent)]
73+
StaleNode(#[from] StaleNodeError),
7074
}
7175

7276
impl<T: Debug + Mockable> From<DapiClientError<T>> for Error {
@@ -80,3 +84,36 @@ impl From<PlatformVersionError> for Error {
8084
Self::Protocol(value.into())
8185
}
8286
}
87+
88+
impl CanRetry for Error {
89+
fn can_retry(&self) -> bool {
90+
matches!(self, Error::StaleNode(..) | Error::TimeoutReached(_, _))
91+
}
92+
}
93+
94+
/// Server returned stale metadata
95+
#[derive(Debug, thiserror::Error)]
96+
pub enum StaleNodeError {
97+
/// Server returned metadata with outdated height
98+
#[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
99+
Height {
100+
/// Expected height - last block height seen by the Sdk
101+
expected_height: u64,
102+
/// Block height received from the server
103+
received_height: u64,
104+
/// Tolerance - how many blocks can be behind the expected height
105+
tolerance_blocks: u64,
106+
},
107+
/// Server returned metadata with time outside of the tolerance
108+
#[error(
109+
"received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
110+
)]
111+
Time {
112+
/// Expected time in milliseconds - is local time when the message was received
113+
expected_timestamp_ms: u64,
114+
/// Time received from the server in the message, in milliseconds
115+
received_timestamp_ms: u64,
116+
/// Tolerance in milliseconds
117+
tolerance_ms: u64,
118+
},
119+
}

0 commit comments

Comments
 (0)