use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct Budget {
pub target: Duration,
pub warning: Duration,
pub panic: Duration,
}
impl Budget {
#[must_use]
pub const fn new(target_us: u64, warning_us: u64, panic_us: u64) -> Self {
Self {
target: Duration::from_micros(target_us),
warning: Duration::from_micros(warning_us),
panic: Duration::from_micros(panic_us),
}
}
#[must_use]
pub const fn from_ms(target_ms: u64, warning_ms: u64, panic_ms: u64) -> Self {
Self {
target: Duration::from_millis(target_ms),
warning: Duration::from_millis(warning_ms),
panic: Duration::from_millis(panic_ms),
}
}
#[must_use]
pub fn exceeds_warning(&self, duration: Duration) -> bool {
duration > self.warning
}
#[must_use]
pub fn exceeds_panic(&self, duration: Duration) -> bool {
duration > self.panic
}
#[must_use]
pub fn status(&self, duration: Duration) -> BudgetStatus {
if duration > self.panic {
BudgetStatus::Panic
} else if duration > self.warning {
BudgetStatus::Warning
} else if duration > self.target {
BudgetStatus::Elevated
} else {
BudgetStatus::Ok
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BudgetStatus {
Ok,
Elevated,
Warning,
Panic,
}
#[derive(Debug, Clone, Copy)]
pub struct Deadline {
start: Instant,
max_duration: Duration,
}
impl Deadline {
#[must_use]
pub fn new(max_duration: Duration) -> Self {
Self {
start: Instant::now(),
max_duration,
}
}
#[must_use]
pub fn fail_open_default() -> Self {
Self::new(ABSOLUTE_MAX)
}
#[must_use]
pub fn is_exceeded(&self) -> bool {
self.start.elapsed() > self.max_duration
}
#[must_use]
pub fn remaining(&self) -> Option<Duration> {
self.max_duration.checked_sub(self.start.elapsed())
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
#[must_use]
pub const fn max_duration(&self) -> Duration {
self.max_duration
}
#[must_use]
pub fn has_budget_for(&self, budget: &Budget) -> bool {
self.remaining().is_some_and(|r| r > budget.panic)
}
}
pub const QUICK_REJECT: Budget = Budget::new(
1, 5, 50, );
pub const FAST_PATH: Budget = Budget::new(
75, 150, 500, );
pub const PATTERN_MATCH: Budget = Budget::new(
100, 250, 1000, );
pub const HEREDOC_TRIGGER: Budget = Budget::new(
5, 10, 100, );
pub const HEREDOC_EXTRACT: Budget = Budget::new(
200, 500, 2000, );
pub const LANGUAGE_DETECT: Budget = Budget::new(
20, 50, 200, );
pub const FULL_HEREDOC_PIPELINE: Budget = Budget::from_ms(
5, 15, 20, );
pub const ABSOLUTE_MAX: Duration = Duration::from_millis(200);
pub const HOOK_EVALUATION_BUDGET_MS: u64 = 200;
pub const HOOK_EVALUATION_BUDGET: Duration = Duration::from_millis(HOOK_EVALUATION_BUDGET_MS);
#[must_use]
pub fn should_fail_open(duration: Duration) -> bool {
duration > ABSOLUTE_MAX
}
pub const FAST_PATH_BUDGET_US: u64 = 500;
pub const SLOW_PATH_BUDGET_MS: u64 = 200;
pub const FAIL_OPEN_THRESHOLD_MS: u64 = 200;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_status_classification() {
let budget = Budget::new(10, 50, 100);
assert_eq!(budget.status(Duration::from_micros(5)), BudgetStatus::Ok);
assert_eq!(budget.status(Duration::from_micros(10)), BudgetStatus::Ok);
assert_eq!(
budget.status(Duration::from_micros(11)),
BudgetStatus::Elevated
);
assert_eq!(
budget.status(Duration::from_micros(50)),
BudgetStatus::Elevated
);
assert_eq!(
budget.status(Duration::from_micros(51)),
BudgetStatus::Warning
);
assert_eq!(
budget.status(Duration::from_micros(100)),
BudgetStatus::Warning
);
assert_eq!(
budget.status(Duration::from_micros(101)),
BudgetStatus::Panic
);
}
#[test]
fn fail_open_threshold() {
assert!(!should_fail_open(Duration::from_millis(199)));
assert!(!should_fail_open(Duration::from_millis(200)));
assert!(should_fail_open(Duration::from_millis(201)));
}
#[test]
fn budget_hierarchy_makes_sense() {
assert!(QUICK_REJECT.panic < FAST_PATH.target);
assert!(FAST_PATH.panic <= PATTERN_MATCH.panic);
assert!(HEREDOC_TRIGGER.panic < HEREDOC_EXTRACT.target);
assert!(FULL_HEREDOC_PIPELINE.panic >= HEREDOC_EXTRACT.panic);
}
#[test]
fn deadline_creation() {
let deadline = Deadline::new(Duration::from_millis(100));
assert!(!deadline.is_exceeded());
assert!(deadline.remaining().is_some());
assert_eq!(deadline.max_duration(), Duration::from_millis(100));
}
#[test]
fn deadline_fail_open_default() {
let deadline = Deadline::fail_open_default();
assert_eq!(deadline.max_duration(), ABSOLUTE_MAX);
assert!(!deadline.is_exceeded());
}
#[test]
fn deadline_exceeded_with_zero_duration() {
let deadline = Deadline::new(Duration::ZERO);
assert!(deadline.is_exceeded());
assert!(deadline.remaining().is_none());
}
#[test]
fn deadline_has_budget_for() {
let deadline = Deadline::new(Duration::from_millis(100));
let small_budget = Budget::new(1000, 5000, 10_000); let large_budget = Budget::new(10_000, 50_000, 200_000);
assert!(deadline.has_budget_for(&small_budget));
assert!(!deadline.has_budget_for(&large_budget));
}
}