#![forbid(unsafe_code)]
use std::collections::VecDeque;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub struct DeadlineConfig {
pub deadline: Duration,
pub loss_stale: f64,
pub loss_delay: f64,
pub loss_false_invalid: f64,
pub loss_recompute: f64,
pub window_size: usize,
pub min_samples: usize,
}
impl Default for DeadlineConfig {
fn default() -> Self {
Self {
deadline: Duration::from_millis(500),
loss_stale: 0.8,
loss_delay: 0.5,
loss_false_invalid: 0.6,
loss_recompute: 0.3,
window_size: 100,
min_samples: 5,
}
}
}
impl DeadlineConfig {
#[must_use]
pub fn new(deadline: Duration) -> Self {
Self {
deadline,
..Default::default()
}
}
#[must_use]
pub fn with_losses(
mut self,
stale: f64,
delay: f64,
false_invalid: f64,
recompute: f64,
) -> Self {
self.loss_stale = stale;
self.loss_delay = delay;
self.loss_false_invalid = false_invalid;
self.loss_recompute = recompute;
self
}
#[must_use]
pub fn with_window_size(mut self, size: usize) -> Self {
self.window_size = size.max(1);
self
}
#[must_use]
pub fn with_min_samples(mut self, min: usize) -> Self {
self.min_samples = min.max(1);
self
}
}
#[derive(Debug, Clone)]
pub struct SurvivalStats {
samples: VecDeque<f64>,
window_size: usize,
lambda: f64,
k: f64,
ema: f64,
ema_sq: f64,
alpha: f64,
}
impl SurvivalStats {
#[must_use]
pub fn new(window_size: usize) -> Self {
Self {
samples: VecDeque::with_capacity(window_size),
window_size: window_size.max(1),
lambda: 0.1, k: 1.5, ema: 0.1,
ema_sq: 0.01,
alpha: 0.1,
}
}
pub fn record(&mut self, duration: Duration) {
let t = duration.as_secs_f64();
if self.samples.len() >= self.window_size {
self.samples.pop_front();
}
self.samples.push_back(t);
self.ema = self.alpha * t + (1.0 - self.alpha) * self.ema;
self.ema_sq = self.alpha * t * t + (1.0 - self.alpha) * self.ema_sq;
if self.samples.len() >= 3 {
self.estimate_weibull();
}
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn mean(&self) -> f64 {
self.ema
}
#[must_use]
pub fn variance(&self) -> f64 {
(self.ema_sq - self.ema * self.ema).max(0.0)
}
#[must_use]
pub fn std_dev(&self) -> f64 {
self.variance().sqrt()
}
#[must_use]
pub fn lambda(&self) -> f64 {
self.lambda
}
#[must_use]
pub fn k(&self) -> f64 {
self.k
}
#[must_use]
pub fn survival(&self, t: f64) -> f64 {
if t <= 0.0 {
return 1.0;
}
(-(t / self.lambda).powf(self.k)).exp()
}
#[must_use]
pub fn expected_remaining(&self, elapsed: f64) -> f64 {
let s_t = self.survival(elapsed);
if s_t <= 1e-9 {
return 0.0;
}
let max_t = elapsed + 10.0 * self.lambda; let steps = 100;
let dt = (max_t - elapsed) / steps as f64;
let mut integral = 0.0;
for i in 0..steps {
let u = elapsed + (i as f64 + 0.5) * dt;
integral += self.survival(u) * dt;
}
integral / s_t
}
fn estimate_weibull(&mut self) {
if self.samples.is_empty() {
return;
}
let n = self.samples.len() as f64;
let mean: f64 = self.samples.iter().sum::<f64>() / n;
let variance: f64 = self
.samples
.iter()
.map(|&x| (x - mean).powi(2))
.sum::<f64>()
/ n;
if mean <= 0.0 || variance <= 0.0 {
return;
}
let cv = variance.sqrt() / mean;
let k = (1.2 / cv).clamp(0.5, 10.0);
let gamma_approx = 1.0 - 0.5772 / k + 0.98905 / (k * k);
let lambda = (mean / gamma_approx).max(0.001);
self.k = k;
self.lambda = lambda;
}
}
impl Default for SurvivalStats {
fn default() -> Self {
Self::new(100)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeadlineDecision {
Wait,
Cancel,
}
#[derive(Debug, Clone)]
pub struct DecisionRationale {
pub decision: DeadlineDecision,
pub loss_wait: f64,
pub loss_cancel: f64,
pub prob_exceed_deadline: f64,
pub expected_remaining_secs: f64,
pub model_reliable: bool,
pub explanation: String,
}
impl DecisionRationale {
fn new(decision: DeadlineDecision) -> Self {
Self {
decision,
loss_wait: 0.0,
loss_cancel: 0.0,
prob_exceed_deadline: 0.0,
expected_remaining_secs: 0.0,
model_reliable: false,
explanation: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct DeadlineController {
config: DeadlineConfig,
stats: SurvivalStats,
sequence: u64,
latest_input_sequence: u64,
}
impl DeadlineController {
#[must_use]
pub fn new(config: DeadlineConfig) -> Self {
let stats = SurvivalStats::new(config.window_size);
Self {
config,
stats,
sequence: 0,
latest_input_sequence: 0,
}
}
#[must_use]
pub fn with_deadline(deadline: Duration) -> Self {
Self::new(DeadlineConfig::new(deadline))
}
pub fn record_completion(&mut self, duration: Duration) {
self.stats.record(duration);
}
pub fn new_input(&mut self) -> u64 {
self.sequence += 1;
self.latest_input_sequence = self.sequence;
self.sequence
}
#[must_use]
pub fn start_validation(&mut self) -> u64 {
self.sequence += 1;
self.sequence
}
#[must_use]
pub fn is_stale(&self, validation_sequence: u64) -> bool {
validation_sequence < self.latest_input_sequence
}
#[must_use]
pub fn decide(&self, elapsed: Duration) -> DeadlineDecision {
self.decide_with_rationale(elapsed).decision
}
#[must_use]
pub fn decide_with_rationale(&self, elapsed: Duration) -> DecisionRationale {
let elapsed_secs = elapsed.as_secs_f64();
let deadline_secs = self.config.deadline.as_secs_f64();
let model_reliable = self.stats.sample_count() >= self.config.min_samples;
if elapsed >= self.config.deadline {
let mut rationale = DecisionRationale::new(DeadlineDecision::Cancel);
rationale.prob_exceed_deadline = 1.0;
rationale.model_reliable = model_reliable;
rationale.explanation = format!(
"Elapsed time ({:.0}ms) exceeds deadline ({:.0}ms)",
elapsed.as_millis(),
self.config.deadline.as_millis()
);
return rationale;
}
if !model_reliable {
let mut rationale = DecisionRationale::new(DeadlineDecision::Wait);
rationale.model_reliable = false;
rationale.explanation = format!(
"Model not reliable (only {} samples, need {})",
self.stats.sample_count(),
self.config.min_samples
);
return rationale;
}
let s_t = self.stats.survival(elapsed_secs);
let s_deadline = self.stats.survival(deadline_secs);
let prob_exceed = s_deadline / s_t.max(1e-9); let expected_remaining = self.stats.expected_remaining(elapsed_secs);
let loss_wait =
self.config.loss_stale * prob_exceed + self.config.loss_delay * expected_remaining;
let loss_cancel = self.config.loss_false_invalid + self.config.loss_recompute;
let decision = if loss_wait < loss_cancel {
DeadlineDecision::Wait
} else {
DeadlineDecision::Cancel
};
let mut rationale = DecisionRationale::new(decision.clone());
rationale.loss_wait = loss_wait;
rationale.loss_cancel = loss_cancel;
rationale.prob_exceed_deadline = prob_exceed;
rationale.expected_remaining_secs = expected_remaining;
rationale.model_reliable = true;
rationale.explanation = format!(
"Loss(wait)={:.3} vs Loss(cancel)={:.3}. P(exceed deadline|running)={:.1}%, E[remaining]={:.0}ms",
loss_wait,
loss_cancel,
prob_exceed * 100.0,
expected_remaining * 1000.0
);
rationale
}
#[must_use]
pub fn stats(&self) -> &SurvivalStats {
&self.stats
}
#[must_use]
pub fn config(&self) -> &DeadlineConfig {
&self.config
}
pub fn reset_stats(&mut self) {
self.stats = SurvivalStats::new(self.config.window_size);
}
}
impl Default for DeadlineController {
fn default() -> Self {
Self::new(DeadlineConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stats_new() {
let stats = SurvivalStats::new(50);
assert_eq!(stats.sample_count(), 0);
assert_eq!(stats.window_size, 50);
}
#[test]
fn stats_record() {
let mut stats = SurvivalStats::new(10);
stats.record(Duration::from_millis(100));
assert_eq!(stats.sample_count(), 1);
stats.record(Duration::from_millis(150));
assert_eq!(stats.sample_count(), 2);
}
#[test]
fn stats_rolling_window() {
let mut stats = SurvivalStats::new(3);
stats.record(Duration::from_millis(100));
stats.record(Duration::from_millis(200));
stats.record(Duration::from_millis(300));
assert_eq!(stats.sample_count(), 3);
stats.record(Duration::from_millis(400));
assert_eq!(stats.sample_count(), 3); }
#[test]
fn stats_survival_function() {
let mut stats = SurvivalStats::new(10);
for _ in 0..10 {
stats.record(Duration::from_millis(100));
}
assert!((stats.survival(0.0) - 1.0).abs() < 0.01);
let s_1 = stats.survival(0.1);
let s_2 = stats.survival(0.2);
assert!(s_1 > s_2, "S(0.1)={} should be > S(0.2)={}", s_1, s_2);
}
#[test]
fn stats_mean_and_variance() {
let mut stats = SurvivalStats::new(10);
stats.record(Duration::from_millis(100));
stats.record(Duration::from_millis(100));
stats.record(Duration::from_millis(100));
let mean = stats.mean();
assert!(mean > 0.05 && mean < 0.15);
let variance = stats.variance();
assert!(variance >= 0.0);
assert!(variance.is_finite());
let std_dev = stats.std_dev();
assert!(std_dev >= 0.0);
assert!(std_dev.is_finite());
assert!(stats.lambda() > 0.0);
assert!(stats.lambda().is_finite());
assert!(stats.k() > 0.0);
assert!(stats.k().is_finite());
}
#[test]
fn stats_default_uses_window_size_100() {
let stats = SurvivalStats::default();
assert_eq!(stats.sample_count(), 0);
assert_eq!(stats.window_size, 100);
}
#[test]
fn stats_estimate_weibull_empty_is_noop() {
let mut stats = SurvivalStats::new(10);
let lambda_before = stats.lambda;
let k_before = stats.k;
stats.estimate_weibull();
assert_eq!(stats.sample_count(), 0);
assert!((stats.lambda - lambda_before).abs() < 1e-12);
assert!((stats.k - k_before).abs() < 1e-12);
}
#[test]
fn config_default() {
let config = DeadlineConfig::default();
assert_eq!(config.deadline, Duration::from_millis(500));
assert!(config.loss_stale > 0.0);
assert!(config.window_size > 0);
}
#[test]
fn config_builder() {
let config = DeadlineConfig::new(Duration::from_secs(1))
.with_losses(0.9, 0.4, 0.5, 0.2)
.with_window_size(50);
assert_eq!(config.deadline, Duration::from_secs(1));
assert_eq!(config.loss_stale, 0.9);
assert_eq!(config.window_size, 50);
}
#[test]
fn controller_new() {
let controller = DeadlineController::with_deadline(Duration::from_millis(100));
assert_eq!(controller.config().deadline, Duration::from_millis(100));
assert_eq!(controller.stats().sample_count(), 0);
}
#[test]
fn controller_record_completion() {
let mut controller = DeadlineController::default();
controller.record_completion(Duration::from_millis(50));
assert_eq!(controller.stats().sample_count(), 1);
}
#[test]
fn unit_cancel_when_long_tail() {
let config = DeadlineConfig::new(Duration::from_millis(100))
.with_losses(0.8, 0.5, 0.3, 0.2) .with_min_samples(3);
let mut controller = DeadlineController::new(config);
controller.record_completion(Duration::from_millis(50));
controller.record_completion(Duration::from_millis(60));
controller.record_completion(Duration::from_millis(500)); controller.record_completion(Duration::from_millis(80));
controller.record_completion(Duration::from_millis(1000));
let rationale = controller.decide_with_rationale(Duration::from_millis(80));
assert!(
rationale.model_reliable,
"Model should be reliable with 5 samples"
);
}
#[test]
fn unit_wait_when_fast() {
let config = DeadlineConfig::new(Duration::from_millis(500))
.with_losses(0.8, 0.5, 0.7, 0.4) .with_min_samples(3);
let mut controller = DeadlineController::new(config);
for _ in 0..10 {
controller.record_completion(Duration::from_millis(50));
}
let decision = controller.decide(Duration::from_millis(100));
assert_eq!(
decision,
DeadlineDecision::Wait,
"Should wait for fast validator"
);
}
#[test]
fn unit_deadline_respected() {
let config = DeadlineConfig::new(Duration::from_millis(100));
let controller = DeadlineController::new(config);
let decision = controller.decide(Duration::from_millis(100));
assert_eq!(
decision,
DeadlineDecision::Cancel,
"Should cancel at deadline"
);
let decision = controller.decide(Duration::from_millis(150));
assert_eq!(
decision,
DeadlineDecision::Cancel,
"Should cancel past deadline"
);
}
#[test]
fn controller_staleness_tracking() {
let mut controller = DeadlineController::default();
let seq1 = controller.start_validation();
assert!(!controller.is_stale(seq1));
controller.new_input();
assert!(controller.is_stale(seq1), "Old validation should be stale");
let seq2 = controller.start_validation();
assert!(
!controller.is_stale(seq2),
"New validation should not be stale"
);
}
#[test]
fn controller_unreliable_model_waits() {
let config = DeadlineConfig::new(Duration::from_millis(100)).with_min_samples(10);
let mut controller = DeadlineController::new(config);
controller.record_completion(Duration::from_millis(50));
controller.record_completion(Duration::from_millis(60));
let rationale = controller.decide_with_rationale(Duration::from_millis(80));
assert!(!rationale.model_reliable);
assert_eq!(rationale.decision, DeadlineDecision::Wait);
}
#[test]
fn controller_reset_stats() {
let mut controller = DeadlineController::default();
controller.record_completion(Duration::from_millis(100));
assert_eq!(controller.stats().sample_count(), 1);
controller.reset_stats();
assert_eq!(controller.stats().sample_count(), 0);
}
#[test]
fn rationale_explanation_populated() {
let mut controller = DeadlineController::default();
for _ in 0..10 {
controller.record_completion(Duration::from_millis(100));
}
let rationale = controller.decide_with_rationale(Duration::from_millis(50));
assert!(!rationale.explanation.is_empty());
assert!(rationale.model_reliable);
}
#[test]
fn property_random_durations() {
let config = DeadlineConfig::new(Duration::from_millis(200))
.with_losses(0.7, 0.4, 0.3, 0.3)
.with_min_samples(5);
let mut controller = DeadlineController::new(config.clone());
let training_durations = [30, 50, 80, 100, 150, 120, 90, 70, 60, 110];
for &ms in &training_durations {
controller.record_completion(Duration::from_millis(ms));
}
for elapsed_ms in [10, 50, 100, 150, 180, 199] {
let rationale = controller.decide_with_rationale(Duration::from_millis(elapsed_ms));
if rationale.loss_wait < rationale.loss_cancel {
assert_eq!(rationale.decision, DeadlineDecision::Wait);
} else {
assert_eq!(rationale.decision, DeadlineDecision::Cancel);
}
assert!(rationale.expected_remaining_secs >= 0.0);
assert!(rationale.prob_exceed_deadline >= 0.0);
assert!(rationale.prob_exceed_deadline <= 1.0);
}
}
#[test]
fn stats_window_size_zero_clamped_to_one() {
let stats = SurvivalStats::new(0);
assert_eq!(stats.window_size, 1);
}
#[test]
fn stats_survival_negative_t_returns_one() {
let stats = SurvivalStats::new(10);
assert!((stats.survival(-5.0) - 1.0).abs() < 1e-12);
}
#[test]
fn stats_expected_remaining_near_zero_survival() {
let mut stats = SurvivalStats::new(10);
for _ in 0..5 {
stats.record(Duration::from_millis(10));
}
let remaining = stats.expected_remaining(1_000_000.0);
assert!(
remaining.abs() < 1e-6,
"Expected remaining should be ~0 for extreme elapsed, got {}",
remaining
);
}
#[test]
fn stats_expected_remaining_positive_for_moderate_elapsed() {
let mut stats = SurvivalStats::new(10);
for _ in 0..10 {
stats.record(Duration::from_millis(100));
}
let remaining = stats.expected_remaining(0.05); assert!(
remaining > 0.0,
"Expected remaining should be positive, got {}",
remaining
);
assert!(remaining.is_finite());
}
#[test]
fn stats_estimate_weibull_identical_samples_no_panic() {
let mut stats = SurvivalStats::new(10);
for _ in 0..5 {
stats.record(Duration::from_millis(100));
}
assert!(stats.k().is_finite());
assert!(stats.lambda().is_finite());
}
#[test]
fn stats_ema_converges_toward_input() {
let mut stats = SurvivalStats::new(100);
for _ in 0..100 {
stats.record(Duration::from_millis(200));
}
assert!(
(stats.mean() - 0.2).abs() < 0.01,
"EMA should converge near 0.2, got {}",
stats.mean()
);
}
#[test]
fn config_with_window_size_zero_clamped_to_one() {
let config = DeadlineConfig::default().with_window_size(0);
assert_eq!(config.window_size, 1);
}
#[test]
fn config_with_min_samples_zero_clamped_to_one() {
let config = DeadlineConfig::default().with_min_samples(0);
assert_eq!(config.min_samples, 1);
}
#[test]
fn config_partial_eq() {
let a = DeadlineConfig::new(Duration::from_millis(500));
let b = DeadlineConfig::new(Duration::from_millis(500));
assert_eq!(a, b);
let c = DeadlineConfig::new(Duration::from_millis(200));
assert_ne!(a, c);
}
#[test]
fn controller_default() {
let controller = DeadlineController::default();
assert_eq!(controller.config().deadline, Duration::from_millis(500));
assert_eq!(controller.stats().sample_count(), 0);
}
#[test]
fn controller_decide_matches_rationale() {
let mut controller = DeadlineController::with_deadline(Duration::from_millis(500));
for _ in 0..10 {
controller.record_completion(Duration::from_millis(100));
}
let elapsed = Duration::from_millis(50);
let decision = controller.decide(elapsed);
let rationale = controller.decide_with_rationale(elapsed);
assert_eq!(decision, rationale.decision);
}
#[test]
fn controller_new_input_returns_sequence() {
let mut controller = DeadlineController::default();
let s1 = controller.new_input();
let s2 = controller.new_input();
assert!(s2 > s1, "Sequence should increase");
}
#[test]
fn controller_start_validation_increments_sequence() {
let mut controller = DeadlineController::default();
let s1 = controller.start_validation();
let s2 = controller.start_validation();
assert_eq!(s2, s1 + 1);
}
#[test]
fn decision_debug_clone_eq() {
let wait = DeadlineDecision::Wait;
let cancel = DeadlineDecision::Cancel;
assert_ne!(wait, cancel);
assert_eq!(wait.clone(), DeadlineDecision::Wait);
assert!(!format!("{:?}", wait).is_empty());
assert!(!format!("{:?}", cancel).is_empty());
}
#[test]
fn rationale_debug_clone() {
let mut controller = DeadlineController::default();
for _ in 0..10 {
controller.record_completion(Duration::from_millis(50));
}
let rationale = controller.decide_with_rationale(Duration::from_millis(30));
let cloned = rationale.clone();
assert_eq!(cloned.decision, rationale.decision);
assert!(!format!("{:?}", rationale).is_empty());
}
#[test]
fn rationale_past_deadline_explanation_contains_exceeds() {
let controller = DeadlineController::with_deadline(Duration::from_millis(100));
let rationale = controller.decide_with_rationale(Duration::from_millis(200));
assert_eq!(rationale.decision, DeadlineDecision::Cancel);
assert!(
rationale.explanation.contains("exceeds deadline"),
"Explanation should mention exceeds deadline: {}",
rationale.explanation
);
assert!((rationale.prob_exceed_deadline - 1.0).abs() < 1e-12);
}
#[test]
fn controller_debug_clone() {
let controller = DeadlineController::default();
let cloned = controller.clone();
assert_eq!(cloned.config().deadline, controller.config().deadline);
assert!(!format!("{:?}", controller).is_empty());
}
#[test]
fn controller_staleness_multiple_inputs() {
let mut controller = DeadlineController::default();
let v1 = controller.start_validation();
controller.new_input();
let v2 = controller.start_validation();
controller.new_input();
let v3 = controller.start_validation();
assert!(controller.is_stale(v1));
assert!(controller.is_stale(v2));
assert!(!controller.is_stale(v3));
}
}