Use humantime to parse timeouts

In issue

    29c6c6fc8171287faa0079798ba2d6e3e7fd86f3

was noted that it would be nice to use value parsers for the timeouts in
the CLI.

This patch implements this.

Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
Co-Authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Matthias Beyer 2026-03-06 21:54:52 +01:00 committed by Fintan Halpenny
parent 52e5581228
commit 99d9242122
15 changed files with 53 additions and 33 deletions

View File

@ -66,6 +66,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `TypeName` strings defined in `radicle-cob` are restricted to reflect the
size limits on domain names as specified in
[RFC-1035](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4).
- The `--timeout` flag, which is available on a number of CLI subcommands, now
takes a human-readable duration as parameter, E.G. "9s" or "1min" instead of
"9" or "60".
## 1.6.1

7
Cargo.lock generated
View File

@ -1854,6 +1854,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "humantime"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@ -2939,6 +2945,7 @@ dependencies = [
"clap_complete",
"dunce",
"human-panic",
"humantime",
"itertools",
"log",
"nonempty",

View File

@ -33,6 +33,7 @@ git2 = { version = "0.20.4", default-features = false, features = ["vendored-lib
gix-hash = { version = "0.22.1", default-features = false, features = ["sha1"] }
gix-packetline = { version = "0.21.1", default-features = false }
human-panic = "2.0.6"
humantime = "2.3"
itertools = "0.14"
lexopt = "0.3.0"
libc = "0.2.137"

View File

@ -20,6 +20,7 @@ clap = { version = "4.5.44", features = ["derive"] }
clap_complete = "4.5"
dunce = { workspace = true }
human-panic.workspace = true
humantime.workspace = true
itertools.workspace = true
log = { workspace = true, features = ["std"] }
nonempty = { workspace = true }

View File

@ -15,7 +15,7 @@ When she tries to clone, one of those will fail to fetch. But the clone command
still returns successfully.
```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --timeout 3
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --timeout 3s
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 3 potential seed(s).
✓ Target met: 1 seed(s)

View File

@ -156,7 +156,7 @@ At this point, when Alice runs `rad sync`, she will fetch Eve's fork
since she has become a delegate:
``` ~alice
$ rad sync --timeout 3
$ rad sync --timeout 3s
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
✓ Fetched repository from 2 seed(s)

View File

@ -4,7 +4,7 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --des
```
``` ~bob
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000ms
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)

View File

@ -29,7 +29,7 @@ $ rad inspect --identity
``` ~bob
$ rad ls --all --private
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1s
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✓ Target met: 1 preferred seed(s).
@ -48,7 +48,7 @@ Run `cd ./heartwood` to go to the repository directory.
We can also use `rad seed` to seed and fetch without creating a checkout.
``` ~bob
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1s
✓ Seeding policy exists for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)

View File

@ -5,7 +5,7 @@ Bob tries to fetch it, and even though he's connected to Alice, it fails.
$ rad ls
```
``` ~bob (fail)
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1s
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✗ Target not met: could not fetch from [z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi], and required 1 more seed(s)
@ -20,7 +20,7 @@ the refs.
``` ~alice
$ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
...
$ rad sync --announce --timeout 3
$ rad sync --announce --timeout 3s
✓ Synced with 1 seed(s)
```

View File

@ -1,5 +1,4 @@
use std::path::PathBuf;
use std::time;
use clap::Parser;
@ -31,15 +30,17 @@ pub(super) struct SyncArgs {
#[arg(short, long = "seed", value_name = "NID", action = clap::ArgAction::Append)]
seeds: Vec<NodeId>,
/// Timeout for fetching repository in seconds
#[arg(long, default_value_t = 9, value_name = "SECS")]
timeout: usize,
/// Timeout for fetching repository
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration, default_value = "9s")]
timeout: std::time::Duration,
}
impl From<SyncArgs> for SyncSettings {
fn from(args: SyncArgs) -> Self {
SyncSettings {
timeout: time::Duration::from_secs(args.timeout as u64),
timeout: args.timeout,
seeds: args.seeds.into_iter().collect(),
..SyncSettings::default()
}

View File

@ -29,9 +29,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
match command {
Command::Connect { addr, timeout } => {
let timeout = timeout
.map(time::Duration::from_secs)
.unwrap_or(time::Duration::MAX);
let timeout = timeout.unwrap_or(time::Duration::MAX);
match addr {
Addr::Peer(addr) => control::connect(&mut node, addr.id, addr.addr, timeout)?,
Addr::Node(nid) => {
@ -70,9 +68,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
}
Command::Events { timeout, count } => {
let count = count.unwrap_or(usize::MAX);
let timeout = timeout
.map(time::Duration::from_secs)
.unwrap_or(time::Duration::MAX);
let timeout = timeout.unwrap_or(time::Duration::MAX);
events::run(node, count, timeout)?;
}

View File

@ -105,8 +105,10 @@ pub(super) enum Command {
addr: Addr,
/// How long to wait for the connection to be established
#[arg(long, value_name = "SECS")]
timeout: Option<u64>,
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration)]
timeout: Option<std::time::Duration>,
},
/// Show the config
@ -128,8 +130,10 @@ pub(super) enum Command {
/// If no timeout or count is specified, it will run indefinitely.
Events {
/// How long to wait to receive an event before giving up
#[arg(long, value_name = "SECS")]
timeout: Option<u64>,
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration)]
timeout: Option<std::time::Duration>,
/// Exit after <COUNT> events
#[arg(long, short = 'n')]

View File

@ -43,8 +43,10 @@ pub struct Args {
pub(super) from: Vec<NodeId>,
/// Fetch timeout in seconds
#[arg(long, short, value_name = "SECS", default_value_t = 9)]
timeout: u64,
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, short, value_parser = humantime::parse_duration, default_value = "9s")]
timeout: std::time::Duration,
/// Peer follow scope for this repository
#[arg(long, value_parser = terminal::args::ScopeParser)]
@ -86,7 +88,7 @@ impl From<Args> for Operation {
impl Args {
fn timeout(&self) -> time::Duration {
time::Duration::from_secs(self.timeout)
self.timeout
}
fn should_fetch(&self) -> bool {

View File

@ -89,15 +89,17 @@ pub(super) struct SyncArgs {
)]
seeds: Vec<NodeId>,
/// How many seconds to wait while synchronizing
/// How long to wait while synchronizing
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(
long,
short,
default_value_t = 9,
value_name = "SECS",
default_value = "9s",
value_parser = humantime::parse_duration,
conflicts_with = "inventory"
)]
timeout: u64,
timeout: std::time::Duration,
/// The repository to perform the synchronizing for [default: cwd]
rid: Option<RepoId>,
@ -146,7 +148,7 @@ impl SyncArgs {
}
fn timeout(&self) -> time::Duration {
time::Duration::from_secs(self.timeout)
self.timeout
}
fn replication(&self) -> sync::ReplicationFactor {

View File

@ -43,8 +43,10 @@ pub struct Args {
interval: u64,
/// Timeout, in milliseconds
#[arg(long, value_name = "MILLIS")]
timeout: Option<u64>,
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration)]
timeout: Option<std::time::Duration>,
}
impl Args {
@ -55,7 +57,8 @@ impl Args {
/// Provide the timeout duration in milliseconds.
pub(super) fn timeout(&self) -> time::Duration {
time::Duration::from_millis(self.timeout.unwrap_or(u64::MAX))
self.timeout
.unwrap_or_else(|| time::Duration::from_millis(u64::MAX))
}
}