aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-30 15:10:29 +0100
committertv <tv@krebsco.de>2026-10-30 15:10:29 +0100
commit4250d9843133d38c7ace1a1b510265a15ebaa0fd (patch)
tree3f996542020c3b44351a765fcf27119bc181f767
parent65d2118bcc9b7465c8086f2ca9c2b26c8ae7e57c (diff)
add dormant-probe-max option
-rw-r--r--src/config/mod.rs13
-rw-r--r--src/main.rs3
-rw-r--r--src/state/timing.rs21
-rw-r--r--src/units.rs11
4 files changed, 43 insertions, 5 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 85d71d2..99690ae 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -229,6 +229,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[
"Keepalive interval; dormancy, resync and probe timings derive from it.",
),
option(
+ "dormant-probe-max",
+ Kind::Duration,
+ "DURATION",
+ None,
+ "Longest wait between HELLOs to a member that doesn't answer (dormant, or unanswered HELLOs): they start at 10 keepalives and double up to this. It bounds how long a returning member, or the other side of a partition, takes to be noticed. Default: 60 keepalives (1h at 1m). Shorter than 10 keepalives, it is every wait.",
+ ),
+ option(
"neighbours",
Kind::Integer,
"COUNT",
@@ -443,6 +450,12 @@ pub(crate) struct Settings {
pub(crate) alias: Option<String>,
#[serde(default = "default_keepalive", with = "crate::units::duration_option")]
pub(crate) keepalive: Duration,
+ #[serde(
+ default,
+ deserialize_with = "crate::units::optional_duration",
+ serialize_with = "crate::units::serialize_optional_duration"
+ )]
+ pub(crate) dormant_probe_max: Option<Duration>,
#[serde(default = "default_neighbours")]
pub(crate) neighbours: usize,
#[serde(
diff --git a/src/main.rs b/src/main.rs
index 1cfd173..7481f38 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -253,7 +253,8 @@ async fn main() -> Result<()> {
initial_epoch,
initial_doc,
earlier_actors,
- timing: state::Timing::from_keepalive(args.keepalive, delivery_limit),
+ timing: state::Timing::from_keepalive(args.keepalive, delivery_limit)
+ .with_probe_max(args.dormant_probe_max),
neighbours_k: args.neighbours,
alias: args.alias.clone(),
electorate_window: args.electorate_window,
diff --git a/src/state/timing.rs b/src/state/timing.rs
index 9055ed6..988e430 100644
--- a/src/state/timing.rs
+++ b/src/state/timing.rs
@@ -11,8 +11,10 @@ const SYNC_REPLY_TIMEOUT: Duration = Duration::from_secs(10);
/// tries to deliver a message (`Transport::delivery_limit`).
const SYNC_REPLY_TIMEOUT_MAX_FACTOR: u32 = 30;
/// Upper bound on the wait between probes of an unreachable peer, as a multiple of
-/// `dormant_probe`: an hour at the default keepalive (1m). A returning node HELLOs its own
-/// neighbours, so this only bounds how long one we never hear from stays out.
+/// `dormant_probe`, unless `--dormant-probe-max` says otherwise: an hour at the default
+/// keepalive (1m). A returning node HELLOs its own neighbours, and once one pair across
+/// a partition meets, the rest are probed at once (`handshake::probe_now`), so this
+/// bounds how long one we never hear from stays out.
const PROBE_MAX_FACTOR: u32 = 6;
#[derive(Clone, Copy, Debug)]
@@ -25,6 +27,8 @@ pub(crate) struct Timing {
pub(crate) dormant_after: Duration,
pub(crate) resync_after: Duration,
pub(crate) dormant_probe: Duration,
+ /// The longest wait between probes (`probe_backoff`).
+ pub(crate) probe_max: Duration,
/// While catching up: send the source a STATUS after this much silence (`swarm::source`).
pub(crate) catch_up_probe: Duration,
/// First wait for an answer to a sync message before sending again; doubles while
@@ -81,6 +85,7 @@ impl Timing {
// the first sync reply timeout, so a lost message's sender resends it first.
resync_after: (keepalive * 2).max(sync_reply_timeout * 2),
dormant_probe: keepalive * 10, // >= 10*: dormant peers cost almost nothing, but still rejoin within minutes
+ probe_max: keepalive * 10 * PROBE_MAX_FACTOR,
// Seconds, not a keepalive interval: a dead source should cost little time.
// Only the source gets these, and only while catching up.
catch_up_probe: (keepalive / 30).clamp(Duration::from_secs(1), Duration::from_secs(5)),
@@ -89,10 +94,18 @@ impl Timing {
}
}
+ /// `--dormant-probe-max`, if given: the longest wait between probes.
+ pub(crate) fn with_probe_max(mut self, max: Option<Duration>) -> Self {
+ if let Some(max) = max {
+ self.probe_max = max;
+ }
+ self
+ }
+
/// Waits between probes of an unreachable peer: `dormant_probe`, doubling up to
- /// `PROBE_MAX_FACTOR` times as much.
+ /// `probe_max` (all of them `probe_max` if that is shorter).
pub(crate) fn probe_backoff(&self) -> Backoff {
- Backoff::new(self.dormant_probe, self.dormant_probe * PROBE_MAX_FACTOR)
+ Backoff::new(self.dormant_probe.min(self.probe_max), self.probe_max)
}
/// Waits for sync answers: `sync_reply_timeout`, doubling up to 30 times as much.
diff --git a/src/units.rs b/src/units.rs
index ac08ebf..28adab9 100644
--- a/src/units.rs
+++ b/src/units.rs
@@ -171,6 +171,17 @@ pub(crate) fn optional_duration<'de, D: serde::Deserializer<'de>>(
duration_option::deserialize(d).map(Some)
}
+/// Writes an optional `Duration` as [`duration_option`] does; `None` as null.
+pub(crate) fn serialize_optional_duration<S: serde::Serializer>(
+ value: &Option<Duration>,
+ s: S,
+) -> Result<S::Ok, S::Error> {
+ match value {
+ Some(value) => duration_option::serialize(value, s),
+ None => s.serialize_none(),
+ }
+}
+
/// Serde for a size option: a string as [`parse_size`] reads it, or a number of bytes;
/// written as [`format_size`].
pub(crate) mod size_option {