diff options
| author | tv <tv@krebsco.de> | 2026-10-26 05:24:32 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 05:24:32 +0100 |
| commit | 223834c9dabdab00fceda69b9ce989995de47efa (patch) | |
| tree | cdc8bc55750436bc81fec6c6b67b036504f313c8 /src | |
| parent | c0ab6dbd13f26c7307b57a032da961bd6c49461c (diff) | |
remove unit suffixes from fields
Diffstat (limited to 'src')
| -rw-r--r-- | src/force.rs | 12 | ||||
| -rw-r--r-- | src/remake/api.rs | 57 | ||||
| -rw-r--r-- | src/remake/coordinator.rs | 2 | ||||
| -rw-r--r-- | src/units.rs | 24 |
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 { |
