summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-26 05:24:32 +0100
committertv <tv@krebsco.de>2026-10-26 05:24:32 +0100
commit223834c9dabdab00fceda69b9ce989995de47efa (patch)
treecdc8bc55750436bc81fec6c6b67b036504f313c8 /src
parentc0ab6dbd13f26c7307b57a032da961bd6c49461c (diff)
remove unit suffixes from fields
Diffstat (limited to 'src')
-rw-r--r--src/force.rs12
-rw-r--r--src/remake/api.rs57
-rw-r--r--src/remake/coordinator.rs2
-rw-r--r--src/units.rs24
4 files changed, 61 insertions, 34 deletions
diff --git a/src/force.rs b/src/force.rs
index 40f0ffc..ef1ca73 100644
--- a/src/force.rs
+++ b/src/force.rs
@@ -3,7 +3,7 @@
//! override, or is `true` for all of them (`false`, or nothing, for none).
//!
//! The same format everywhere: the `?force=` query parameter of the API and the
-//! `--force` option. Names are camelCase, like the API's fields (`switchDelaySecs`),
+//! `--force` option. Names are camelCase, like the API's fields (`switchDelay`),
//! on the command line too. An unknown name is an error, so a typo can't silently
//! override nothing.
@@ -52,17 +52,17 @@ impl Force {
mod tests {
use super::*;
- const KNOWN: &[&str] = &["switchDelaySecs", "supersede"];
+ const KNOWN: &[&str] = &["switchDelay", "supersede"];
#[test]
fn all_some_or_none() {
let all = Force::parse(["true"], KNOWN).unwrap();
- assert!(all.allows("supersede") && all.allows("switchDelaySecs"));
+ assert!(all.allows("supersede") && all.allows("switchDelay"));
let some = Force::parse(["supersede"], KNOWN).unwrap();
- assert!(some.allows("supersede") && !some.allows("switchDelaySecs"));
- let list = Force::parse(["switchDelaySecs, supersede"], KNOWN).unwrap();
- assert!(list.allows("supersede") && list.allows("switchDelaySecs"));
+ assert!(some.allows("supersede") && !some.allows("switchDelay"));
+ let list = Force::parse(["switchDelay, supersede"], KNOWN).unwrap();
+ assert!(list.allows("supersede") && list.allows("switchDelay"));
for none in [vec![], vec!["false"], vec![""]] {
assert!(!Force::parse(none, KNOWN).unwrap().allows("supersede"));
diff --git a/src/remake/api.rs b/src/remake/api.rs
index 515468d..ab1b01b 100644
--- a/src/remake/api.rs
+++ b/src/remake/api.rs
@@ -5,7 +5,7 @@
//! node becomes its coordinator. `202` with the report, or with `?wait=true` the final
//! report once decided. A dropped request does not abort anything. The remake
//! commits as soon as a majority of its electorate voted for it (`vote`), and aborts
-//! once that is out of reach or `settleSecs` passed without it.
+//! once that is out of reach or `settle` passed without it.
//! * `GET /admin/remakes` lists all remakes.
//! * `GET /admin/remakes/{id}` reports one; `?wait=true` long-polls until decided
//! (bounded by the proposal's own expiry).
@@ -22,10 +22,12 @@
//! `{ "kind": "compact", "spec": null, "baseHeads": ["<hex>", …],
//! "replayWindow": {"epochs": 1, "seconds": 604800},
//! "archiveRetention": {"epochs": 2, "seconds": 2592000},
-//! "settleSecs": 480, "switchDelaySecs": 60 }`
+//! "settle": "8m", "switchDelay": "1m" }`
+//!
+//! `settle` and `switchDelay` are durations (`units`: seconds, or e.g. `500ms`, `8m`).
//!
//! `?force=` overrides checks: `true` all of them, or a comma-separated list of
-//! * `switchDelaySecs`: accept one below a keepalive interval (for tests and debugging,
+//! * `switchDelay`: accept one below a keepalive interval (for tests and debugging,
//! not for real swarms);
//! * `supersede`: abort expired remakes of this epoch and propose in their place.
//! Without it, an expired remake still holding votes answers `409`.
@@ -39,8 +41,8 @@
//! downloads it (`content`). Requests may be up to 64 MiB.
//!
//! An import can also be uploaded as is, with `Content-Type: application/octet-stream`;
-//! then `rules` (JSON), `baseHeads` (comma-separated), `settleSecs` and
-//! `switchDelaySecs` go in the query. Restoring a backup, keeping writes made since at
+//! then `rules` (JSON), `baseHeads` (comma-separated), `settle` and `switchDelay` go
+//! in the query. Restoring a backup, keeping writes made since at
//! their paths:
//! `curl --data-binary @backup.automerge -H 'Content-Type: application/octet-stream' --url-query 'rules=[{"from":"","to":""}]' …/admin/remakes`
@@ -103,16 +105,18 @@ pub(crate) struct StartQuery {
rules: Option<String>,
/// Comma-separated hex.
base_heads: Option<String>,
- settle_secs: Option<u64>,
- switch_delay_secs: Option<u64>,
+ #[serde(default, with = "crate::units::optional_duration")]
+ settle: Option<Duration>,
+ #[serde(default, with = "crate::units::optional_duration")]
+ switch_delay: Option<Duration>,
}
impl StartQuery {
fn has_request_fields(&self) -> bool {
self.rules.is_some()
|| self.base_heads.is_some()
- || self.settle_secs.is_some()
- || self.switch_delay_secs.is_some()
+ || self.settle.is_some()
+ || self.switch_delay.is_some()
}
/// An import request for a document uploaded as is.
@@ -135,8 +139,8 @@ impl StartQuery {
.map(str::to_owned)
.collect()
}),
- settle_secs: self.settle_secs,
- switch_delay_secs: self.switch_delay_secs,
+ settle: self.settle,
+ switch_delay: self.switch_delay,
..StartRequest::default()
})
}
@@ -164,14 +168,16 @@ struct StartRequest {
replay_window: Option<Retention>,
archive_retention: Option<Retention>,
/// How long votes are collected; twice the dormancy threshold if absent.
- settle_secs: Option<u64>,
+ #[serde(default, with = "crate::units::optional_duration")]
+ settle: Option<Duration>,
/// How long nodes wait after seeing the commit before switching; one keepalive
- /// interval if absent. Shorter only with `?force=switchDelaySecs`.
- switch_delay_secs: Option<u64>,
+ /// interval if absent. Shorter only with `?force=switchDelay`.
+ #[serde(default, with = "crate::units::optional_duration")]
+ switch_delay: Option<Duration>,
}
/// What `?force=` may override on a proposal (`force`).
-const FORCE: &[&str] = &["switchDelaySecs", "supersede", "coordinatorActive"];
+const FORCE: &[&str] = &["switchDelay", "supersede", "coordinatorActive"];
/// What `?force=` may override on an abort.
const ABORT_FORCE: &[&str] = &["coordinatorActive"];
@@ -387,7 +393,7 @@ async fn start_kind<H: Host>(
} else if query.has_request_fields() {
return error_response(
StatusCode::BAD_REQUEST,
- "rules, baseHeads, settleSecs and switchDelaySecs go in the query only for an application/octet-stream import; put them in the JSON body",
+ "rules, baseHeads, settle and switchDelay go in the query only for an application/octet-stream import; put them in the JSON body",
None,
);
} else if body.is_empty() {
@@ -461,20 +467,17 @@ async fn start_kind<H: Host>(
// The voting period. No lower bound: the majority protects against competitors,
// a short period only risks an abort for lack of votes.
let dormant_after = host.dormant_after();
- let settle = request
- .settle_secs
- .map(Duration::from_secs)
- .unwrap_or(dormant_after * 2);
+ let settle = request.settle.unwrap_or(dormant_after * 2);
// The delay lets the commit reach every node through ordinary sync before any of
// them stops syncing the old epoch.
let keepalive = dormant_after / 4;
- let switch_delay = request.switch_delay_secs.map(Duration::from_secs);
+ let switch_delay = request.switch_delay;
match switch_delay {
- Some(delay) if delay < keepalive && force.allows("switchDelaySecs") => {
+ Some(delay) if delay < keepalive && force.allows("switchDelay") => {
warn!(
- switch_delay_secs = delay.as_secs(),
- keepalive_secs = keepalive.as_secs(),
+ switch_delay = %crate::units::format_duration(delay),
+ keepalive = %crate::units::format_duration(keepalive),
"Forced switch delay shorter than a keepalive interval; slow nodes may have to rejoin"
);
}
@@ -482,8 +485,8 @@ async fn start_kind<H: Host>(
return error_response(
StatusCode::BAD_REQUEST,
format!(
- "switchDelaySecs must be at least {} (one keepalive interval), so the commit reaches every node before they switch; add ?force=switchDelaySecs to override",
- keepalive.as_secs()
+ "switchDelay must be at least {} (one keepalive interval), so the commit reaches every node before they switch; add ?force=switchDelay to override",
+ crate::units::format_duration(keepalive)
),
None,
);
@@ -607,7 +610,7 @@ async fn start_kind<H: Host>(
info!(
remake = %id,
- settle_secs = settle.as_secs(),
+ settle = %crate::units::format_duration(settle),
"Remake proposed"
);
diff --git a/src/remake/coordinator.rs b/src/remake/coordinator.rs
index 20de712..6902515 100644
--- a/src/remake/coordinator.rs
+++ b/src/remake/coordinator.rs
@@ -6,7 +6,7 @@
//! * an operator requested an abort,
//! * the local epoch changed,
//! * a competing remake of the same epoch committed,
-//! * the voting period (`settleSecs`) passed without a majority, or
+//! * the voting period (`settle`) passed without a majority, or
//! * a majority is out of reach (too many members voted for a competing remake).
//!
//! The decision is written in the same step that checks the records once more, so a
diff --git a/src/units.rs b/src/units.rs
index e4576bc..5128b6e 100644
--- a/src/units.rs
+++ b/src/units.rs
@@ -162,6 +162,30 @@ pub(crate) mod duration_option {
}
}
+/// Serde for an optional `Duration` in a request (JSON body or query string), read as
+/// [`duration_option`] reads it; absent is `None`. Use with `#[serde(default)]`.
+pub(crate) mod optional_duration {
+ use std::time::Duration;
+
+ use serde::{Deserializer, Serializer};
+
+ pub(crate) fn serialize<S: Serializer>(
+ value: &Option<Duration>,
+ s: S,
+ ) -> Result<S::Ok, S::Error> {
+ match value {
+ Some(duration) => super::duration_option::serialize(duration, s),
+ None => s.serialize_none(),
+ }
+ }
+
+ pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
+ d: D,
+ ) -> Result<Option<Duration>, D::Error> {
+ super::duration_option::deserialize(d).map(Some)
+ }
+}
+
/// 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 {