feat(retry): more sensible backoff calc (#1413)
This commit is contained in:
parent
c7e1237a6b
commit
7603731d87
|
@ -35,6 +35,8 @@ where
|
||||||
policy: Box<dyn RetryPolicy<T::Error>>,
|
policy: Box<dyn RetryPolicy<T::Error>>,
|
||||||
max_retry: u32,
|
max_retry: u32,
|
||||||
initial_backoff: u64,
|
initial_backoff: u64,
|
||||||
|
/// available CPU per second
|
||||||
|
compute_units_per_second: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> RetryClient<T>
|
impl<T> RetryClient<T>
|
||||||
|
@ -60,7 +62,26 @@ where
|
||||||
// in milliseconds
|
// in milliseconds
|
||||||
initial_backoff: u64,
|
initial_backoff: u64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { inner, requests_enqueued: AtomicU32::new(0), policy, max_retry, initial_backoff }
|
Self {
|
||||||
|
inner,
|
||||||
|
requests_enqueued: AtomicU32::new(0),
|
||||||
|
policy,
|
||||||
|
max_retry,
|
||||||
|
initial_backoff,
|
||||||
|
// alchemy max cpus <https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups>
|
||||||
|
compute_units_per_second: 330,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the free compute units per second limit.
|
||||||
|
///
|
||||||
|
/// This is the maximum number of weighted request that can be handled per second by the
|
||||||
|
/// endpoint before rate limit kicks in.
|
||||||
|
///
|
||||||
|
/// This is used to guesstimate how long to wait until to retry again
|
||||||
|
pub fn set_compute_units(&mut self, cpus: u64) -> &mut Self {
|
||||||
|
self.compute_units_per_second = cpus;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +135,7 @@ where
|
||||||
A: std::fmt::Debug + Serialize + Send + Sync,
|
A: std::fmt::Debug + Serialize + Send + Sync,
|
||||||
R: DeserializeOwned,
|
R: DeserializeOwned,
|
||||||
{
|
{
|
||||||
self.requests_enqueued.fetch_add(1, Ordering::SeqCst);
|
let ahead_in_queue = self.requests_enqueued.fetch_add(1, Ordering::SeqCst) as u64;
|
||||||
|
|
||||||
let params =
|
let params =
|
||||||
serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
|
serde_json::to_value(params).map_err(|err| RetryClientError::SerdeJson(err))?;
|
||||||
|
@ -144,11 +165,31 @@ where
|
||||||
|
|
||||||
let should_retry = self.policy.should_retry(&err);
|
let should_retry = self.policy.should_retry(&err);
|
||||||
if should_retry {
|
if should_retry {
|
||||||
let current_queued_requests = self.requests_enqueued.load(Ordering::SeqCst);
|
let current_queued_requests = self.requests_enqueued.load(Ordering::SeqCst) as u64;
|
||||||
// using `retry_number + current_queued_requests` for creating back pressure because
|
// using `retry_number` for creating back pressure because
|
||||||
// of already queued requests
|
// of already queued requests
|
||||||
let next_backoff =
|
// this increases exponentially with retries and adds a delay based on how many
|
||||||
self.initial_backoff * 2u64.pow(retry_number + current_queued_requests);
|
// requests are currently queued
|
||||||
|
let mut next_backoff = self.initial_backoff * 2u64.pow(retry_number);
|
||||||
|
|
||||||
|
// requests are usually weighted and can vary from 10 CU to several 100 CU, cheaper
|
||||||
|
// requests are more common some example alchemy weights:
|
||||||
|
// - `eth_getStorageAt`: 17
|
||||||
|
// - `eth_getBlockByNumber`: 16
|
||||||
|
// - `eth_newFilter`: 20
|
||||||
|
//
|
||||||
|
// (coming from forking mode) assuming here that storage request will be the driver
|
||||||
|
// for Rate limits we choose `17` as the average cost of any request
|
||||||
|
const AVG_COST: u64 = 17u64;
|
||||||
|
let seconds_to_wait_for_compute_budge = compute_unit_offset_in_secs(
|
||||||
|
AVG_COST,
|
||||||
|
self.compute_units_per_second,
|
||||||
|
current_queued_requests,
|
||||||
|
ahead_in_queue,
|
||||||
|
);
|
||||||
|
// backoff is measured in millis
|
||||||
|
next_backoff += seconds_to_wait_for_compute_budge * 1000;
|
||||||
|
|
||||||
trace!("retrying and backing off for {}ms", next_backoff);
|
trace!("retrying and backing off for {}ms", next_backoff);
|
||||||
tokio::time::sleep(Duration::from_millis(next_backoff)).await;
|
tokio::time::sleep(Duration::from_millis(next_backoff)).await;
|
||||||
} else {
|
} else {
|
||||||
|
@ -177,3 +218,82 @@ impl RetryPolicy<ClientError> for HttpRateLimitRetryPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates an offset in seconds by taking into account the number of currently queued requests,
|
||||||
|
/// number of requests that were ahead in the queue when the request was first issued, the average
|
||||||
|
/// cost a weighted request (heuristic), and the number of available compute units per seconds.
|
||||||
|
///
|
||||||
|
/// Returns the number of seconds (the unit the remote endpoint measures compute budget) a request
|
||||||
|
/// is supposed to wait to not get rate limited. The budget per second is
|
||||||
|
/// `compute_units_per_second`, assuming an average cost of `avg_cost` this allows (in theory)
|
||||||
|
/// `compute_units_per_second / avg_cost` requests per seconds without getting rate limited.
|
||||||
|
/// By taking into account the number of concurrent request and the position in queue when the
|
||||||
|
/// request was first issued and determine the number of seconds a request is supposed to wait, if
|
||||||
|
/// at all
|
||||||
|
fn compute_unit_offset_in_secs(
|
||||||
|
avg_cost: u64,
|
||||||
|
compute_units_per_second: u64,
|
||||||
|
current_queued_requests: u64,
|
||||||
|
ahead_in_queue: u64,
|
||||||
|
) -> u64 {
|
||||||
|
let request_capacity_per_second = compute_units_per_second.saturating_div(avg_cost);
|
||||||
|
if current_queued_requests > request_capacity_per_second {
|
||||||
|
current_queued_requests.min(ahead_in_queue).saturating_div(request_capacity_per_second)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
// assumed average cost of a request
|
||||||
|
const AVG_COST: u64 = 17u64;
|
||||||
|
const COMPUTE_UNITS: u64 = 330u64;
|
||||||
|
|
||||||
|
fn compute_offset(current_queued_requests: u64, ahead_in_queue: u64) -> u64 {
|
||||||
|
compute_unit_offset_in_secs(
|
||||||
|
AVG_COST,
|
||||||
|
COMPUTE_UNITS,
|
||||||
|
current_queued_requests,
|
||||||
|
ahead_in_queue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_measure_unit_offset_single_request() {
|
||||||
|
let current_queued_requests = 1;
|
||||||
|
let ahead_in_queue = 0;
|
||||||
|
let to_wait = compute_offset(current_queued_requests, ahead_in_queue);
|
||||||
|
assert_eq!(to_wait, 0);
|
||||||
|
|
||||||
|
let current_queued_requests = 19;
|
||||||
|
let ahead_in_queue = 18;
|
||||||
|
let to_wait = compute_offset(current_queued_requests, ahead_in_queue);
|
||||||
|
assert_eq!(to_wait, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_measure_unit_offset_1x_over_budget() {
|
||||||
|
let current_queued_requests = 20;
|
||||||
|
let ahead_in_queue = 19;
|
||||||
|
let to_wait = compute_offset(current_queued_requests, ahead_in_queue);
|
||||||
|
// need to wait 1 second
|
||||||
|
assert_eq!(to_wait, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_measure_unit_offset_2x_over_budget() {
|
||||||
|
let current_queued_requests = 49;
|
||||||
|
let ahead_in_queue = 48;
|
||||||
|
let to_wait = compute_offset(current_queued_requests, ahead_in_queue);
|
||||||
|
// need to wait 1 second
|
||||||
|
assert_eq!(to_wait, 2);
|
||||||
|
|
||||||
|
let current_queued_requests = 49;
|
||||||
|
let ahead_in_queue = 20;
|
||||||
|
let to_wait = compute_offset(current_queued_requests, ahead_in_queue);
|
||||||
|
// need to wait 1 second
|
||||||
|
assert_eq!(to_wait, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue