Files
wanderbooks/crates/ledger-host/src/knobs.rs
T
swaitsandClaude Opus 5.5 019d8d76a3 P2 slice 7: backups and restore (D-30, D-31)
The server backs each book up itself: a per-book git archive, pushed
fast-forward only to a git remote and to a folder, when the books open,
after a quiet spell, before they close and on request; every commit,
push and drill is a record in ops/backup.jsonl. Drills clone each
target and verify the chain, the blobs, the vault and the head, weekly
and after a failure. `wanderbooks books restore` previews, then
restores beside or (with --replace) over, never moving books something
holds. The archive refuses anything but a plain bare repository and
pins git against the owner's configuration. books.backup and
books.back_up_now for the agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-20 11:14:21 -06:00

192 lines
7.4 KiB
Rust

//! The host's and bridge's timings, which tests shorten (`--test-knobs`).
//!
//! Every mode that starts another passes its knobs on, so a test sets
//! them once, on the holder it starts. Nothing else sets them: the
//! defaults are the plan's (3.9).
use std::time::Duration;
/// The timings in force.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Knobs {
/// How long the host waits after the last lease ends before it
/// closes the books (plan: 10 s).
pub close_after: Duration,
/// How often the host looks whether its executable was replaced.
pub exe_poll: Duration,
/// The first wait before a holder starts a failed host again; it
/// doubles each time (plan: 1, 2, 4 … 60 s, five tries).
pub backoff: Duration,
/// How long the host waits before starting the server (tests of a
/// slow start only; 0 otherwise).
pub server_delay: Duration,
/// How long the bridge waits for the server before answering
/// `initialize` itself (plan: 5 s).
pub init_wait: Duration,
/// How long the bridge holds a call while the books open (plan: 60 s).
pub queue_wait: Duration,
/// How long a command waits for the books to open.
pub open_wait: Duration,
/// Keep agents' tokens in files in the root's `run/`, not the OS
/// keyring (tests: tokens.rs INVARIANT 3). `file_keyring=1`.
pub file_keyring: bool,
/// How long the books stay unwritten before the server backs them up
/// (plan 3.7: 30 s), passed to the server as `--backup-quiet`.
pub backup_quiet: Duration,
/// How long after books open the server first looks for a drill due
/// (60 s), passed to the server as `--drill-first`.
pub drill_first: Duration,
}
impl Default for Knobs {
fn default() -> Self {
Self {
close_after: Duration::from_secs(10),
exe_poll: Duration::from_secs(5),
backoff: Duration::from_secs(1),
server_delay: Duration::ZERO,
init_wait: Duration::from_secs(5),
queue_wait: Duration::from_secs(60),
open_wait: Duration::from_secs(120),
file_keyring: false,
backup_quiet: Duration::from_secs(30),
drill_first: Duration::from_secs(60),
}
}
}
/// The flag that carries them.
pub const FLAG: &str = "--test-knobs";
impl Knobs {
/// Read `name=millis,...` (any subset; the rest stay default).
pub fn parse(text: &str) -> Result<Self, String> {
let mut k = Self::default();
for pair in text.split(',').filter(|p| !p.is_empty()) {
let (name, value) = pair
.split_once('=')
.ok_or_else(|| format!("{FLAG}: {pair:?} isn't name=milliseconds"))?;
if name == "file_keyring" {
k.file_keyring = match value {
"1" => true,
"0" => false,
_ => return Err(format!("{FLAG}: file_keyring is 1 or 0")),
};
continue;
}
let ms: u64 = value
.parse()
.map_err(|_| format!("{FLAG}: {value:?} isn't a number of milliseconds"))?;
let d = Duration::from_millis(ms);
match name {
"close_after" => k.close_after = d,
"exe_poll" => k.exe_poll = d,
"backoff" => k.backoff = d,
"server_delay" => k.server_delay = d,
"init_wait" => k.init_wait = d,
"queue_wait" => k.queue_wait = d,
"open_wait" => k.open_wait = d,
"backup_quiet" => k.backup_quiet = d,
"drill_first" => k.drill_first = d,
other => return Err(format!("{FLAG}: there is no knob {other:?}")),
}
}
Ok(k)
}
/// The flag's value that gives these knobs, or `None` for the
/// defaults (then nothing is passed on).
pub fn to_arg(&self) -> Option<String> {
let d = Self::default();
let mut parts = Vec::new();
for (name, now, default) in [
("close_after", self.close_after, d.close_after),
("exe_poll", self.exe_poll, d.exe_poll),
("backoff", self.backoff, d.backoff),
("server_delay", self.server_delay, d.server_delay),
("init_wait", self.init_wait, d.init_wait),
("queue_wait", self.queue_wait, d.queue_wait),
("open_wait", self.open_wait, d.open_wait),
("backup_quiet", self.backup_quiet, d.backup_quiet),
("drill_first", self.drill_first, d.drill_first),
] {
if now != default {
parts.push(format!("{name}={}", now.as_millis()));
}
}
if self.file_keyring {
parts.push("file_keyring=1".into());
}
(!parts.is_empty()).then(|| parts.join(","))
}
/// The arguments that pass these knobs on to another mode.
pub fn args(&self) -> Vec<String> {
match self.to_arg() {
Some(v) => vec![FLAG.to_string(), v],
None => Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_defaults_are_the_plans() {
let k = Knobs::default();
assert_eq!(k.close_after, Duration::from_secs(10));
assert_eq!(k.backoff, Duration::from_secs(1));
assert_eq!(k.init_wait, Duration::from_secs(5));
assert_eq!(k.queue_wait, Duration::from_secs(60));
assert_eq!(k.exe_poll, Duration::from_secs(5));
assert_eq!(k.server_delay, Duration::ZERO);
assert_eq!(k.open_wait, Duration::from_secs(120));
assert_eq!(k.backup_quiet, Duration::from_secs(30));
assert_eq!(k.drill_first, Duration::from_secs(60));
assert_eq!(k.to_arg(), None);
assert!(k.args().is_empty());
assert_eq!(Knobs::parse("").unwrap(), k);
}
#[test]
fn knobs_pass_on_exactly() {
let k = Knobs::parse(
"close_after=500,backoff=20,server_delay=3000,init_wait=100,queue_wait=900,exe_poll=50,open_wait=70,backup_quiet=40,drill_first=50",
)
.unwrap();
assert_eq!(k.close_after, Duration::from_millis(500));
assert_eq!(k.backoff, Duration::from_millis(20));
assert_eq!(k.server_delay, Duration::from_millis(3000));
assert_eq!(k.init_wait, Duration::from_millis(100));
assert_eq!(k.queue_wait, Duration::from_millis(900));
assert_eq!(k.exe_poll, Duration::from_millis(50));
assert_eq!(k.open_wait, Duration::from_millis(70));
assert_eq!(k.backup_quiet, Duration::from_millis(40));
assert_eq!(k.drill_first, Duration::from_millis(50));
let args = k.args();
assert_eq!(args[0], FLAG);
assert_eq!(Knobs::parse(&args[1]).unwrap(), k);
let keyring = Knobs::parse("file_keyring=1,backoff=5").unwrap();
assert!(keyring.file_keyring);
assert_eq!(Knobs::parse(&keyring.args()[1]).unwrap(), keyring);
assert!(!Knobs::parse("file_keyring=0").unwrap().file_keyring);
let one = Knobs::parse("close_after=1").unwrap();
assert_eq!(one.to_arg().as_deref(), Some("close_after=1"));
}
#[test]
fn a_bad_knob_is_refused() {
for bad in [
"close_after",
"close_after=x",
"nope=1",
"close_after=-1",
"file_keyring=yes",
] {
assert!(Knobs::parse(bad).is_err(), "{bad}");
}
}
}