use compact_str::CompactString;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReservationStatus {
Pending,
Active,
Completed,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GpuSpec {
Count(u32),
Indices(Vec<u32>),
}
impl GpuSpec {
pub fn count(&self) -> u32 {
match self {
GpuSpec::Count(n) => *n,
GpuSpec::Indices(indices) => indices.len() as u32,
}
}
pub fn indices(&self) -> Option<&[u32]> {
match self {
GpuSpec::Indices(indices) => Some(indices),
GpuSpec::Count(_) => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuReservation {
pub id: u32,
pub user: CompactString,
pub gpu_spec: GpuSpec,
pub start_time: SystemTime,
pub duration: Duration,
pub status: ReservationStatus,
pub created_at: SystemTime,
pub cancelled_at: Option<SystemTime>,
}
impl GpuReservation {
pub fn is_active(&self, now: SystemTime) -> bool {
if self.status == ReservationStatus::Cancelled {
return false;
}
now >= self.start_time && now < self.end_time()
}
pub fn end_time(&self) -> SystemTime {
self.start_time + self.duration
}
pub fn overlaps_with(&self, start: SystemTime, end: SystemTime) -> bool {
self.start_time < end && start < self.end_time()
}
pub fn update_status(&mut self, now: SystemTime) {
match self.status {
ReservationStatus::Pending => {
if now >= self.start_time && now < self.end_time() {
self.status = ReservationStatus::Active;
} else if now >= self.end_time() {
self.status = ReservationStatus::Completed;
}
}
ReservationStatus::Active => {
if now >= self.end_time() {
self.status = ReservationStatus::Completed;
}
}
ReservationStatus::Completed | ReservationStatus::Cancelled => {
}
}
}
pub fn next_transition_time(&self, now: SystemTime) -> Option<SystemTime> {
match self.status {
ReservationStatus::Pending => {
if self.start_time > now {
Some(self.start_time)
} else {
None
}
}
ReservationStatus::Active => {
let end = self.end_time();
if end > now {
Some(end)
} else {
None
}
}
ReservationStatus::Completed | ReservationStatus::Cancelled => {
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_active() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let duration = Duration::from_secs(3600);
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let before = start - Duration::from_secs(100);
assert!(!reservation.is_active(before));
assert!(reservation.is_active(start));
let during = start + Duration::from_secs(1800); assert!(reservation.is_active(during));
let end = start + duration;
assert!(!reservation.is_active(end));
let after = end + Duration::from_secs(100);
assert!(!reservation.is_active(after));
reservation.status = ReservationStatus::Cancelled;
assert!(!reservation.is_active(during));
}
#[test]
fn test_end_time() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let duration = Duration::from_secs(3600);
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
assert_eq!(reservation.end_time(), start + duration);
}
#[test]
fn test_overlaps_with() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let duration = Duration::from_secs(3600);
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end = start + duration;
let before_start = start - Duration::from_secs(200);
let before_end = start - Duration::from_secs(100);
assert!(!reservation.overlaps_with(before_start, before_end));
let after_start = end + Duration::from_secs(100);
let after_end = end + Duration::from_secs(200);
assert!(!reservation.overlaps_with(after_start, after_end));
let overlap_start = start - Duration::from_secs(100);
let overlap_end = start + Duration::from_secs(100);
assert!(reservation.overlaps_with(overlap_start, overlap_end));
let overlap_start = end - Duration::from_secs(100);
let overlap_end = end + Duration::from_secs(100);
assert!(reservation.overlaps_with(overlap_start, overlap_end));
let contains_start = start - Duration::from_secs(100);
let contains_end = end + Duration::from_secs(100);
assert!(reservation.overlaps_with(contains_start, contains_end));
let contained_start = start + Duration::from_secs(100);
let contained_end = end - Duration::from_secs(100);
assert!(reservation.overlaps_with(contained_start, contained_end));
assert!(reservation.overlaps_with(start, end));
}
#[test]
fn test_update_status() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let duration = Duration::from_secs(3600);
let end = start + duration;
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let before = start - Duration::from_secs(100);
reservation.update_status(before);
assert_eq!(reservation.status, ReservationStatus::Pending);
reservation.update_status(start);
assert_eq!(reservation.status, ReservationStatus::Active);
let during = start + Duration::from_secs(1800);
reservation.update_status(during);
assert_eq!(reservation.status, ReservationStatus::Active);
reservation.update_status(end);
assert_eq!(reservation.status, ReservationStatus::Completed);
let after = end + Duration::from_secs(100);
reservation.update_status(after);
assert_eq!(reservation.status, ReservationStatus::Completed);
reservation.status = ReservationStatus::Cancelled;
reservation.update_status(during);
assert_eq!(reservation.status, ReservationStatus::Cancelled);
}
#[test]
fn test_pending_to_completed_directly() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let duration = Duration::from_secs(3600);
let end = start + duration;
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let after = end + Duration::from_secs(100);
reservation.update_status(after);
assert_eq!(reservation.status, ReservationStatus::Completed);
}
#[test]
fn test_next_transition_time_pending() {
let now = SystemTime::now();
let start_time = now + Duration::from_secs(3600);
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time,
duration: Duration::from_secs(7200),
status: ReservationStatus::Pending,
created_at: now,
cancelled_at: None,
};
assert_eq!(reservation.next_transition_time(now), Some(start_time));
let future = start_time + Duration::from_secs(100);
assert_eq!(reservation.next_transition_time(future), None);
}
#[test]
fn test_next_transition_time_active() {
let now = SystemTime::now();
let start_time = now - Duration::from_secs(1800); let duration = Duration::from_secs(3600); let end_time = start_time + duration;
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time,
duration,
status: ReservationStatus::Active,
created_at: now - Duration::from_secs(2000),
cancelled_at: None,
};
assert_eq!(reservation.next_transition_time(now), Some(end_time));
let future = end_time + Duration::from_secs(100);
assert_eq!(reservation.next_transition_time(future), None);
}
#[test]
fn test_next_transition_time_terminal_states() {
let now = SystemTime::now();
let start_time = now - Duration::from_secs(7200);
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time,
duration: Duration::from_secs(3600),
status: ReservationStatus::Completed,
created_at: now - Duration::from_secs(8000),
cancelled_at: None,
};
assert_eq!(reservation.next_transition_time(now), None);
reservation.status = ReservationStatus::Cancelled;
reservation.cancelled_at = Some(now);
assert_eq!(reservation.next_transition_time(now), None);
}
#[test]
fn test_gpu_spec_count() {
let spec = GpuSpec::Count(4);
assert_eq!(spec.count(), 4);
assert_eq!(spec.indices(), None);
}
#[test]
fn test_gpu_spec_indices() {
let spec = GpuSpec::Indices(vec![0, 2, 3]);
assert_eq!(spec.count(), 3);
assert_eq!(spec.indices(), Some(&[0, 2, 3][..]));
}
#[test]
fn test_gpu_spec_empty_indices() {
let spec = GpuSpec::Indices(vec![]);
assert_eq!(spec.count(), 0);
assert_eq!(spec.indices(), Some(&[][..]));
}
mod proptests {
use super::*;
use proptest::prelude::*;
fn time_range_strategy() -> impl Strategy<Value = (SystemTime, Duration)> {
(1000u64..1_000_000_000, 1u64..86400).prop_map(|(start_secs, duration_secs)| {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(start_secs);
let duration = Duration::from_secs(duration_secs);
(start, duration)
})
}
proptest! {
#[test]
fn prop_overlap_is_symmetric(
(start1, dur1) in time_range_strategy(),
(start2, dur2) in time_range_strategy(),
) {
let res1 = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start1,
duration: dur1,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end2 = start2 + dur2;
let overlap_1_with_2 = res1.overlaps_with(start2, end2);
let res2 = GpuReservation {
id: 2,
user: "bob".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start2,
duration: dur2,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end1 = start1 + dur1;
let overlap_2_with_1 = res2.overlaps_with(start1, end1);
prop_assert_eq!(overlap_1_with_2, overlap_2_with_1);
}
#[test]
fn prop_no_overlap_after_end(
(start, dur) in time_range_strategy(),
gap in 1u64..1000,
) {
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration: dur,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end = start + dur;
let after_start = end + Duration::from_secs(gap);
let after_end = after_start + Duration::from_secs(100);
prop_assert!(!reservation.overlaps_with(after_start, after_end));
}
#[test]
fn prop_overlap_when_contained(
(start, dur) in time_range_strategy(),
before in 1u64..1000,
after in 1u64..1000,
) {
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration: dur,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end = start + dur;
let container_start = start - Duration::from_secs(before);
let container_end = end + Duration::from_secs(after);
prop_assert!(reservation.overlaps_with(container_start, container_end));
}
#[test]
fn prop_status_monotonic(
(start, dur) in time_range_strategy(),
) {
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration: dur,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
let end = start + dur;
let before = start - Duration::from_secs(100);
reservation.update_status(before);
prop_assert_eq!(reservation.status, ReservationStatus::Pending);
reservation.update_status(start);
prop_assert_eq!(reservation.status, ReservationStatus::Active);
let after = end + Duration::from_secs(100);
reservation.update_status(after);
prop_assert_eq!(reservation.status, ReservationStatus::Completed);
reservation.update_status(after + Duration::from_secs(1000));
prop_assert_eq!(reservation.status, ReservationStatus::Completed);
}
#[test]
fn prop_end_time_calculation(
(start, dur) in time_range_strategy(),
) {
let reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration: dur,
status: ReservationStatus::Pending,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: None,
};
prop_assert_eq!(reservation.end_time(), start + dur);
}
#[test]
fn prop_gpu_spec_count(count in 1u32..100) {
let spec = GpuSpec::Count(count);
prop_assert_eq!(spec.count(), count);
prop_assert_eq!(spec.indices(), None);
}
#[test]
fn prop_gpu_spec_indices(indices in prop::collection::vec(0u32..16, 0..10)) {
let spec = GpuSpec::Indices(indices.clone());
prop_assert_eq!(spec.count(), indices.len() as u32);
prop_assert_eq!(spec.indices(), Some(indices.as_slice()));
}
#[test]
fn prop_cancelled_never_active(
(start, dur) in time_range_strategy(),
check_time in 1000u64..1_000_000_000,
) {
let mut reservation = GpuReservation {
id: 1,
user: "alice".into(),
gpu_spec: GpuSpec::Count(2),
start_time: start,
duration: dur,
status: ReservationStatus::Cancelled,
created_at: SystemTime::UNIX_EPOCH,
cancelled_at: Some(SystemTime::UNIX_EPOCH),
};
let check = SystemTime::UNIX_EPOCH + Duration::from_secs(check_time);
prop_assert!(!reservation.is_active(check));
reservation.update_status(check);
prop_assert_eq!(reservation.status, ReservationStatus::Cancelled);
}
}
}
}