mj_controller/
recovery_gate.rs1use mj_core::state::RecoveryObservation;
3use std::collections::{BTreeMap, BTreeSet};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex};
6use tokio::sync::{mpsc, watch};
7pub(crate) fn worker_target_mutex(session_id: &str) -> Arc<Mutex<()>> {
11 static LOCKS: std::sync::OnceLock<Mutex<BTreeMap<String, std::sync::Weak<Mutex<()>>>>> =
12 std::sync::OnceLock::new();
13 let mut locks = LOCKS
14 .get_or_init(Mutex::default)
15 .lock()
16 .unwrap_or_else(std::sync::PoisonError::into_inner);
17 locks.retain(|_, lock| lock.strong_count() > 0);
18 let slot = locks.entry(session_id.to_owned()).or_default();
19 if let Some(lock) = slot.upgrade() {
20 return lock;
21 }
22 let lock = Arc::new(Mutex::new(()));
23 *slot = Arc::downgrade(&lock);
24 lock
25}
26
27#[derive(Clone)]
42pub struct RecoveryObserver {
43 pub observations: mpsc::UnboundedSender<RecoveryObservation>,
44 pub gate: Arc<RecoveryGate>,
45}
46
47pub struct RecoveryReservation {
50 session_id: String,
51 gate: Arc<RecoveryGate>,
52}
53
54impl Drop for RecoveryReservation {
55 fn drop(&mut self) {
56 self.gate.release(&self.session_id);
57 }
58}
59
60pub struct RecoveryGate {
66 state: Mutex<RecoveryGateState>,
67 busy: watch::Sender<BTreeSet<String>>,
70}
71
72impl Default for RecoveryGate {
73 fn default() -> Self {
74 Self {
75 state: Mutex::default(),
76 busy: watch::channel(BTreeSet::new()).0,
77 }
78 }
79}
80
81#[derive(Default)]
82struct RecoveryGateState {
83 busy: BTreeMap<String, Arc<AtomicBool>>,
86 reservations: BTreeMap<String, usize>,
87}
88
89impl RecoveryGate {
90 pub fn reserve(self: &Arc<Self>, session_id: &str) -> RecoveryReservation {
91 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
92 *state.reservations.entry(session_id.to_owned()).or_default() += 1;
93 RecoveryReservation {
94 session_id: session_id.to_owned(),
95 gate: self.clone(),
96 }
97 }
98
99 fn release(&self, session_id: &str) {
100 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
101 let Some(count) = state.reservations.get_mut(session_id) else {
102 return;
103 };
104 *count -= 1;
105 if *count == 0 {
106 state.reservations.remove(session_id);
107 }
108 }
109
110 pub fn try_start(&self, session_id: &str) -> Option<Arc<AtomicBool>> {
114 let cancelled = {
115 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
116 if state.busy.contains_key(session_id) || state.reservations.contains_key(session_id) {
117 return None;
118 }
119 let cancelled = Arc::new(AtomicBool::new(false));
120 state.busy.insert(session_id.to_owned(), cancelled.clone());
121 cancelled
122 };
123 self.publish_busy();
124 Some(cancelled)
125 }
126
127 pub fn finish(&self, session_id: &str) {
128 self.state
129 .lock()
130 .unwrap_or_else(|error| error.into_inner())
131 .busy
132 .remove(session_id);
133 self.publish_busy();
134 }
135
136 fn publish_busy(&self) {
137 let busy = self.busy_sessions();
138 self.busy.send_replace(busy);
139 }
140
141 pub fn subscribe(&self) -> watch::Receiver<BTreeSet<String>> {
142 self.busy.subscribe()
143 }
144
145 pub fn is_busy(&self, session_id: &str) -> bool {
146 self.state
147 .lock()
148 .unwrap_or_else(|error| error.into_inner())
149 .busy
150 .contains_key(session_id)
151 }
152
153 pub fn cancel_busy(&self, session_id: &str) {
155 if let Some(cancelled) = self
156 .state
157 .lock()
158 .unwrap_or_else(|error| error.into_inner())
159 .busy
160 .get(session_id)
161 {
162 cancelled.store(true, Ordering::Release);
163 }
164 }
165
166 pub fn cancel_all(&self) {
168 for cancelled in self
169 .state
170 .lock()
171 .unwrap_or_else(|error| error.into_inner())
172 .busy
173 .values()
174 {
175 cancelled.store(true, Ordering::Release);
176 }
177 }
178
179 pub fn busy_sessions(&self) -> BTreeSet<String> {
180 self.state
181 .lock()
182 .unwrap_or_else(|error| error.into_inner())
183 .busy
184 .keys()
185 .cloned()
186 .collect()
187 }
188}
189
190impl RecoveryObserver {
191 pub fn observe(&self, observation: RecoveryObservation) {
194 let session_id = observation.session.id.clone();
195 if let Err(error) = self.observations.send(observation) {
196 tracing::debug!(
197 %session_id,
198 %error,
199 "recovery observation dropped because the coordinator stopped"
200 );
201 }
202 }
203
204 pub fn is_busy(&self, session_id: &str) -> bool {
205 self.gate.is_busy(session_id)
206 }
207
208 pub fn reserve(&self, session_id: &str) -> RecoveryReservation {
214 self.gate.reserve(session_id)
215 }
216
217 pub fn cancel_busy(&self, session_id: &str) {
221 self.gate.cancel_busy(session_id);
222 }
223
224 pub async fn wait_idle(&self, session_id: &str) {
225 let mut busy = self.gate.subscribe();
226 while self.is_busy(session_id) {
227 if busy.changed().await.is_err() {
228 break;
229 }
230 }
231 }
232}