1use std::fs::{File, OpenOptions};
8use std::path::PathBuf;
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
13pub struct MachineSlots {
14 dir: PathBuf,
15 max: usize,
16}
17
18#[derive(Debug)]
20pub struct Slot(#[allow(dead_code)] File);
21
22impl MachineSlots {
23 pub fn new(dir: impl Into<PathBuf>, max: usize) -> Self {
25 Self {
26 dir: dir.into(),
27 max: max.max(1),
28 }
29 }
30
31 fn try_acquire(&self) -> std::io::Result<Option<Slot>> {
32 std::fs::create_dir_all(&self.dir)?;
33 for i in 0..self.max {
34 let file = OpenOptions::new()
35 .create(true)
36 .truncate(false)
37 .write(true)
38 .open(self.dir.join(format!("slot-{i}.lock")))?;
39 match file.try_lock() {
40 Ok(()) => return Ok(Some(Slot(file))),
41 Err(std::fs::TryLockError::WouldBlock) => {}
42 Err(std::fs::TryLockError::Error(e)) => return Err(e),
43 }
44 }
45 Ok(None)
46 }
47
48 pub async fn acquire(&self) -> std::io::Result<Slot> {
50 loop {
51 let this = self.clone();
52 let got = tokio::task::spawn_blocking(move || this.try_acquire())
53 .await
54 .map_err(std::io::Error::other)??;
55 if let Some(slot) = got {
56 return Ok(slot);
57 }
58 tokio::time::sleep(Duration::from_millis(250)).await;
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[tokio::test]
68 async fn limits_holders_across_handles() {
69 let dir = tempfile::tempdir().unwrap();
70 let a = MachineSlots::new(dir.path(), 2);
71 let b = MachineSlots::new(dir.path(), 2); let s1 = a.acquire().await.unwrap();
73 let _s2 = b.acquire().await.unwrap();
74 assert!(a.try_acquire().unwrap().is_none(), "both slots taken");
75 drop(s1);
76 assert!(
77 b.try_acquire().unwrap().is_some(),
78 "a freed slot is reusable"
79 );
80 }
81}