1use std::time::Instant;
4
5use crate::config::WorkersConfig;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SlotState {
10 Spawning,
12 Idle,
14 Busy,
16 Stopping,
18 Dead,
20}
21
22pub struct SlotInfo {
24 pub state: SlotState,
25 pub pid: Option<u32>,
26 pub jobs_handled: u64,
27 pub created_at: Instant,
28}
29
30impl SlotInfo {
31 pub fn new() -> Self {
32 Self {
33 state: SlotState::Spawning,
34 pid: None,
35 jobs_handled: 0,
36 created_at: Instant::now(),
37 }
38 }
39
40 pub fn should_recycle(&self, cfg: &WorkersConfig) -> bool {
42 if self.jobs_handled >= cfg.max_jobs {
43 return true;
44 }
45 if self.created_at.elapsed() >= cfg.ttl {
46 return true;
47 }
48 false
49 }
50
51 pub fn mark_ready(&mut self, pid: u32) {
53 debug_assert!(matches!(self.state, SlotState::Spawning));
54 self.state = SlotState::Idle;
55 self.pid = Some(pid);
56 }
57
58 pub fn mark_busy(&mut self) {
60 debug_assert!(matches!(self.state, SlotState::Idle));
61 self.state = SlotState::Busy;
62 }
63
64 pub fn mark_idle(&mut self) {
66 debug_assert!(matches!(self.state, SlotState::Busy | SlotState::Stopping));
67 self.jobs_handled += 1;
68 if matches!(self.state, SlotState::Busy) {
69 self.state = SlotState::Idle;
70 }
71 }
73
74 pub fn mark_dead(&mut self) {
76 self.state = SlotState::Dead;
77 }
78
79 pub fn request_stop(&mut self) {
81 if matches!(self.state, SlotState::Idle | SlotState::Busy) {
82 self.state = SlotState::Stopping;
83 }
84 }
85}
86
87impl Default for SlotInfo {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::time::Duration;
97
98 fn cfg(max_jobs: u64, ttl_secs: u64) -> WorkersConfig {
99 WorkersConfig {
100 max_jobs,
101 ttl: Duration::from_secs(ttl_secs),
102 ..WorkersConfig::default()
103 }
104 }
105
106 #[test]
107 fn fresh_slot_starts_in_spawning_state() {
108 let s = SlotInfo::new();
109 assert_eq!(s.state, SlotState::Spawning);
110 assert_eq!(s.jobs_handled, 0);
111 assert!(s.pid.is_none());
112 }
113
114 #[test]
115 fn ready_transitions_to_idle_with_pid() {
116 let mut s = SlotInfo::new();
117 s.mark_ready(12345);
118 assert_eq!(s.state, SlotState::Idle);
119 assert_eq!(s.pid, Some(12345));
120 }
121
122 #[test]
123 fn busy_idle_cycles_increment_job_counter() {
124 let mut s = SlotInfo::new();
125 s.mark_ready(1);
126 s.mark_busy();
127 s.mark_idle();
128 assert_eq!(s.jobs_handled, 1);
129 assert_eq!(s.state, SlotState::Idle);
130 }
131
132 #[test]
133 fn should_recycle_after_max_jobs() {
134 let mut s = SlotInfo::new();
135 s.mark_ready(1);
136 for _ in 0..3 {
137 s.mark_busy();
138 s.mark_idle();
139 }
140 assert!(s.should_recycle(&cfg(3, 999_999)));
141 }
142
143 #[test]
144 fn should_not_recycle_below_thresholds() {
145 let s = SlotInfo::new();
146 assert!(!s.should_recycle(&cfg(1000, 3600)));
147 }
148}