use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct GracePeriodState {
duration: Duration,
started_at: Option<Instant>,
trace_snapshot_at_terminal: usize,
}
impl GracePeriodState {
#[must_use]
pub const fn new(duration: Duration) -> Self {
Self {
duration,
started_at: None,
trace_snapshot_at_terminal: 0,
}
}
#[must_use]
pub const fn duration(&self) -> Duration {
self.duration
}
pub fn start(&mut self, trace_len: usize) {
self.started_at = Some(Instant::now());
self.trace_snapshot_at_terminal = trace_len;
}
#[must_use]
pub const fn is_started(&self) -> bool {
self.started_at.is_some()
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.started_at
.is_some_and(|start| start.elapsed() >= self.duration)
}
#[must_use]
pub fn remaining(&self) -> Duration {
self.started_at.map_or(Duration::ZERO, |start| {
self.duration.saturating_sub(start.elapsed())
})
}
#[must_use]
pub const fn trace_snapshot_at_terminal(&self) -> usize {
self.trace_snapshot_at_terminal
}
#[must_use]
pub fn elapsed(&self) -> Option<Duration> {
self.started_at.map(|start| start.elapsed())
}
}
#[must_use]
pub fn resolve_grace_period(
cli_override: Option<Duration>,
document_value: Option<&str>,
) -> Duration {
if let Some(cli) = cli_override {
return cli;
}
if let Some(doc_str) = document_value {
if let Ok(d) = humantime::parse_duration(doc_str) {
return d;
}
tracing::warn!(
duration = doc_str,
"could not parse document grace_period, using 0s"
);
}
Duration::ZERO
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn new_state_not_started() {
let state = GracePeriodState::new(Duration::from_secs(30));
assert!(!state.is_started());
assert!(!state.is_expired());
assert_eq!(state.remaining(), Duration::ZERO);
assert_eq!(state.trace_snapshot_at_terminal(), 0);
assert!(state.elapsed().is_none());
}
#[test]
fn start_records_snapshot_and_time() {
let mut state = GracePeriodState::new(Duration::from_secs(30));
state.start(42);
assert!(state.is_started());
assert!(!state.is_expired());
assert_eq!(state.trace_snapshot_at_terminal(), 42);
assert!(state.elapsed().is_some());
}
#[test]
fn zero_duration_expires_immediately() {
let mut state = GracePeriodState::new(Duration::ZERO);
state.start(0);
assert!(state.is_expired());
assert_eq!(state.remaining(), Duration::ZERO);
}
#[test]
fn expires_after_duration() {
let mut state = GracePeriodState::new(Duration::from_millis(10));
state.start(5);
assert!(!state.is_expired());
thread::sleep(Duration::from_millis(15));
assert!(state.is_expired());
}
#[test]
fn remaining_decreases() {
let mut state = GracePeriodState::new(Duration::from_secs(10));
state.start(0);
let r1 = state.remaining();
thread::sleep(Duration::from_millis(5));
let r2 = state.remaining();
assert!(r2 <= r1);
}
#[test]
fn duration_accessor() {
let state = GracePeriodState::new(Duration::from_secs(42));
assert_eq!(state.duration(), Duration::from_secs(42));
}
#[test]
fn cli_override_takes_precedence() {
let result = resolve_grace_period(Some(Duration::from_secs(60)), Some("30s"));
assert_eq!(result, Duration::from_secs(60));
}
#[test]
fn document_value_used_when_no_cli() {
let result = resolve_grace_period(None, Some("30s"));
assert_eq!(result, Duration::from_secs(30));
}
#[test]
fn default_zero_when_nothing_specified() {
let result = resolve_grace_period(None, None);
assert_eq!(result, Duration::ZERO);
}
#[test]
fn unparseable_document_value_falls_back_to_zero() {
let result = resolve_grace_period(None, Some("not-a-duration"));
assert_eq!(result, Duration::ZERO);
}
#[test]
fn humantime_formats_accepted() {
assert_eq!(
resolve_grace_period(None, Some("2m")),
Duration::from_secs(120)
);
assert_eq!(
resolve_grace_period(None, Some("1h")),
Duration::from_secs(3600)
);
assert_eq!(
resolve_grace_period(None, Some("500ms")),
Duration::from_millis(500)
);
}
}