Skip to main content

oxihuman_core/
semaphore_pool.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A pool of named counting semaphores for concurrency-budget tracking.
6
7use std::collections::HashMap;
8
9/// A single counting semaphore.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct Semaphore {
13    pub name: String,
14    pub capacity: u32,
15    pub acquired: u32,
16}
17
18#[allow(dead_code)]
19impl Semaphore {
20    pub fn new(name: &str, capacity: u32) -> Self {
21        Self {
22            name: name.to_string(),
23            capacity,
24            acquired: 0,
25        }
26    }
27
28    pub fn available(&self) -> u32 {
29        self.capacity.saturating_sub(self.acquired)
30    }
31
32    pub fn try_acquire(&mut self, count: u32) -> bool {
33        if self.available() >= count {
34            self.acquired += count;
35            true
36        } else {
37            false
38        }
39    }
40
41    pub fn release(&mut self, count: u32) {
42        self.acquired = self.acquired.saturating_sub(count);
43    }
44
45    pub fn is_full(&self) -> bool {
46        self.acquired >= self.capacity
47    }
48}
49
50/// Pool of named semaphores.
51#[allow(dead_code)]
52pub struct SemaphorePool {
53    semaphores: HashMap<String, Semaphore>,
54    total_acquired: u64,
55    total_released: u64,
56}
57
58#[allow(dead_code)]
59impl SemaphorePool {
60    pub fn new() -> Self {
61        Self {
62            semaphores: HashMap::new(),
63            total_acquired: 0,
64            total_released: 0,
65        }
66    }
67
68    pub fn add(&mut self, name: &str, capacity: u32) {
69        self.semaphores
70            .insert(name.to_string(), Semaphore::new(name, capacity));
71    }
72
73    pub fn remove(&mut self, name: &str) -> bool {
74        self.semaphores.remove(name).is_some()
75    }
76
77    pub fn acquire(&mut self, name: &str, count: u32) -> bool {
78        if let Some(s) = self.semaphores.get_mut(name) {
79            if s.try_acquire(count) {
80                self.total_acquired += u64::from(count);
81                return true;
82            }
83        }
84        false
85    }
86
87    pub fn release(&mut self, name: &str, count: u32) {
88        if let Some(s) = self.semaphores.get_mut(name) {
89            s.release(count);
90            self.total_released += u64::from(count);
91        }
92    }
93
94    pub fn available(&self, name: &str) -> u32 {
95        self.semaphores
96            .get(name)
97            .map(|s| s.available())
98            .unwrap_or(0)
99    }
100
101    pub fn capacity(&self, name: &str) -> u32 {
102        self.semaphores.get(name).map(|s| s.capacity).unwrap_or(0)
103    }
104
105    pub fn count(&self) -> usize {
106        self.semaphores.len()
107    }
108
109    pub fn total_acquired(&self) -> u64 {
110        self.total_acquired
111    }
112
113    pub fn total_released(&self) -> u64 {
114        self.total_released
115    }
116
117    pub fn names(&self) -> Vec<&str> {
118        let mut v: Vec<&str> = self.semaphores.keys().map(|s| s.as_str()).collect();
119        v.sort_unstable();
120        v
121    }
122
123    pub fn is_full(&self, name: &str) -> bool {
124        self.semaphores.get(name).is_some_and(|s| s.is_full())
125    }
126
127    pub fn clear(&mut self) {
128        self.semaphores.clear();
129    }
130}
131
132impl Default for SemaphorePool {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138pub fn new_semaphore_pool() -> SemaphorePool {
139    SemaphorePool::new()
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn acquire_and_release() {
148        let mut p = new_semaphore_pool();
149        p.add("net", 4);
150        assert!(p.acquire("net", 2));
151        assert_eq!(p.available("net"), 2);
152        p.release("net", 2);
153        assert_eq!(p.available("net"), 4);
154    }
155
156    #[test]
157    fn over_capacity_fails() {
158        let mut p = new_semaphore_pool();
159        p.add("db", 2);
160        assert!(p.acquire("db", 2));
161        assert!(!p.acquire("db", 1));
162    }
163
164    #[test]
165    fn unknown_semaphore() {
166        let mut p = new_semaphore_pool();
167        assert!(!p.acquire("missing", 1));
168        assert_eq!(p.available("missing"), 0);
169    }
170
171    #[test]
172    fn is_full() {
173        let mut p = new_semaphore_pool();
174        p.add("s", 1);
175        p.acquire("s", 1);
176        assert!(p.is_full("s"));
177    }
178
179    #[test]
180    fn total_acquired_tracked() {
181        let mut p = new_semaphore_pool();
182        p.add("s", 10);
183        p.acquire("s", 3);
184        p.acquire("s", 2);
185        assert_eq!(p.total_acquired(), 5);
186    }
187
188    #[test]
189    fn remove_semaphore() {
190        let mut p = new_semaphore_pool();
191        p.add("x", 5);
192        assert!(p.remove("x"));
193        assert_eq!(p.count(), 0);
194    }
195
196    #[test]
197    fn names_sorted() {
198        let mut p = new_semaphore_pool();
199        p.add("b", 1);
200        p.add("a", 1);
201        assert_eq!(p.names(), vec!["a", "b"]);
202    }
203
204    #[test]
205    fn capacity_query() {
206        let mut p = new_semaphore_pool();
207        p.add("q", 8);
208        assert_eq!(p.capacity("q"), 8);
209    }
210
211    #[test]
212    fn clear_empties_pool() {
213        let mut p = new_semaphore_pool();
214        p.add("a", 1);
215        p.add("b", 2);
216        p.clear();
217        assert_eq!(p.count(), 0);
218    }
219
220    #[test]
221    fn release_no_underflow() {
222        let mut p = new_semaphore_pool();
223        p.add("s", 5);
224        p.release("s", 100); // saturating
225        assert_eq!(p.available("s"), 5);
226    }
227}