use chrono::{DateTime, Duration as ChronoDuration, Utc};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub struct PriorityBoostConfig {
pub enabled: bool,
pub deadline: Duration,
pub boost_intervals: Vec<(Duration, u8)>,
}
impl PriorityBoostConfig {
pub fn new(deadline: Duration) -> Self {
Self {
enabled: true,
deadline,
boost_intervals: Vec::new(),
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
deadline: Duration::ZERO,
boost_intervals: Vec::new(),
}
}
pub fn with_boost(mut self, time_before_deadline: Duration, boost: u8) -> Self {
self.boost_intervals.push((time_before_deadline, boost));
self.boost_intervals
.sort_by_key(|interval| std::cmp::Reverse(interval.0));
self
}
pub fn standard(deadline: Duration) -> Self {
Self::new(deadline)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.75), 1)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.50), 2)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.25), 3)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.10), 4)
}
pub fn aggressive(deadline: Duration) -> Self {
Self::new(deadline)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.80), 2)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.60), 4)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.40), 6)
.with_boost(Duration::from_secs_f64(deadline.as_secs_f64() * 0.20), 8)
}
}
impl Default for PriorityBoostConfig {
fn default() -> Self {
Self::disabled()
}
}
pub struct PriorityBooster {
config: PriorityBoostConfig,
}
impl PriorityBooster {
pub fn new(config: PriorityBoostConfig) -> Self {
Self { config }
}
pub fn calculate_priority(&self, base_priority: u8, created_at: DateTime<Utc>) -> u8 {
if !self.config.enabled {
return base_priority;
}
let now = Utc::now();
let deadline = created_at
+ ChronoDuration::from_std(self.config.deadline).unwrap_or(ChronoDuration::zero());
let time_remaining = deadline.signed_duration_since(now);
if time_remaining <= ChronoDuration::zero() {
return 0; }
let time_remaining_std =
Duration::from_millis(time_remaining.num_milliseconds().max(0) as u64);
let mut boost: u8 = 0;
for (threshold, boost_amount) in &self.config.boost_intervals {
if time_remaining_std <= *threshold {
boost = *boost_amount;
}
}
base_priority.saturating_sub(boost)
}
pub fn is_past_deadline(&self, created_at: DateTime<Utc>) -> bool {
if !self.config.enabled {
return false;
}
let now = Utc::now();
let deadline = created_at
+ ChronoDuration::from_std(self.config.deadline).unwrap_or(ChronoDuration::zero());
now >= deadline
}
pub fn time_remaining(&self, created_at: DateTime<Utc>) -> Option<Duration> {
if !self.config.enabled {
return None;
}
let now = Utc::now();
let deadline = created_at
+ ChronoDuration::from_std(self.config.deadline).unwrap_or(ChronoDuration::zero());
let remaining = deadline.signed_duration_since(now);
if remaining <= ChronoDuration::zero() {
Some(Duration::ZERO)
} else {
Some(Duration::from_millis(remaining.num_milliseconds() as u64))
}
}
pub fn config(&self) -> &PriorityBoostConfig {
&self.config
}
}
impl Default for PriorityBooster {
fn default() -> Self {
Self::new(PriorityBoostConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_priority_boost_config_new() {
let config = PriorityBoostConfig::new(Duration::from_secs(60));
assert!(config.enabled);
assert_eq!(config.deadline, Duration::from_secs(60));
assert!(config.boost_intervals.is_empty());
}
#[test]
fn test_priority_boost_config_disabled() {
let config = PriorityBoostConfig::disabled();
assert!(!config.enabled);
}
#[test]
fn test_priority_boost_config_with_boost() {
let config = PriorityBoostConfig::new(Duration::from_secs(60))
.with_boost(Duration::from_secs(30), 1)
.with_boost(Duration::from_secs(15), 2);
assert_eq!(config.boost_intervals.len(), 2);
assert_eq!(config.boost_intervals[0].0, Duration::from_secs(30));
assert_eq!(config.boost_intervals[1].0, Duration::from_secs(15));
}
#[test]
fn test_priority_boost_config_standard() {
let config = PriorityBoostConfig::standard(Duration::from_secs(100));
assert!(config.enabled);
assert_eq!(config.deadline, Duration::from_secs(100));
assert_eq!(config.boost_intervals.len(), 4);
}
#[test]
fn test_priority_boost_config_aggressive() {
let config = PriorityBoostConfig::aggressive(Duration::from_secs(100));
assert!(config.enabled);
assert_eq!(config.boost_intervals.len(), 4);
assert!(config.boost_intervals.iter().any(|(_, b)| *b >= 6));
}
#[test]
fn test_priority_booster_disabled() {
let booster = PriorityBooster::new(PriorityBoostConfig::disabled());
let created_at = Utc::now() - ChronoDuration::hours(1);
assert_eq!(booster.calculate_priority(5, created_at), 5);
assert!(!booster.is_past_deadline(created_at));
assert!(booster.time_remaining(created_at).is_none());
}
#[test]
fn test_priority_booster_no_boost_before_threshold() {
let config = PriorityBoostConfig::new(Duration::from_secs(60))
.with_boost(Duration::from_secs(30), 2);
let booster = PriorityBooster::new(config);
let created_at = Utc::now();
assert_eq!(booster.calculate_priority(5, created_at), 5);
}
#[test]
fn test_priority_booster_boost_after_threshold() {
let config = PriorityBoostConfig::new(Duration::from_secs(60))
.with_boost(Duration::from_secs(30), 2);
let booster = PriorityBooster::new(config);
let created_at = Utc::now() - ChronoDuration::seconds(40);
assert_eq!(booster.calculate_priority(5, created_at), 3);
}
#[test]
fn test_priority_booster_past_deadline() {
let config = PriorityBoostConfig::new(Duration::from_secs(60))
.with_boost(Duration::from_secs(30), 2);
let booster = PriorityBooster::new(config);
let created_at = Utc::now() - ChronoDuration::seconds(120);
assert_eq!(booster.calculate_priority(5, created_at), 0);
assert!(booster.is_past_deadline(created_at));
}
#[test]
fn test_priority_booster_multiple_thresholds() {
let config = PriorityBoostConfig::new(Duration::from_secs(100))
.with_boost(Duration::from_secs(75), 1)
.with_boost(Duration::from_secs(50), 2)
.with_boost(Duration::from_secs(25), 3);
let booster = PriorityBooster::new(config);
let created_at = Utc::now() - ChronoDuration::seconds(20);
assert_eq!(booster.calculate_priority(10, created_at), 10);
let created_at = Utc::now() - ChronoDuration::seconds(40);
assert_eq!(booster.calculate_priority(10, created_at), 9);
let created_at = Utc::now() - ChronoDuration::seconds(60);
assert_eq!(booster.calculate_priority(10, created_at), 8);
let created_at = Utc::now() - ChronoDuration::seconds(80);
assert_eq!(booster.calculate_priority(10, created_at), 7);
}
#[test]
fn test_priority_booster_saturating_sub() {
let config = PriorityBoostConfig::new(Duration::from_secs(60))
.with_boost(Duration::from_secs(30), 10); let booster = PriorityBooster::new(config);
let created_at = Utc::now() - ChronoDuration::seconds(40);
assert_eq!(booster.calculate_priority(5, created_at), 0);
}
#[test]
fn test_priority_booster_time_remaining() {
let config = PriorityBoostConfig::new(Duration::from_secs(60));
let booster = PriorityBooster::new(config);
let created_at = Utc::now();
let remaining = booster.time_remaining(created_at).unwrap();
assert!(remaining.as_secs() >= 59 && remaining.as_secs() <= 60);
let created_at = Utc::now() - ChronoDuration::seconds(30);
let remaining = booster.time_remaining(created_at).unwrap();
assert!(remaining.as_secs() >= 29 && remaining.as_secs() <= 31);
let created_at = Utc::now() - ChronoDuration::seconds(120);
let remaining = booster.time_remaining(created_at).unwrap();
assert_eq!(remaining, Duration::ZERO);
}
}