behest_runtime/
session_gate.rs1use std::collections::HashMap;
9use std::sync::Arc;
10
11use tokio::sync::{Mutex, RwLock};
12use uuid::Uuid;
13
14pub struct SessionGuard {
19 _inner: tokio::sync::OwnedMutexGuard<()>,
20}
21
22impl SessionGuard {
23 fn new(inner: tokio::sync::OwnedMutexGuard<()>) -> Self {
25 Self { _inner: inner }
26 }
27}
28
29impl std::fmt::Debug for SessionGuard {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("SessionGuard").finish_non_exhaustive()
32 }
33}
34
35#[derive(Debug, Clone, Default)]
42pub struct SessionGate {
43 locks: Arc<RwLock<HashMap<Uuid, Arc<Mutex<()>>>>>,
44}
45
46impl SessionGate {
47 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub async fn acquire(&self, session_id: Uuid) -> Result<SessionGuard, SessionBusy> {
64 let mutex = {
65 let read = self.locks.read().await;
66 read.get(&session_id).cloned()
67 };
68
69 let mutex = if let Some(m) = mutex {
70 m
71 } else {
72 let mut write = self.locks.write().await;
73 write
74 .entry(session_id)
75 .or_insert_with(|| Arc::new(Mutex::new(())))
76 .clone()
77 };
78
79 let guard = mutex
82 .try_lock_owned()
83 .map_err(|_| SessionBusy { session_id })?;
84
85 Ok(SessionGuard::new(guard))
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct SessionBusy {
92 pub session_id: Uuid,
94}
95
96impl std::fmt::Display for SessionBusy {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(
99 f,
100 "session {} is busy — another run is active",
101 self.session_id
102 )
103 }
104}
105
106impl std::error::Error for SessionBusy {}
107
108#[cfg(test)]
109#[allow(clippy::unwrap_used, clippy::expect_used)]
110mod tests {
111 use super::*;
112
113 #[tokio::test]
114 async fn acquire_and_release() {
115 let gate = SessionGate::new();
116 let sid = Uuid::new_v4();
117
118 let guard = gate.acquire(sid).await.expect("should acquire");
119 drop(guard);
121
122 let _guard2 = gate
124 .acquire(sid)
125 .await
126 .expect("should re-acquire after drop");
127 }
128
129 #[tokio::test]
130 async fn concurrent_acquire_fails() {
131 let gate = SessionGate::new();
132 let sid = Uuid::new_v4();
133
134 let guard = gate.acquire(sid).await.expect("first acquire");
135 let result = gate.acquire(sid).await;
136 assert!(result.is_err(), "second acquire should fail");
137 assert_eq!(result.unwrap_err().session_id, sid);
138
139 drop(guard);
140
141 let _guard2 = gate
143 .acquire(sid)
144 .await
145 .expect("should succeed after release");
146 }
147
148 #[tokio::test]
149 async fn independent_sessions_no_contention() {
150 let gate = SessionGate::new();
151 let sid1 = Uuid::new_v4();
152 let sid2 = Uuid::new_v4();
153
154 let guard1 = gate.acquire(sid1).await.expect("acquire sid1");
155 let guard2 = gate.acquire(sid2).await.expect("acquire sid2");
156
157 drop(guard1);
159 drop(guard2);
160 }
161
162 #[tokio::test]
163 async fn sequential_runs_serialize() {
164 let gate = SessionGate::new();
165 let sid = Uuid::new_v4();
166
167 let g1 = gate.acquire(sid).await.expect("first");
168 drop(g1);
170
171 let g2 = gate.acquire(sid).await.expect("second");
172 drop(g2);
173 }
174}