Skip to main content

solti_model/domain/policy/
admission.rs

1//! # Admission policy.
2//!
3//! [`AdmissionPolicy`] controls how duplicate task submissions are handled.
4
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8use crate::error::{ModelError, ModelResult};
9
10/// Defines how the controller admits a new task into a slot.
11///
12/// A slot may only run one task at a time.
13/// When a new task arrives, the controller applies the selected policy to determine what to do if the slot is already occupied.
14///
15/// | Variant         | Behaviour                                              |
16/// |-----------------|--------------------------------------------------------|
17/// | `DropIfRunning` | Ignore the new task, return success without scheduling |
18/// | `Replace`       | Cancel the running task, schedule the new one          |
19/// | `Queue`         | Enqueue the new task, run when slot becomes free       |
20#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22#[non_exhaustive]
23pub enum AdmissionPolicy {
24    /// If the slot already has a running task, ignore the new one.
25    #[default]
26    DropIfRunning,
27    /// Cancel the currently running task in the slot and replace it with the newly submitted task.
28    Replace,
29    /// Enqueue the new task to be executed after the current one completes.
30    Queue,
31}
32
33impl FromStr for AdmissionPolicy {
34    type Err = ModelError;
35    fn from_str(s: &str) -> ModelResult<Self> {
36        let s = s.trim();
37        if s.is_empty()
38            || s.eq_ignore_ascii_case("drop-if-running")
39            || s.eq_ignore_ascii_case("drop")
40        {
41            Ok(AdmissionPolicy::DropIfRunning)
42        } else if s.eq_ignore_ascii_case("queue")
43            || s.eq_ignore_ascii_case("add")
44            || s.eq_ignore_ascii_case("new")
45        {
46            Ok(AdmissionPolicy::Queue)
47        } else if s.eq_ignore_ascii_case("replace") {
48            Ok(AdmissionPolicy::Replace)
49        } else {
50            Err(ModelError::UnknownAdmission(s.to_string()))
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn parse_drop_if_running_variants() {
61        assert_eq!(
62            "drop-if-running".parse::<AdmissionPolicy>().unwrap(),
63            AdmissionPolicy::DropIfRunning
64        );
65        assert_eq!(
66            "drop".parse::<AdmissionPolicy>().unwrap(),
67            AdmissionPolicy::DropIfRunning
68        );
69        assert_eq!(
70            "DROP".parse::<AdmissionPolicy>().unwrap(),
71            AdmissionPolicy::DropIfRunning
72        );
73    }
74
75    #[test]
76    fn parse_queue_variants() {
77        assert_eq!(
78            "queue".parse::<AdmissionPolicy>().unwrap(),
79            AdmissionPolicy::Queue
80        );
81        assert_eq!(
82            "add".parse::<AdmissionPolicy>().unwrap(),
83            AdmissionPolicy::Queue
84        );
85        assert_eq!(
86            "new".parse::<AdmissionPolicy>().unwrap(),
87            AdmissionPolicy::Queue
88        );
89    }
90
91    #[test]
92    fn parse_replace() {
93        assert_eq!(
94            "replace".parse::<AdmissionPolicy>().unwrap(),
95            AdmissionPolicy::Replace
96        );
97        assert_eq!(
98            "REPLACE".parse::<AdmissionPolicy>().unwrap(),
99            AdmissionPolicy::Replace
100        );
101    }
102
103    #[test]
104    fn empty_string_maps_to_default() {
105        let parsed: AdmissionPolicy = "".parse().unwrap();
106        assert_eq!(parsed, AdmissionPolicy::default());
107    }
108
109    #[test]
110    fn whitespace_trimmed() {
111        assert_eq!(
112            "  queue  ".parse::<AdmissionPolicy>().unwrap(),
113            AdmissionPolicy::Queue
114        );
115    }
116
117    #[test]
118    fn unknown_value_fails() {
119        assert!("foobar".parse::<AdmissionPolicy>().is_err());
120    }
121}