1use crate::SharedStatus;
4use std::sync::{Arc, Mutex};
5use tokio::{
6 sync::oneshot,
7 time::{Duration, Instant},
8};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum CoolingError {
12 Busy,
13 Unavailable,
14 Expired,
15 Cancelled,
16 Invalid(String),
17 Uncertain { message: String, code: Option<i32> },
18}
19
20impl std::fmt::Display for CoolingError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 Self::Busy => f.write_str("Another cooler request is pending"),
24 Self::Unavailable => f.write_str("Camera controls are unavailable"),
25 Self::Expired => f.write_str("Cooler request expired before dispatch"),
26 Self::Cancelled => f.write_str("Cooler request was cancelled before dispatch"),
27 Self::Invalid(message) => f.write_str(message),
28 Self::Uncertain { message, .. } => write!(f, "Cooler outcome is uncertain: {message}"),
29 }
30 }
31}
32impl std::error::Error for CoolingError {}
33type Outcome = Result<i64, CoolingError>;
34#[derive(Clone, Copy, PartialEq, Eq)]
35enum Phase {
36 Queued,
37 Claimed,
38 Running,
39 Finished,
40}
41pub(crate) struct Request {
42 pub kind: i32,
43 pub value: i64,
44 pub deadline: Instant,
45 phase: Phase,
46 uncertain: bool,
47 reply: Option<oneshot::Sender<Outcome>>,
48}
49#[derive(Default)]
50struct State {
51 accepting: bool,
52 pending: Option<Arc<Mutex<Request>>>,
53}
54#[derive(Clone, Default)]
55pub(crate) struct Mailbox(Arc<Mutex<State>>);
56#[derive(Clone)]
57pub struct CoolingHandle {
58 pub(crate) mailbox: Mailbox,
59 pub(crate) status: SharedStatus,
60}
61pub struct CoolingReceipt {
62 mailbox: Mailbox,
63 request: Arc<Mutex<Request>>,
64 reply: oneshot::Receiver<Outcome>,
65 deadline: Instant,
66}
67#[derive(Clone)]
70pub struct CoolingCancellation {
71 mailbox: Mailbox,
72 request: Arc<Mutex<Request>>,
73}
74impl CoolingCancellation {
75 pub fn cancel_before_dispatch(&self) {
76 self.mailbox.cancel_queued(&self.request);
77 }
78}
79pub(crate) fn validate(status: &SharedStatus, kind: i32, value: i64) -> Result<(), CoolingError> {
80 if !matches!(kind, 16 | 17) {
81 return Err(CoolingError::Invalid(
82 "Only cooler target and enable are supported".into(),
83 ));
84 }
85 let state = status.lock().unwrap();
86 if !state.connected || !state.control_connection_available {
87 return Err(CoolingError::Unavailable);
88 }
89 let cap = state
90 .controls
91 .get(&kind)
92 .ok_or_else(|| CoolingError::Invalid("Cooler control is unavailable".into()))?;
93 if !cap.writable || value < cap.min || value > cap.max {
94 return Err(CoolingError::Invalid(format!(
95 "Cooler control must be writable and between {} and {}",
96 cap.min, cap.max
97 )));
98 }
99 Ok(())
100}
101impl CoolingHandle {
102 pub fn submit(
105 &self,
106 kind: i32,
107 value: i64,
108 timeout: Duration,
109 ) -> Result<CoolingReceipt, CoolingError> {
110 validate(&self.status, kind, value)?;
111 let deadline = Instant::now()
112 .checked_add(timeout)
113 .filter(|_| !timeout.is_zero())
114 .ok_or(CoolingError::Expired)?;
115 let (sender, reply) = oneshot::channel();
116 let request = Arc::new(Mutex::new(Request {
117 kind,
118 value,
119 deadline,
120 phase: Phase::Queued,
121 uncertain: false,
122 reply: Some(sender),
123 }));
124 let mut state = self.mailbox.0.lock().unwrap();
125 if !state.accepting {
126 return Err(CoolingError::Unavailable);
127 }
128 if state.pending.is_some() {
129 return Err(CoolingError::Busy);
130 }
131 state.pending = Some(request.clone());
132 Ok(CoolingReceipt {
133 mailbox: self.mailbox.clone(),
134 request,
135 reply,
136 deadline,
137 })
138 }
139 pub fn pending(&self) -> bool {
140 self.mailbox.0.lock().unwrap().pending.is_some()
141 }
142}
143impl CoolingReceipt {
144 pub fn cancellation(&self) -> CoolingCancellation {
145 CoolingCancellation {
146 mailbox: self.mailbox.clone(),
147 request: self.request.clone(),
148 }
149 }
150 pub async fn wait(mut self) -> Outcome {
151 tokio::select! {
152 biased;
153 result=&mut self.reply=>result.unwrap_or(Err(CoolingError::Unavailable)),
154 _=tokio::time::sleep_until(self.deadline)=>{
155 if let Some(error)=self.mailbox.expire(&self.request) {return Err(error);}
156 (&mut self.reply).await.unwrap_or(Err(CoolingError::Unavailable))
159 }
160 }
161 }
162}
163impl Drop for CoolingReceipt {
164 fn drop(&mut self) {
165 self.mailbox.cancel_queued(&self.request);
166 }
167}
168impl Mailbox {
169 pub fn activate(&self) {
170 self.retire();
171 self.0.lock().unwrap().accepting = true;
172 }
173 pub fn retire(&self) {
174 let mut state = self.0.lock().unwrap();
175 state.accepting = false;
176 if let Some(request) = state.pending.take() {
177 let mut request = request.lock().unwrap();
178 let result = if request.phase == Phase::Running {
179 Err(CoolingError::Uncertain {
180 message: "Camera owner retired during cooler dispatch".into(),
181 code: None,
182 })
183 } else {
184 Err(CoolingError::Unavailable)
185 };
186 request.phase = Phase::Finished;
187 if let Some(reply) = request.reply.take() {
188 let _ = reply.send(result);
189 }
190 }
191 }
192 fn cancel_queued(&self, request: &Arc<Mutex<Request>>) {
193 let mut state = self.0.lock().unwrap();
194 if !state
195 .pending
196 .as_ref()
197 .is_some_and(|pending| Arc::ptr_eq(pending, request))
198 {
199 return;
200 }
201 let mut request = request.lock().unwrap();
202 if matches!(request.phase, Phase::Queued | Phase::Claimed) {
203 request.phase = Phase::Finished;
204 state.pending = None;
205 if let Some(reply) = request.reply.take() {
206 let _ = reply.send(Err(CoolingError::Cancelled));
207 }
208 }
209 }
210 fn expire(&self, request: &Arc<Mutex<Request>>) -> Option<CoolingError> {
211 let mut state = self.0.lock().unwrap();
212 if !state
213 .pending
214 .as_ref()
215 .is_some_and(|pending| Arc::ptr_eq(pending, request))
216 {
217 return None;
218 }
219 let mut request = request.lock().unwrap();
220 if matches!(request.phase, Phase::Queued | Phase::Claimed) {
221 request.phase = Phase::Finished;
222 state.pending = None;
223 let error = CoolingError::Expired;
224 if let Some(reply) = request.reply.take() {
225 let _ = reply.send(Err(error.clone()));
226 }
227 Some(error)
228 } else {
229 request.uncertain = true;
231 state.accepting = false;
232 Some(CoolingError::Uncertain {
233 message: "Cooler acknowledgement exceeded its deadline".into(),
234 code: None,
235 })
236 }
237 }
238 pub fn claim(&self) -> Option<Arc<Mutex<Request>>> {
239 let mut state = self.0.lock().unwrap();
240 let request = state.pending.clone()?;
241 let mut command = request.lock().unwrap();
242 if command.phase != Phase::Queued {
243 return None;
244 }
245 if Instant::now() >= command.deadline {
246 command.phase = Phase::Finished;
247 state.pending = None;
248 if let Some(reply) = command.reply.take() {
249 let _ = reply.send(Err(CoolingError::Expired));
250 }
251 return None;
252 }
253 command.phase = Phase::Claimed;
254 drop(command);
255 Some(request)
256 }
257 pub fn dispatch(&self, request: &Arc<Mutex<Request>>) -> bool {
258 let mut state = self.0.lock().unwrap();
259 if !state
260 .pending
261 .as_ref()
262 .is_some_and(|pending| Arc::ptr_eq(pending, request))
263 {
264 return false;
265 }
266 let mut command = request.lock().unwrap();
267 if command.phase != Phase::Claimed || !state.accepting {
268 return false;
269 }
270 if Instant::now() >= command.deadline {
271 command.phase = Phase::Finished;
272 state.pending = None;
273 if let Some(reply) = command.reply.take() {
274 let _ = reply.send(Err(CoolingError::Expired));
275 }
276 return false;
277 }
278 command.phase = Phase::Running;
279 true
280 }
281 pub fn finish(
284 &self,
285 request: &Arc<Mutex<Request>>,
286 mut result: Outcome,
287 commit: impl FnOnce(),
288 ) -> Outcome {
289 let mut state = self.0.lock().unwrap();
290 let current = state
291 .pending
292 .as_ref()
293 .is_some_and(|pending| Arc::ptr_eq(pending, request));
294 let mut command = request.lock().unwrap();
295 if !current || !matches!(command.phase, Phase::Claimed | Phase::Running) {
296 return Err(CoolingError::Uncertain {
297 message: "Cooler owner changed before acknowledgement".into(),
298 code: None,
299 });
300 }
301 if !matches!(result, Err(CoolingError::Uncertain { .. }))
302 && (command.uncertain
303 || !state.accepting
304 || (result.is_ok()
305 && (command.phase != Phase::Running || Instant::now() > command.deadline)))
306 {
307 result = Err(CoolingError::Uncertain {
308 message: "Cooler acknowledgement exceeded its deadline".into(),
309 code: None,
310 });
311 }
312 if matches!(result, Err(CoolingError::Uncertain { .. })) {
313 state.accepting = false;
314 }
315 if result.is_ok() {
316 commit();
317 }
318 command.phase = Phase::Finished;
319 state.pending = None;
320 if let Some(reply) = command.reply.take() {
321 let _ = reply.send(result.clone());
322 }
323 result
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::{Control, Status};
331 fn handle() -> CoolingHandle {
332 let status = Status {
333 connected: true,
334 control_connection_available: true,
335 controls: [
336 (
337 16,
338 Control {
339 kind: 16,
340 min: -40,
341 max: 30,
342 value: 0,
343 writable: true,
344 },
345 ),
346 (
347 17,
348 Control {
349 kind: 17,
350 min: 0,
351 max: 1,
352 value: 0,
353 writable: true,
354 },
355 ),
356 ]
357 .into(),
358 values: [(16, 0), (17, 0)].into(),
359 ..Status::default()
360 };
361 let handle = CoolingHandle {
362 mailbox: Mailbox::default(),
363 status: Arc::new(Mutex::new(status)),
364 };
365 handle.mailbox.activate();
366 handle
367 }
368 fn dispatch(handle: &CoolingHandle) -> Arc<Mutex<Request>> {
369 let request = handle.mailbox.claim().unwrap();
370 assert!(handle.mailbox.dispatch(&request));
371 request
372 }
373 #[tokio::test]
374 async fn bounded_admission_validation_and_known_acknowledgement() {
375 let handle = handle();
376 for (kind, value) in [(0, 1), (16, -41), (16, 31), (17, 2)] {
377 assert!(matches!(
378 handle.submit(kind, value, Duration::from_secs(1)),
379 Err(CoolingError::Invalid(_))
380 ));
381 }
382 let receipt = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
383 assert!(matches!(
384 handle.submit(17, 1, Duration::from_secs(5)),
385 Err(CoolingError::Busy)
386 ));
387 assert_eq!(handle.status.lock().unwrap().values[&16], 0);
388 let request = dispatch(&handle);
389 handle
390 .mailbox
391 .finish(&request, Ok(-10), || {
392 handle.status.lock().unwrap().values.insert(16, -10);
393 })
394 .unwrap();
395 assert_eq!(receipt.wait().await, Ok(-10));
396 assert!(!handle.pending());
397 assert_eq!(handle.status.lock().unwrap().values[&16], -10);
398 }
399 #[tokio::test]
400 async fn queued_expiry_and_waiter_loss_never_dispatch() {
401 let handle = handle();
402 drop(handle.submit(17, 1, Duration::from_secs(5)).unwrap());
403 assert!(!handle.pending());
404 assert!(handle.mailbox.claim().is_none());
405 let receipt = handle.submit(16, -10, Duration::from_millis(1)).unwrap();
406 assert_eq!(receipt.wait().await, Err(CoolingError::Expired));
407 assert!(handle.mailbox.claim().is_none());
408 assert!(!handle.pending());
409 }
410 #[tokio::test]
411 async fn dispatched_waiter_loss_keeps_owned_commit() {
412 let handle = handle();
413 let receipt = handle.submit(17, 1, Duration::from_secs(5)).unwrap();
414 let request = dispatch(&handle);
415 drop(receipt);
416 assert!(handle.pending());
417 handle
418 .mailbox
419 .finish(&request, Ok(1), || {
420 handle.status.lock().unwrap().values.insert(17, 1);
421 })
422 .unwrap();
423 assert!(!handle.pending());
424 assert_eq!(handle.status.lock().unwrap().values[&17], 1);
425 }
426 #[tokio::test]
427 async fn dispatched_deadline_fences_and_cannot_commit_late_or_after_reactivation() {
428 let handle = handle();
429 let mut receipt = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
430 let request = dispatch(&handle);
431 let deadline = Instant::now();
434 request.lock().unwrap().deadline = deadline;
435 receipt.deadline = deadline;
436 assert!(matches!(
437 receipt.wait().await,
438 Err(CoolingError::Uncertain { .. })
439 ));
440 assert!(handle.pending());
441 assert!(matches!(
442 handle.submit(17, 1, Duration::from_secs(1)),
443 Err(CoolingError::Unavailable)
444 ));
445 assert!(matches!(
446 handle.mailbox.finish(&request, Ok(-10), || panic!(
447 "Late cooler result must not commit"
448 )),
449 Err(CoolingError::Uncertain { .. })
450 ));
451 handle.mailbox.activate();
452 let next = handle.submit(16, -15, Duration::from_secs(5)).unwrap();
453 assert!(matches!(
454 handle.mailbox.finish(&request, Ok(-10), || panic!(
455 "Old cooler result must not commit"
456 )),
457 Err(CoolingError::Uncertain { .. })
458 ));
459 assert!(handle.pending());
460 drop(next);
461 assert!(!handle.pending());
462 }
463 #[tokio::test]
464 async fn retirement_distinguishes_unsent_and_dispatched_and_preserves_codes() {
465 let handle = handle();
466 let queued = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
467 handle.mailbox.retire();
468 assert_eq!(queued.wait().await, Err(CoolingError::Unavailable));
469 handle.mailbox.activate();
470 let running = handle.submit(17, 1, Duration::from_secs(5)).unwrap();
471 let request = dispatch(&handle);
472 handle.mailbox.retire();
473 assert!(matches!(
474 running.wait().await,
475 Err(CoolingError::Uncertain { .. })
476 ));
477 assert!(matches!(
478 handle
479 .mailbox
480 .finish(&request, Ok(1), || panic!("Retired cooler must not commit")),
481 Err(CoolingError::Uncertain { .. })
482 ));
483 handle.mailbox.activate();
484 let receipt = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
485 let request = dispatch(&handle);
486 let error = CoolingError::Uncertain {
487 message: "private failure".into(),
488 code: Some(11),
489 };
490 assert_eq!(
491 handle
492 .mailbox
493 .finish(&request, Err(error.clone()), || panic!(
494 "Failed cooler must not commit"
495 )),
496 Err(error.clone())
497 );
498 assert_eq!(receipt.wait().await, Err(error));
499 assert!(!handle.pending());
500 assert!(matches!(
501 handle.submit(17, 1, Duration::from_secs(1)),
502 Err(CoolingError::Unavailable)
503 ));
504 }
505 #[tokio::test]
506 async fn caller_loss_and_deadline_during_preflight_skip_dispatch() {
507 let handle = handle();
508 let receipt = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
509 let request = handle.mailbox.claim().unwrap();
510 drop(receipt);
511 assert!(!handle.mailbox.dispatch(&request));
512 assert!(!handle.pending());
513 let mut receipt = handle.submit(17, 1, Duration::from_secs(5)).unwrap();
514 let request = handle.mailbox.claim().unwrap();
515 let deadline = Instant::now();
516 request.lock().unwrap().deadline = deadline;
517 receipt.deadline = deadline;
518 assert_eq!(receipt.wait().await, Err(CoolingError::Expired));
519 assert!(!handle.mailbox.dispatch(&request));
520 assert!(!handle.pending());
521 }
522 #[tokio::test]
523 async fn separate_caller_cancellation_preserves_the_owned_receipt() {
524 let handle = handle();
525 for claimed in [false, true] {
526 let receipt = handle.submit(16, -10, Duration::from_secs(5)).unwrap();
527 let cancellation = receipt.cancellation();
528 let request = claimed.then(|| handle.mailbox.claim().unwrap());
529 cancellation.cancel_before_dispatch();
530 assert_eq!(receipt.wait().await, Err(CoolingError::Cancelled));
531 if let Some(request) = request {
532 assert!(!handle.mailbox.dispatch(&request));
533 }
534 assert!(!handle.pending());
535 }
536 let receipt = handle.submit(17, 1, Duration::from_secs(5)).unwrap();
537 let cancellation = receipt.cancellation();
538 let request = dispatch(&handle);
539 cancellation.cancel_before_dispatch();
540 assert!(handle.pending());
541 handle.mailbox.finish(&request, Ok(1), || {}).unwrap();
542 assert_eq!(receipt.wait().await, Ok(1));
543 let next = handle.submit(16, -15, Duration::from_secs(5)).unwrap();
545 cancellation.cancel_before_dispatch();
546 assert!(handle.pending());
547 drop(next);
548 }
549}