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(RuntimeTransport): port cups/retry logic #6594

Merged
merged 8 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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: 0 additions & 1 deletion crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ extern crate self as foundry_common;
extern crate tracing;

pub mod abi;
pub mod alloy_runtime_transport;
pub mod calc;
pub mod clap_helpers;
pub mod compile;
Expand Down
15 changes: 9 additions & 6 deletions crates/common/src/provider/alloy.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Commonly used helpers to construct `Provider`s

use crate::{
alloy_runtime_transport::RuntimeTransportBuilder, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
provider::runtime_transport::RuntimeTransportBuilder, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
};
use alloy_primitives::U256;
use alloy_providers::provider::{Provider, TempProvider};
Expand Down Expand Up @@ -217,19 +217,22 @@ impl ProviderBuilder {
let ProviderBuilder {
url,
chain: _,
max_retry: _,
timeout_retry: _,
initial_backoff: _,
max_retry,
timeout_retry,
initial_backoff,
timeout,
compute_units_per_second: _,
compute_units_per_second,
jwt,
headers,
} = self;
let url = url?;

// todo: port alchemy compute units logic?
// todo: provider polling interval
let transport_builder = RuntimeTransportBuilder::new(url.clone())
.with_max_rate_limit_retries(max_retry)
.with_max_timeout_retries(timeout_retry)
.with_cups(compute_units_per_second)
.with_initial_backoff(initial_backoff)
.with_timeout(timeout)
.with_headers(headers)
.with_jwt(jwt);
Expand Down
2 changes: 2 additions & 0 deletions crates/common/src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

pub mod alloy;
pub mod ethers;
pub mod retry;
pub mod runtime_transport;
95 changes: 95 additions & 0 deletions crates/common/src/provider/retry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! An utility trait for retrying requests based on the error type. See [TransportError].
use alloy_json_rpc::ErrorPayload;
use alloy_transport::TransportError;
use serde::Deserialize;

/// [RetryPolicy] defines logic for which [JsonRpcClient::Error] instances should
/// the client retry the request and try to recover from.
pub trait RetryPolicy: Send + Sync + std::fmt::Debug {
/// Whether to retry the request based on the given `error`
fn should_retry(&self, error: &TransportError) -> bool;

/// Providers may include the `backoff` in the error response directly
fn backoff_hint(&self, error: &TransportError) -> Option<std::time::Duration>;
}

/// Implements [RetryPolicy] that will retry requests that errored with
/// status code 429 i.e. TOO_MANY_REQUESTS
///
/// Infura often fails with a `"header not found"` rpc error which is apparently linked to load
/// balancing, which are retried as well.
#[derive(Clone, Debug, Default)]
pub struct RateLimitRetryPolicy;

impl RetryPolicy for RateLimitRetryPolicy {
fn backoff_hint(&self, error: &TransportError) -> Option<std::time::Duration> {
if let TransportError::ErrorResp(resp) = error {
println!("resp: {:?}", resp);
let data = resp.try_data_as::<serde_json::Value>();
if let Some(Ok(data)) = data {
// if daily rate limit exceeded, infura returns the requested backoff in the error
// response
let backoff_seconds = &data["rate"]["backoff_seconds"];
// infura rate limit error
if let Some(seconds) = backoff_seconds.as_u64() {
return Some(std::time::Duration::from_secs(seconds))
}
if let Some(seconds) = backoff_seconds.as_f64() {
return Some(std::time::Duration::from_secs(seconds as u64 + 1))
}
}
}
None
}

fn should_retry(&self, error: &TransportError) -> bool {
match error {
TransportError::Transport(_) => true,
// The transport could not serialize the error itself. The request was malformed from
// the start.
TransportError::SerError(_) => false,
TransportError::DeserError { text, .. } => {
// some providers send invalid JSON RPC in the error case (no `id:u64`), but the
// text should be a `JsonRpcError`
#[derive(Deserialize)]
struct Resp {
error: ErrorPayload,
}

if let Ok(resp) = serde_json::from_str::<Resp>(text) {
return should_retry_json_rpc_error(&resp.error)
}
false
}
TransportError::ErrorResp(err) => should_retry_json_rpc_error(err),
}
}
}

/// Analyzes the [ErrorPayload] and decides if the request should be retried based on the
/// error code or the message.
fn should_retry_json_rpc_error(error: &ErrorPayload) -> bool {
let ErrorPayload { code, message, .. } = error;
// alchemy throws it this way
if *code == 429 {
return true
}

// This is an infura error code for `exceeded project rate limit`
if *code == -32005 {
return true
}

// alternative alchemy error for specific IPs
if *code == -32016 && message.contains("rate limit") {
return true
}

match message.as_str() {
// this is commonly thrown by infura and is apparently a load balancer issue, see also <https://github.com/MetaMask/metamask-extension/issues/7234>
"header not found" => true,
// also thrown by infura if out of budget for the day and ratelimited
"daily request count exceeded, request rate limited" => true,
_ => false,
}
}
Loading