Skip to main content

folk_core/
worker_slot.rs

1//! Per-slot state for one worker.
2
3use std::time::Instant;
4
5use crate::config::WorkersConfig;
6
7/// Current state of a worker slot.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SlotState {
10    /// Spawn requested; waiting for `control.ready`.
11    Spawning,
12    /// Ready to accept work.
13    Idle,
14    /// Currently handling a request.
15    Busy,
16    /// Recycle requested; finishing in-flight work, then will be replaced.
17    Stopping,
18    /// Terminated. Slot will be replaced.
19    Dead,
20}
21
22/// Mutable state for a single worker slot. Owned exclusively by its supervisor task.
23pub 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    /// Returns `true` if this slot should be recycled per the configured policies.
41    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    /// Transition to `Idle` after a successful boot.
52    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    /// Transition to `Busy` when dispatching a request.
59    pub fn mark_busy(&mut self) {
60        debug_assert!(matches!(self.state, SlotState::Idle));
61        self.state = SlotState::Busy;
62    }
63
64    /// Transition to `Idle` after a successful response.
65    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        // If Stopping, stay there until shutdown completes.
72    }
73
74    /// Mark the slot as terminated (worker died, runtime returned EOF, etc.).
75    pub fn mark_dead(&mut self) {
76        self.state = SlotState::Dead;
77    }
78
79    /// Request graceful drain.
80    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}