use std::sync::mpsc::{RecvTimeoutError, Sender, channel};
use std::time::{Duration, Instant};
use anyhow::anyhow;
pub(crate) const ENV: &str = "MAHBOT_DEBUG_READ_LIMIT_SECS";
const DEFAULT_SECS: u64 = 60;
const MAX_SECS: u64 = 86_400;
pub(crate) const WATCHDOG_GRACE_SECS: u64 = 1;
pub(crate) const WATCHDOG_EXIT_CODE: i32 = 1;
fn limit() -> Duration {
crate::util::env_duration_secs(ENV, DEFAULT_SECS).min(Duration::from_secs(MAX_SECS))
}
#[must_use]
fn limit_message(target: &str, limit: Duration, over_channel: bool) -> String {
let state = if over_channel {
"the instance holds the store and its file was left untouched; raise the limit for a \
slower read, or re-run once the instance is idle"
} else {
"raise the limit for a slower read"
};
format!(
"'{target}' was not read within the {}s read limit ({ENV}) — {state}",
limit.as_secs()
)
}
pub(crate) struct Budget {
limit: Duration,
deadline: Instant,
cutoff: Instant,
}
impl Budget {
#[must_use]
pub(crate) fn start() -> Self {
let limit = limit();
let deadline = Instant::now() + limit;
Self {
limit,
deadline,
cutoff: deadline + Duration::from_secs(WATCHDOG_GRACE_SECS),
}
}
fn remaining(&self) -> Duration {
self.deadline.saturating_duration_since(Instant::now())
}
}
#[derive(Clone, Copy)]
pub(crate) struct Read<'a> {
target: &'a str,
budget: &'a Budget,
over_channel: bool,
}
impl<'a> Read<'a> {
#[must_use]
pub(crate) fn direct(target: &'a str, budget: &'a Budget) -> Self {
Self {
target,
budget,
over_channel: false,
}
}
#[must_use]
pub(crate) fn over_channel(target: &'a str, budget: &'a Budget) -> Self {
Self {
target,
budget,
over_channel: true,
}
}
pub(crate) fn remaining(&self) -> Duration {
self.budget.remaining()
}
#[must_use]
pub(crate) fn timeout(&self) -> anyhow::Error {
anyhow!(self.message())
}
fn message(&self) -> String {
limit_message(self.target, self.budget.limit, self.over_channel)
}
pub(crate) fn check(&self) -> anyhow::Result<()> {
if self.remaining().is_zero() {
return Err(self.timeout());
}
Ok(())
}
pub(crate) fn arm(&self) -> anyhow::Result<Watchdog> {
self.check()?;
Ok(Watchdog::arm(self.budget.cutoff, self.message()))
}
}
#[must_use]
pub(crate) struct Watchdog {
_armed: Option<Sender<()>>,
}
#[cfg(test)]
static PROCESS_EXIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(test)]
pub(crate) fn enable_process_exit() {
PROCESS_EXIT.store(true, std::sync::atomic::Ordering::Relaxed);
}
impl Watchdog {
fn arm(cutoff: Instant, message: String) -> Self {
#[cfg(test)]
if !PROCESS_EXIT.load(std::sync::atomic::Ordering::Relaxed) {
return Self { _armed: None };
}
let (tx, rx) = channel::<()>();
std::thread::spawn(move || {
if rx.recv_timeout(cutoff.saturating_duration_since(Instant::now()))
== Err(RecvTimeoutError::Timeout)
{
crate::util::print_stderr(&format!("Error: {message}"));
std::process::exit(WATCHDOG_EXIT_CODE);
}
});
Self { _armed: Some(tx) }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn limit_with(value: Option<&str>) -> Duration {
let _env = crate::util::test::set_env_var(ENV, value);
limit()
}
#[test]
#[serial_test::serial(read_limit)]
fn limit_reads_the_knob_within_its_bounds() {
assert_eq!(
limit_with(None),
Duration::from_secs(DEFAULT_SECS),
"an unset knob means the default"
);
assert_eq!(
limit_with(Some("3")),
Duration::from_secs(3),
"an override is taken as given"
);
assert_eq!(
limit_with(Some("99999999")),
Duration::from_secs(MAX_SECS),
"an absurd value is clamped"
);
assert_eq!(
limit_with(Some("0")),
Duration::ZERO,
"zero is an immediate failure, never 'no limit'"
);
}
#[test]
fn limit_message_names_the_target_the_limit_and_the_placement() {
let direct = limit_message("logs", Duration::from_secs(0), false);
assert!(direct.contains("'logs'"), "got: {direct}");
assert!(direct.contains("0s read limit"), "got: {direct}");
assert!(direct.contains(ENV), "got: {direct}");
assert!(
!direct.contains("left untouched"),
"a store this process read itself was not left alone: {direct}"
);
let channel = limit_message("logs", Duration::from_secs(0), true);
assert!(
channel.contains("left untouched"),
"the channel variant must say the live store was not opened: {channel}"
);
assert!(channel.contains(ENV), "got: {channel}");
assert_ne!(direct, channel, "the two placements must differ");
}
}