diff --git a/crates/app/src/sse/mod.rs b/crates/app/src/sse/mod.rs index ffaf45bc..bd6df884 100644 --- a/crates/app/src/sse/mod.rs +++ b/crates/app/src/sse/mod.rs @@ -392,12 +392,12 @@ impl SseListenerActor { /// failure. async fn fetch_config(client: &EthBeaconNodeApiClient) -> Result<(DateTime, Duration, u64)> { let genesis_time = (|| client.fetch_genesis_time()) - .retry(fast_backoff()) + .retry(pluto_core::expbackoff::fast()) .notify(|err, _| tracing::error!(err = ?err, "Failure fetching genesis time")) .await?; let (slot_duration, slots_per_epoch) = (|| client.fetch_slots_config()) - .retry(fast_backoff()) + .retry(pluto_core::expbackoff::fast()) .notify(|err, _| tracing::error!(err = ?err, "Failure fetching slots config")) .await?; @@ -503,18 +503,6 @@ async fn stream_once( } } -// TODO: Extract these backoff configurations into a shared module. - -/// Backoff used while waiting for the beacon node configuration. -fn fast_backoff() -> ExponentialBuilder { - ExponentialBuilder::default() - .with_min_delay(Duration::from_millis(100)) - .with_max_delay(Duration::from_secs(5)) - .with_factor(1.6) - .without_max_times() - .with_jitter() -} - /// Backoff used between SSE reconnection attempts. fn reconnect_backoff() -> ExponentialBuilder { ExponentialBuilder::default() diff --git a/crates/core/src/expbackoff.rs b/crates/core/src/expbackoff.rs new file mode 100644 index 00000000..05a6bb56 --- /dev/null +++ b/crates/core/src/expbackoff.rs @@ -0,0 +1,41 @@ +//! Exponential backoff builders mirroring Charon's `app/expbackoff` package. +//! +//! Charon defines two shared configurations (`app/expbackoff/expbackoff.go`): +//! `FastConfig` (100ms base, 5s max) for quick retries, and `DefaultConfig` +//! (1s base, 120s max) for slower loops. Both use a 1.6 multiplier and 0.2 +//! jitter and retry until the surrounding context is cancelled. +//! +//! Note on jitter: Charon applies a `±20%` multiplicative jitter, whereas +//! `backon`'s [`ExponentialBuilder::with_jitter`] adds a randomized delay in +//! `(0, base)`. This is an approximation, but it matches every existing +//! Pluto backoff call site, so consolidating here introduces no behavioral +//! change. + +use std::time::Duration; + +use backon::ExponentialBuilder; + +/// Backoff matching Charon's `expbackoff.FastConfig`: base=100ms, max=5s, +/// multiplier=1.6, jitter≈0.2. Retries until the surrounding cancellation +/// stops it (`without_max_times`), mirroring Go's "back off until the context +/// is cancelled" loops. +pub fn fast() -> ExponentialBuilder { + ExponentialBuilder::default() + .with_min_delay(Duration::from_millis(100)) + .with_max_delay(Duration::from_secs(5)) + .with_factor(1.6) + .without_max_times() + .with_jitter() +} + +/// Backoff matching Charon's `expbackoff.DefaultConfig`: base=1s, max=120s, +/// multiplier=1.6, jitter≈0.2. Retries until the surrounding cancellation +/// stops it (`without_max_times`). +pub fn default() -> ExponentialBuilder { + ExponentialBuilder::default() + .with_min_delay(Duration::from_secs(1)) + .with_max_delay(Duration::from_secs(120)) + .with_factor(1.6) + .without_max_times() + .with_jitter() +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 9f9e21ec..b1a244c1 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -30,6 +30,9 @@ pub mod clock; /// future. pub mod gater; +/// Exponential backoff builders mirroring Charon's `app/expbackoff`. +pub mod expbackoff; + /// parsigdb pub mod parsigdb; diff --git a/crates/core/src/scheduler.rs b/crates/core/src/scheduler.rs index 8cac3df7..ad6ec3c1 100644 --- a/crates/core/src/scheduler.rs +++ b/crates/core/src/scheduler.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, hash_map::Entry}, - time::Duration, -}; +use std::collections::{HashMap, hash_map::Entry}; use backon::{BackoffBuilder, Retryable}; use tokio::sync; @@ -719,39 +716,10 @@ async fn resolve_active_validators( Ok(validators) } -// TODO: Duplicated from `crates/p2p/src/bootnode.rs` -fn fast_backoff() -> backon::ExponentialBuilder { - /// Backoff configuration constants matching Go's expbackoff.FastConfig. - const FAST_BASE_DELAY: Duration = Duration::from_millis(100); - const FAST_MAX_DELAY: Duration = Duration::from_secs(5); - const FAST_MULTIPLIER: f32 = 1.6; - - backon::ExponentialBuilder::default() - .with_min_delay(FAST_BASE_DELAY) - .with_max_delay(FAST_MAX_DELAY) - .with_factor(FAST_MULTIPLIER) - .without_max_times() - .with_jitter() -} - -fn default_backoff() -> backon::ExponentialBuilder { - /// Backoff configuration constants matching Go's expbackoff.DefaultConfig. - const DEFAULT_BASE_DELAY: Duration = Duration::from_secs(1); - const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(120); - const DEFAULT_MULTIPLIER: f32 = 1.6; - - backon::ExponentialBuilder::default() - .with_min_delay(DEFAULT_BASE_DELAY) - .with_max_delay(DEFAULT_MAX_DELAY) - .with_factor(DEFAULT_MULTIPLIER) - .without_max_times() - .with_jitter() -} - /// Blocks until the beacon chain has started. async fn wait_chain_start(client: &pluto_eth2api::BeaconNodeClient) -> Result<()> { let fetch = || client.api().fetch_genesis_time(); - let backoff = fast_backoff(); + let backoff = crate::expbackoff::fast(); let genesis_time = fetch .retry(backoff) .notify(|err, _| tracing::error!(err = ?err, "Failure getting genesis")) @@ -777,9 +745,9 @@ async fn wait_beacon_sync(client: &pluto_eth2api::BeaconNodeClient) -> Result<() .api() .get_syncing_status(pluto_eth2api::GetSyncingStatusRequest {}) }; - let fetch_backoff = fast_backoff(); + let fetch_backoff = crate::expbackoff::fast(); - let mut is_syncing_backoff = default_backoff().build(); + let mut is_syncing_backoff = crate::expbackoff::default().build(); loop { let response: pluto_eth2api::GetSyncingStatusResponse = fetch @@ -1036,7 +1004,7 @@ async fn fetch_sync_committee_duties( #[cfg(test)] mod tests { - use std::collections::HashSet; + use std::{collections::HashSet, time::Duration}; use pluto_eth2api::{ BeaconNodeClient, GetStateValidatorsResponseResponse, diff --git a/crates/p2p/src/bootnode.rs b/crates/p2p/src/bootnode.rs index b35e8125..f62385f1 100644 --- a/crates/p2p/src/bootnode.rs +++ b/crates/p2p/src/bootnode.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use backon::{ExponentialBuilder, Retryable}; +use backon::Retryable; use libp2p::Multiaddr; use pluto_eth2util::enr::Record; use tokio_util::sync::CancellationToken; @@ -12,11 +12,6 @@ use crate::peer::{ AddrInfo, MutablePeer, Peer, PeerError, addr_infos_from_p2p_addrs, peer_id_from_key, }; -/// Backoff configuration constants matching Go's expbackoff.FastConfig. -const FAST_BASE_DELAY: Duration = Duration::from_millis(100); -const FAST_MAX_DELAY: Duration = Duration::from_secs(5); -const FAST_MULTIPLIER: f32 = 1.6; - /// Polling interval for relay address updates. const RELAY_POLL_INTERVAL: Duration = Duration::from_secs(120); // 2 minutes @@ -235,12 +230,9 @@ async fn query_relay_addrs( return Err(BootnodeError::InvalidRelayUrl); } - // Retry with exponential backoff - let backoff = ExponentialBuilder::default() - .with_min_delay(FAST_BASE_DELAY) - .with_max_delay(FAST_MAX_DELAY) - .with_factor(FAST_MULTIPLIER) - .with_jitter(); + // Retry with exponential backoff until the cancel token fires, matching + // Go's `queryRelayAddrs` ("It retries until the context is cancelled"). + let backoff = pluto_core::expbackoff::fast(); let fetch = || async { if cancel.is_cancelled() {