feat(client): make reconnect exponential backoff configurable
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo clippy (push) Successful in 32s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 24s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Successful in 32s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m6s
Rust CI / cargo doc (push) Successful in 26s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Successful in 1m18s
Fuzz Smoke / cargo fuzz smoke (push) Successful in 1m39s
Netem Interop / netem interop (push) Successful in 1m22s
Supply chain / cargo-deny advisories (push) Successful in 19s
Supply chain / actionlint (push) Successful in 4s
Supply chain / cargo-deny bans licenses sources (push) Successful in 15s
Build binaries / Build (arm-musleabi) (push) Successful in 4m52s
Build binaries / Build (aarch64-musl) (push) Successful in 4m59s
Build binaries / Build (arm-musleabihf) (push) Successful in 4m17s
Build binaries / Build (armv7-musleabi) (push) Successful in 4m20s
Build binaries / Build (armv7-musleabihf) (push) Successful in 4m30s
Build binaries / Build (i686-musl) (push) Successful in 4m27s
Build binaries / Build (mips-musl) (push) Successful in 4m14s
Build binaries / Build (mips64-muslabi64) (push) Successful in 4m15s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 4m15s
Build binaries / Build (mipsel-musl) (push) Successful in 4m14s
Build binaries / Build (riscv64gc-musl) (push) Successful in 4m39s
Build binaries / Build (x86_64-musl) (push) Successful in 4m40s

This commit is contained in:
Nils Andreas Svee committed 2026-10-27 01:26:20 +07:00
1 parent 3344407706
commit 04e1f11f3a
7 files changed
+73 -10

No files matched your search

+13
View File
@@ -104,6 +104,12 @@ pub struct ManagedClientConfig {
/// Retry failed/closed sessions and detect silence while sending. Only valid
/// for continuous measurement runs. None (the default) disables reconnection.
pub reconnect_timeout: Option<Duration>,
/// First retry delay, also restored after a valid reply (default: 250 ms).
/// Must be positive and at most half of `reconnect_max_delay`.
pub reconnect_initial_delay: Duration,
/// Cap for per-target exponential backoff with a fixed multiplier of two
/// (default: 10 seconds). Must be schedulable from the current instant.
pub reconnect_max_delay: Duration,
}
impl Default for ManagedClientConfig {
@@ -118,6 +124,8 @@ impl Default for ManagedClientConfig {
max_live_target_generations: DEFAULT_MANAGED_MAX_LIVE_TARGET_GENERATIONS,
final_drain: DEFAULT_MANAGED_FINAL_DRAIN,
reconnect_timeout: None,
reconnect_initial_delay: Duration::from_millis(250),
reconnect_max_delay: Duration::from_secs(10),
}
}
}
@@ -370,6 +378,11 @@ pub struct ManagedTargetFailure {
pub enum ManagedConfigError {
#[error("reconnection requires continuous measurement mode and a positive, schedulable silence timeout")]
InvalidReconnectPolicy,
#[error("reconnection delays must be positive and schedulable, with maximum delay at least twice the initial delay (initial: {initial:?}, maximum: {maximum:?})")]
InvalidReconnectDelays {
initial: Duration,
maximum: Duration,
},
#[error("FinishWhenQuiescent requires at least one initial target")]
EmptyInitialTargets,
#[error("duplicate managed target id {id}")]
+21 -1
View File
@@ -367,6 +367,26 @@ fn build_task(
maximum: tokio::sync::Semaphore::MAX_PERMITS,
});
}
if config.reconnect_initial_delay.is_zero()
|| config.reconnect_max_delay.is_zero()
|| config
.reconnect_initial_delay
.checked_mul(2)
.is_none_or(|minimum| config.reconnect_max_delay < minimum)
// Tokio represents timer deadlines in u64 milliseconds, reserving the
// top two values for state. Leave room for its millisecond rounding.
|| config.reconnect_max_delay.as_millis() >= u128::from(u64::MAX - 2)
|| config
.reconnect_max_delay
.checked_add(std::time::Duration::from_millis(1))
.and_then(|delay| Instant::now().checked_add(delay))
.is_none()
{
return Err(ManagedConfigError::InvalidReconnectDelays {
initial: config.reconnect_initial_delay,
maximum: config.reconnect_max_delay,
});
}
if let Some(timeout) = config.reconnect_timeout {
if timeout.is_zero()
|| Instant::now().checked_add(timeout).is_none()
@@ -427,7 +447,7 @@ fn build_task(
remote: None,
counters: TargetCounters::default(),
sent_before_session: 0,
retry_delay: std::time::Duration::from_millis(250),
retry_delay: config.reconnect_initial_delay,
silence_since: None,
last_reply_at: None,
phase: Some(TargetPhase::Pending { client_config }),
+13 -6
View File
@@ -329,7 +329,7 @@ impl ManagedClientTask {
// A healthy sender with a long interval can remain idle.
self.targets[index].silence_since = None;
self.targets[index].last_reply_at = Some(received_at.mono);
self.targets[index].retry_delay = Duration::from_millis(250);
self.targets[index].retry_delay = self.config.reconnect_initial_delay;
}
_ => {}
}
@@ -344,7 +344,7 @@ impl ManagedClientTask {
fn finish_target(
&mut self,
index: usize,
end_reason: ManagedTargetEndReason,
mut end_reason: ManagedTargetEndReason,
cleanup_failure: Option<ManagedTargetFailure>,
) {
if self.can_reconnect(index)
@@ -359,10 +359,17 @@ impl ManagedClientTask {
target.sent_before_session = target.counters.packets_sent;
target.silence_since = None;
target.last_reply_at = None;
let deadline = Instant::now() + target.retry_delay;
target.retry_delay = (target.retry_delay * 2).min(Duration::from_secs(10));
self.install_target_phase(index, TargetPhase::Retrying { deadline });
return;
if let Some(deadline) = Instant::now().checked_add(target.retry_delay) {
target.retry_delay = target
.retry_delay
.checked_mul(2)
.unwrap_or(self.config.reconnect_max_delay)
.min(self.config.reconnect_max_delay);
self.install_target_phase(index, TargetPhase::Retrying { deadline });
return;
}
// A delay validated at construction may no longer fit after a long run.
end_reason = ManagedTargetEndReason::Failed(duration_overflow_failure());
}
let outcome = Arc::new(self.targets[index].outcome(end_reason, cleanup_failure));
// Finalize history before the phase publisher installs Terminal, updates
@@ -142,7 +142,7 @@ impl ManagedClientTask {
remote: None,
counters: TargetCounters::default(),
sent_before_session: 0,
retry_delay: std::time::Duration::from_millis(250),
retry_delay: self.config.reconnect_initial_delay,
silence_since: None,
last_reply_at: None,
phase: Some(TargetPhase::Pending {