use crate::daemon::models::ControlSource;
use crate::daemon::state::StateManager;
use parking_lot::Mutex;
use std::sync::Arc;
pub const MAX_IPC_SLOTS: usize = 64;
pub struct SlotPool {
inner: Arc<Inner>,
}
struct Inner {
in_use: Mutex<[bool; MAX_IPC_SLOTS]>,
state: StateManager,
}
impl SlotPool {
pub fn new(state: StateManager) -> Self {
Self {
inner: Arc::new(Inner {
in_use: Mutex::new([false; MAX_IPC_SLOTS]),
state,
}),
}
}
pub fn acquire(&self) -> Option<SlotGuard> {
let mut slots = self.inner.in_use.lock();
let idx = slots.iter().position(|busy| !busy)?;
slots[idx] = true;
let slot = u32::try_from(idx).ok()?;
Some(SlotGuard {
inner: self.inner.clone(),
slot,
})
}
#[cfg(test)]
pub fn in_use_count(&self) -> usize {
self.inner.in_use.lock().iter().filter(|b| **b).count()
}
}
impl Clone for SlotPool {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
pub struct SlotGuard {
inner: Arc<Inner>,
slot: u32,
}
impl SlotGuard {
pub fn id(&self) -> u32 {
self.slot
}
}
impl Drop for SlotGuard {
fn drop(&mut self) {
self.inner
.state
.clear_source(&ControlSource::UnixSocket(self.slot));
let mut slots = self.inner.in_use.lock();
slots[self.slot as usize] = false;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::daemon::models::{CommandMode, GimbalCommand};
use std::time::Duration;
fn fresh_pool() -> SlotPool {
SlotPool::new(StateManager::new())
}
#[test]
fn acquire_assigns_distinct_slots_until_full() {
let pool = fresh_pool();
let mut guards: Vec<SlotGuard> = Vec::with_capacity(MAX_IPC_SLOTS);
for _ in 0..MAX_IPC_SLOTS {
guards.push(pool.acquire().expect("slot must be available"));
}
assert_eq!(pool.in_use_count(), MAX_IPC_SLOTS);
let mut ids: Vec<u32> = guards.iter().map(|g| g.id()).collect();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), MAX_IPC_SLOTS);
}
#[test]
fn acquire_returns_none_when_pool_full() {
let pool = fresh_pool();
let _holders: Vec<SlotGuard> = (0..MAX_IPC_SLOTS)
.map(|_| pool.acquire().expect("fits"))
.collect();
assert!(pool.acquire().is_none());
}
#[test]
fn drop_releases_slot_for_reuse() {
let pool = fresh_pool();
let g1 = pool.acquire().expect("first");
let id1 = g1.id();
drop(g1);
assert_eq!(pool.in_use_count(), 0);
let g2 = pool.acquire().expect("after drop");
assert_eq!(g2.id(), id1);
}
#[test]
fn drop_clears_state_entries_for_recycled_slot() {
let state = StateManager::with_min_command_interval(Duration::from_secs(60));
let pool = SlotPool::new(state.clone());
let g_a = pool.acquire().expect("first");
let slot_id = g_a.id();
let cmd_a = GimbalCommand::new(
ControlSource::UnixSocket(slot_id),
CommandMode::Position,
Some(0.0),
Some(0.0),
Some(0.0),
);
assert!(state.set_command(cmd_a));
drop(g_a);
let g_b = pool.acquire().expect("recycle");
assert_eq!(g_b.id(), slot_id);
let cmd_b = GimbalCommand::new(
ControlSource::UnixSocket(slot_id),
CommandMode::Position,
Some(1.0),
Some(1.0),
Some(1.0),
);
assert!(
state.set_command(cmd_b),
"fresh client on recycled slot was rate-limited by prior tenant"
);
}
}