From 504435444c65efc59c0f7c6881991296258538a7 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 26 Oct 2026 05:35:40 +0100 Subject: allow humand-friendly durations in API --- src/remake/api.rs | 35 ++++++++++++++++++++++++++++------- src/remake/local.rs | 6 +++--- src/remake/records.rs | 18 +++++++++--------- 3 files changed, 40 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/remake/api.rs b/src/remake/api.rs index 41d72ad..f5063e0 100644 --- a/src/remake/api.rs +++ b/src/remake/api.rs @@ -19,11 +19,12 @@ //! Request body for POST (all optional; `baseHeads` not for `compact`, whose base is //! fixed when it commits): //! `{ "kind": "compact", "spec": null, "baseHeads": ["", …], -//! "replayWindow": {"epochs": 1, "seconds": 604800}, -//! "archiveRetention": {"epochs": 2, "seconds": 2592000}, +//! "replayWindow": {"epochs": 1, "age": "7d"}, +//! "archiveRetention": {"epochs": 2, "age": "30d"}, //! "settle": "8m", "switchDelay": "1m" }` //! -//! `settle` and `switchDelay` are durations (`units`: seconds, or e.g. `500ms`, `8m`). +//! `settle`, `switchDelay` and the retentions' `age` are durations (`units`: seconds, +//! or e.g. `500ms`, `8m`, `7d`). //! //! `?force=` overrides checks: `true` all of them, or a comma-separated list of //! * `switchDelay`: accept one below a keepalive interval (for tests and debugging, @@ -164,8 +165,8 @@ struct StartRequest { /// them are carried over. The current heads if absent. For a remake derived from /// a read, use the heads that read returned (`GET /store?heads=true`). base_heads: Option>, - replay_window: Option, - archive_retention: Option, + replay_window: Option, + archive_retention: Option, /// How long votes are collected; twice the dormancy threshold if absent. #[serde(default, with = "crate::units::optional_duration")] settle: Option, @@ -175,6 +176,26 @@ struct StartRequest { switch_delay: Option, } +/// A retention as a request gives it: the age as a duration. +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct RetentionRequest { + epochs: Option, + #[serde(default, with = "crate::units::optional_duration")] + age: Option, +} + +impl From for Retention { + fn from(request: RetentionRequest) -> Self { + Retention { + epochs: request.epochs, + age_ms: request + .age + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)), + } + } +} + /// What `?force=` may override on a proposal (`force`). const FORCE: &[&str] = &["switchDelay", "supersede", "coordinatorActive"]; /// What `?force=` may override on an abort. @@ -488,10 +509,10 @@ async fn start_kind( policy: Policy { replay_window: request .replay_window - .unwrap_or_else(Policy::default_replay_window), + .map_or_else(Policy::default_replay_window, Retention::from), archive_retention: request .archive_retention - .unwrap_or_else(Policy::default_archive_retention), + .map_or_else(Policy::default_archive_retention, Retention::from), }, settle_ms, proposed_at: now, diff --git a/src/remake/local.rs b/src/remake/local.rs index 8e9d8ba..c6a4c18 100644 --- a/src/remake/local.rs +++ b/src/remake/local.rs @@ -399,7 +399,7 @@ mod tests { AutoCommit::new(), Retention { epochs: Some(1), - seconds: None, + age_ms: None, }, 1, now, @@ -414,7 +414,7 @@ mod tests { AutoCommit::new(), Retention { epochs: Some(0), - seconds: Some(3600), + age_ms: Some(3_600_000), }, 5, now, @@ -427,7 +427,7 @@ mod tests { AutoCommit::new(), Retention { epochs: None, - seconds: None, + age_ms: None, }, 3, now, diff --git a/src/remake/records.rs b/src/remake/records.rs index 119ff0c..9e2b14e 100644 --- a/src/remake/records.rs +++ b/src/remake/records.rs @@ -40,7 +40,7 @@ use crate::document::replica::Epoch; use crate::document::store::{self, StoreError}; /// One day, for retention defaults. -const DAY_SECS: u64 = 24 * 60 * 60; +const DAY_MS: u64 = 24 * 60 * 60 * 1000; // --- record types ------------------------------------------------------------ @@ -71,13 +71,15 @@ impl EpochRecord { } /// How long something is kept. A value is kept until every given limit has passed. +/// Requests give the age as a duration (`api`, `RetentionRequest`); records hold it in +/// milliseconds. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Retention { #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) epochs: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) seconds: Option, + pub(crate) age_ms: Option, } impl Retention { @@ -85,9 +87,7 @@ impl Retention { /// `age_ms` milliseconds after the fact. pub(crate) fn covers(&self, epochs: u64, age_ms: u64) -> bool { self.epochs.is_some_and(|limit| epochs <= limit) - || self - .seconds - .is_some_and(|limit| age_ms <= limit.saturating_mul(1000)) + || self.age_ms.is_some_and(|limit| age_ms <= limit) } } @@ -107,14 +107,14 @@ impl Policy { pub(crate) fn default_replay_window() -> Retention { Retention { epochs: Some(1), - seconds: Some(7 * DAY_SECS), + age_ms: Some(7 * DAY_MS), } } pub(crate) fn default_archive_retention() -> Retention { Retention { epochs: Some(2), - seconds: Some(30 * DAY_SECS), + age_ms: Some(30 * DAY_MS), } } } @@ -515,14 +515,14 @@ mod tests { fn retention_covers_until_every_limit_passed() { let both = Retention { epochs: Some(1), - seconds: Some(10), + age_ms: Some(10_000), }; assert!(both.covers(1, 60_000)); // epochs still within assert!(both.covers(5, 5_000)); // time still within assert!(!both.covers(2, 10_001)); let none = Retention { epochs: None, - seconds: None, + age_ms: None, }; assert!(!none.covers(0, 0)); } -- cgit v1.3.1