Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): sdk-level retry logic for fetch and fetch_many #2266

Merged
merged 37 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7a77ccf
feat(sdk): provide request execution information
shumkov Oct 21, 2024
b85f52e
test: fix compilation errors
shumkov Oct 21, 2024
6de23ab
refactor: rename `unwrap` to `into_inner`
shumkov Oct 21, 2024
fba6ef8
refactor: rename response to inner
shumkov Oct 21, 2024
8af6776
test: increase timeout to pass the test
shumkov Oct 21, 2024
8e278e3
fix: `into_inner` doesn't need `CanRetry` bound
shumkov Oct 21, 2024
5af8822
fix: errored address is none
shumkov Oct 21, 2024
8269c1f
Merge branch 'v1.4-dev' into feat/sdk/execution_info
shumkov Oct 22, 2024
a9a96a0
chore: don't use empty uri
shumkov Oct 22, 2024
920b50d
docs: fix usage example
shumkov Oct 22, 2024
067db87
refactor: remove unused import
shumkov Oct 22, 2024
c684b3f
chore: update test vectors
shumkov Oct 22, 2024
9da89a8
chore: more test vectors
shumkov Oct 22, 2024
34ffdfb
feat(sdk): retry failed requests on sdk level
lklimek Oct 21, 2024
60d220c
fix(dapi-client): impl VersionedGrpcResponse for ExecutionResponse
lklimek Oct 22, 2024
80d19e0
feat(sdk): fech with retries
lklimek Oct 23, 2024
f78555e
chore(sdk): remove unnecessary changes
lklimek Oct 24, 2024
4343b6e
fix(sdk): correct retry logic
lklimek Oct 24, 2024
50ad84c
feat(sdk): retry impl
lklimek Oct 24, 2024
e4e667a
chore: self-review
lklimek Oct 24, 2024
ac388a8
chore: apply feedback
lklimek Oct 25, 2024
016450f
chore: remove outdated comment
lklimek Oct 25, 2024
0738f76
chore(sdk): impl<T> From<ExecutionError<T>> for Error
lklimek Oct 25, 2024
438185f
doc(sdk): comment on sync::retry() logic
lklimek Oct 25, 2024
170d5a9
chore: self review
lklimek Oct 25, 2024
e20a29d
refactor: remove unused code
lklimek Oct 25, 2024
c9c6c2f
chore: fmt
lklimek Oct 25, 2024
e5e17a7
chore: doc
lklimek Oct 25, 2024
dace7fa
test: fix retry test
lklimek Oct 25, 2024
eca69f8
refactor(sdk): apply feedback
lklimek Oct 25, 2024
df706f4
test(sdk): add edge case to test_retry
lklimek Oct 25, 2024
1f954d5
fix: use settings in fetch_many
lklimek Oct 25, 2024
034bdb2
test(sdk): regenerate test vectors
lklimek Oct 28, 2024
1bc31a9
refactor: re-export specific types
shumkov Oct 28, 2024
5a497ca
Merge remote-tracking branch 'origin/feat/sdk/execution_info' into fe…
lklimek Oct 28, 2024
723b1c5
chore: fixes after merge
lklimek Oct 28, 2024
cebc709
Merge remote-tracking branch 'origin/v1.4-dev' into feat/sdk-retry
lklimek Oct 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/rs-dapi-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ edition = "2021"

