Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions plugins/rig-basics/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct HelpersPlugin;
#[serde(default, deny_unknown_fields)]
struct HelperSettings {
/// Seconds, 600 by default.
#[serde(deserialize_with = "deadline_secs")]
timeout_secs: NonZeroU64,
/// Tasks at once, 4 by default.
at_once: NonZeroUsize,
Expand Down Expand Up @@ -410,17 +411,34 @@ fn start(
}

/// How long each helper of a call may take: the call's `timeout`, at least
/// 1 second and as long as it says, else `default`.
/// 1 second and at most [`MOST_SECS`], else `default`.
fn timeout(asked: Option<u64>, default: NonZeroU64) -> Result<Duration, String> {
match asked {
Some(0) => Err(format!(
"`timeout` must be at least 1 second; leave it out for {default} s (`[helpers] \
timeout_secs`)"
)),
asked => Ok(Duration::from_secs(asked.unwrap_or(default.get()))),
asked => deadline(asked.unwrap_or(default.get())),
}
}

/// The longest a helper may take, about 136 years, which any clock adds to now.
const MOST_SECS: u64 = u32::MAX as u64;
/// `secs` as a helper's time limit, refused past [`MOST_SECS`].
fn deadline(secs: u64) -> Result<Duration, String> {
if secs > MOST_SECS {
return Err(format!("a timeout of {secs} s is over {MOST_SECS} s"));
}
Ok(Duration::from_secs(secs))
}

/// A `timeout_secs` that [`deadline`] takes.
fn deadline_secs<'de, D: serde::Deserializer<'de>>(read: D) -> Result<NonZeroU64, D::Error> {
let secs = NonZeroU64::deserialize(read)?;
deadline(secs.get()).map_err(serde::de::Error::custom)?;
Ok(secs)
}

/// Runs a call's children, on a thread of its own.
struct Runner {
command: HelperCommand,
Expand Down Expand Up @@ -557,6 +575,9 @@ impl Runner {
grace: Some(STOP_WINDOW),
..Spawn::default()
};
let Some(deadline) = Instant::now().checked_add(self.timeout) else {
return (Err("no deadline".to_owned()), String::new());
};
// Unsettled from before it can spend until what it spent is known.
self.reports.unsettled.fetch_add(1, Ordering::SeqCst);
let mut process = match launcher.spawn(&self.command.program, args, how) {
Expand All @@ -580,8 +601,7 @@ impl Runner {
}
};
let mut read = Read::new(self.reports.clone());
let deadline = Instant::now().checked_add(self.timeout);
let ended = process.run(deadline, |piece| {
let ended = process.run(Some(deadline), |piece| {
read.piece(piece);
true
});
Expand Down
25 changes: 23 additions & 2 deletions plugins/rig-basics/src/helpers/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,8 @@ fn a_helper_in_tilde_is_in_the_home_directory() {
);
}

/// A call's `timeout` is each helper's deadline as given, however long; a
/// call without one gets `[helpers] timeout_secs`, 600 s by default.
/// A call's `timeout` is each helper's deadline as given; a call without
/// one gets `[helpers] timeout_secs`, 600 s by default.
#[test]
fn a_helpers_deadline_is_the_calls_timeout_or_the_setting() {
let default = HelperSettings::default().timeout_secs;
Expand All @@ -575,6 +575,27 @@ fn the_helpers_table_reads_its_keys_and_refuses_zero() {
assert!(read("[helpers]\ntimeout_secs = 0\n").is_err());
}

/// A timeout too long for a deadline is refused, naming the value: a
/// call's `timeout` when called, `[helpers] timeout_secs` when read.
#[test]
fn a_timeout_too_long_for_a_deadline_is_refused() {
let default = HelperSettings::default().timeout_secs;
let most = u64::from(u32::MAX);
assert_eq!(timeout(Some(most), default), Ok(Duration::from_secs(most)));
let past = most.saturating_add(1).to_string();
let asked = timeout(Some(most.saturating_add(1)), default);
assert!(
asked.as_ref().is_err_and(|why| why.contains(&past)),
"a call's timeout too long for a deadline: {asked:?}"
);
let read = Settings::from_toml(&format!("[helpers]\ntimeout_secs = {past}\n"))
.get::<HelperSettings>("helpers");
assert!(
read.as_ref().is_err_and(|why| why.contains(&past)),
"a timeout_secs too long for a deadline: {read:?}"
);
}

/// A helper past its deadline fails its task, naming where the deadline
/// came from.
#[test]
Expand Down
Loading