[features]
default = ["mocks", "offline-testing"]
tokio-sleep = ["backon/tokio-sleep"]
mocks = [
"dep:sha2",
"dep:hex",
Expand All @@ -20,7 +19,7 @@ dump = ["mocks"]
offline-testing = []

[dependencies]
backon = { version = "1.2"}
backon = { version = "1.2", features = ["tokio-sleep"] }
dapi-grpc = { path = "../dapi-grpc", features = [
"core",
"platform",
Expand Down
15 changes: 15 additions & 0 deletions packages/rs-dapi-client/src/address_list.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Subsystem to manage DAPI nodes.

use chrono::Utc;
use dapi_grpc::tonic::codegen::http;
use dapi_grpc::tonic::transport::Uri;
use rand::{rngs::SmallRng, seq::IteratorRandom, SeedableRng};
use std::collections::HashSet;
Expand All @@ -20,6 +21,16 @@ pub struct Address {
uri: Uri,
}

impl FromStr for Address {
type Err = AddressListError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Uri::from_str(s)
.map(Address::from)
.map_err(AddressListError::from)
}
}

impl PartialEq<Self> for Address {
fn eq(&self, other: &Self) -> bool {
self.uri == other.uri
Expand Down Expand Up @@ -81,6 +92,9 @@ impl Address {
pub enum AddressListError {
#[error("address {0} not found in the list")]
AddressNotFound(#[cfg_attr(feature = "mocks", serde(with = "http_serde::uri"))] Uri),
#[error("unable parse address: {0}")]
#[cfg_attr(feature = "mocks", serde(skip))]
InvalidAddressUri(#[from] http::uri::InvalidUri),
}

/// A structure to manage DAPI addresses to select from
Expand Down Expand Up @@ -200,6 +214,7 @@ impl AddressList {
}
}

// TODO: Must be changed to FromStr
impl From<&str> for AddressList {
fn from(value: &str) -> Self {
let uri_list: Vec<Uri> = value
Expand Down
105 changes: 58 additions & 47 deletions packages/rs-dapi-client/src/dapi_client.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! [DapiClient] definition.

use backon::{ExponentialBuilder, Retryable};
use backon::{ConstantBuilder, Retryable};
use dapi_grpc::mock::Mockable;
use dapi_grpc::tonic::async_trait;
use std::fmt::Debug;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tracing::Instrument;
Expand All @@ -12,19 +13,17 @@ use crate::address_list::AddressListError;
use crate::connection_pool::ConnectionPool;
use crate::{
transport::{TransportClient, TransportRequest},
Address, AddressList, CanRetry, RequestSettings,
Address, AddressList, CanRetry, DapiRequestExecutor, ExecutionError, ExecutionResponse,
ExecutionResult, RequestSettings,
};

/// General DAPI request error type.
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
pub enum DapiClientError<TE: Mockable> {
/// The error happened on transport layer
#[error("transport error with {1}: {0}")]
Transport(
#[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))] TE,
Address,
),
#[error("transport error: {0}")]
Transport(#[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))] TE),
/// There are no valid DAPI addresses to use.
#[error("no available addresses to use")]
NoAvailableAddresses,
Expand All @@ -43,7 +42,7 @@ impl<TE: CanRetry + Mockable> CanRetry for DapiClientError<TE> {
use DapiClientError::*;
match self {
NoAvailableAddresses => false,
Transport(transport_error, _) => transport_error.can_retry(),
Transport(transport_error) => transport_error.can_retry(),
AddressList(_) => false,
#[cfg(feature = "mocks")]
Mock(_) => false,
Expand Down Expand Up @@ -73,21 +72,6 @@ impl<TE: Mockable> Mockable for DapiClientError<TE> {
}
}

#[async_trait]
/// DAPI client executor trait.
pub trait DapiRequestExecutor {
/// Execute request using this DAPI client.
async fn execute<R>(
&self,
request: R,
settings: RequestSettings,
) -> Result<R::Response, DapiClientError<<R::Client as TransportClient>::Error>>
where
R: TransportRequest + Mockable,
R::Response: Mockable,
<R::Client as TransportClient>::Error: Mockable;
}

/// Access point to DAPI.
#[derive(Debug, Clone)]
pub struct DapiClient {
Expand Down Expand Up @@ -126,7 +110,7 @@ impl DapiRequestExecutor for DapiClient {
&self,
request: R,
settings: RequestSettings,
) -> Result<R::Response, DapiClientError<<R::Client as TransportClient>::Error>>
) -> ExecutionResult<R::Response, DapiClientError<<R::Client as TransportClient>::Error>>
where
R: TransportRequest + Mockable,
R::Response: Mockable,
Expand All @@ -140,24 +124,25 @@ impl DapiRequestExecutor for DapiClient {
.finalize();

// Setup retry policy:
let retry_settings = ExponentialBuilder::default()
let retry_settings = ConstantBuilder::default()
.with_max_times(applied_settings.retries)
// backon doesn't accept 1.0
.with_factor(1.001)
.with_min_delay(Duration::from_secs(0))
.with_max_delay(Duration::from_secs(0));
.with_delay(Duration::from_millis(10));

// Save dump dir for later use, as self is moved into routine
#[cfg(feature = "dump")]
let dump_dir = self.dump_dir.clone();
#[cfg(feature = "dump")]
let dump_request = request.clone();

let retries_counter_arc = Arc::new(AtomicUsize::new(0));
let retries_counter_arc_ref = &retries_counter_arc;

// Setup DAPI request execution routine future. It's a closure that will be called
// more once to build new future on each retry.
let routine = move || {
// Try to get an address to initialize transport on:
let retries_counter = Arc::clone(retries_counter_arc_ref);

// Try to get an address to initialize transport on:
let address_list = self
.address_list
.read()
Expand Down Expand Up @@ -192,30 +177,29 @@ impl DapiRequestExecutor for DapiClient {
async move {
// It stays wrapped in `Result` since we want to return
// `impl Future<Output = Result<...>`, not a `Result` itself.
let address = address_result?;
let address = address_result.map_err(|inner| ExecutionError {
inner,
retries: retries_counter.load(std::sync::atomic::Ordering::Acquire),
address: None,
})?;

let pool = self.pool.clone();

let mut transport_client = R::Client::with_uri_and_settings(
address.uri().clone(),
&applied_settings,
&pool,
)
.map_err(|e| {
DapiClientError::<<R::Client as TransportClient>::Error>::Transport(
e,
address.clone(),
)
.map_err(|error| ExecutionError {
inner: DapiClientError::Transport(error),
retries: retries_counter.load(std::sync::atomic::Ordering::Acquire),
address: Some(address.clone()),
})?;

let response = transport_request
.execute_transport(&mut transport_client, &applied_settings)
.await
.map_err(|e| {
DapiClientError::<<R::Client as TransportClient>::Error>::Transport(
e,
address.clone(),
)
});
.map_err(DapiClientError::Transport);

match &response {
Ok(_) => {
Expand All @@ -226,8 +210,14 @@ impl DapiRequestExecutor for DapiClient {
.write()
.expect("can't get address list for write");

address_list.unban_address(&address)
.map_err(DapiClientError::<<R::Client as TransportClient>::Error>::AddressList)?;
address_list.unban_address(&address).map_err(|error| {
ExecutionError {
inner: DapiClientError::AddressList(error),
retries: retries_counter
.load(std::sync::atomic::Ordering::Acquire),
address: Some(address.clone()),
}
})?;
}

tracing::trace!(?response, "received {} response", response_name);
Expand All @@ -240,16 +230,34 @@ impl DapiRequestExecutor for DapiClient {
.write()
.expect("can't get address list for write");

address_list.ban_address(&address)
.map_err(DapiClientError::<<R::Client as TransportClient>::Error>::AddressList)?;
address_list.ban_address(&address).map_err(|error| {
ExecutionError {
inner: DapiClientError::AddressList(error),
retries: retries_counter
.load(std::sync::atomic::Ordering::Acquire),
address: Some(address.clone()),
}
})?;
}
} else {
tracing::trace!(?error, "received error");
}
}
};

let retries = retries_counter.load(std::sync::atomic::Ordering::Acquire);

response
.map(|inner| ExecutionResponse {
inner,
retries,
address: address.clone(),
})
.map_err(|inner| ExecutionError {
inner,
retries,
address: Some(address),
})
}
};

Expand All @@ -258,11 +266,14 @@ impl DapiRequestExecutor for DapiClient {
let result = routine
.retry(retry_settings)
.notify(|error, duration| {
let retries_counter = Arc::clone(&retries_counter_arc);
retries_counter.fetch_add(1, std::sync::atomic::Ordering::AcqRel);

tracing::warn!(
?error,
"retrying error with sleeping {} secs",
duration.as_secs_f32()
)
);
})
.when(|e| e.can_retry())
.instrument(tracing::info_span!("request routine"))
Expand Down
Loading