use std::sync::{Arc, Mutex};
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloseAnswer {
Proceed,
Cancel,
Dropped,
}
type Wake = Arc<dyn Fn() + Send + Sync>;
#[derive(Default)]
struct Slot {
answer: Option<CloseAnswer>,
wake: Option<Wake>,
}
fn answer(slot: &Mutex<Slot>, answer: CloseAnswer) {
let wake = {
let mut slot = slot.lock().unwrap_or_else(|poison| poison.into_inner());
if slot.answer.is_some() {
return;
}
slot.answer = Some(answer);
slot.wake.clone()
};
if let Some(wake) = wake {
wake();
}
}
struct Holder(Arc<Mutex<Slot>>);
impl Drop for Holder {
fn drop(&mut self) {
answer(&self.0, CloseAnswer::Dropped);
}
}
#[derive(Clone)]
pub struct CloseDecision(Arc<Holder>);
impl CloseDecision {
pub fn new() -> Self {
Self(Arc::new(Holder(Arc::new(Mutex::new(Slot::default())))))
}
pub fn proceed(&self) {
answer(&self.0.0, CloseAnswer::Proceed);
}
pub fn cancel(&self) {
answer(&self.0.0, CloseAnswer::Cancel);
}
#[doc(hidden)]
pub fn receipt(&self) -> CloseDecisionReceipt {
CloseDecisionReceipt(self.0.0.clone())
}
}
impl Default for CloseDecision {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CloseDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CloseDecision")
.field(&self.receipt().answer())
.finish()
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct CloseDecisionReceipt(Arc<Mutex<Slot>>);
impl CloseDecisionReceipt {
pub fn answer(&self) -> Option<CloseAnswer> {
self.0
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.answer
}
pub fn on_answer(&self, wake: Arc<dyn Fn() + Send + Sync>) {
self.0
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.wake = Some(wake);
}
}
impl std::fmt::Debug for CloseDecisionReceipt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CloseDecisionReceipt")
.field(&self.answer())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn counting_wake() -> (Arc<AtomicUsize>, Wake) {
let count = Arc::new(AtomicUsize::new(0));
let wake: Wake = {
let count = count.clone();
Arc::new(move || {
count.fetch_add(1, Ordering::SeqCst);
})
};
(count, wake)
}
#[test]
fn unanswered_until_answered_and_the_first_answer_counts() {
let decision = CloseDecision::new();
let receipt = decision.receipt();
assert_eq!(receipt.answer(), None);
decision.clone().proceed();
assert_eq!(receipt.answer(), Some(CloseAnswer::Proceed));
decision.cancel();
assert_eq!(
receipt.answer(),
Some(CloseAnswer::Proceed),
"a later answer is ignored"
);
}
#[test]
fn dropping_every_clone_unanswered_answers_dropped() {
let decision = CloseDecision::new();
let receipt = decision.receipt();
let clone = decision.clone();
drop(decision);
assert_eq!(receipt.answer(), None, "a clone is still alive");
drop(clone);
assert_eq!(receipt.answer(), Some(CloseAnswer::Dropped));
}
#[test]
fn dropping_an_answered_decision_keeps_its_answer() {
let decision = CloseDecision::new();
let receipt = decision.receipt();
decision.cancel();
drop(decision);
assert_eq!(receipt.answer(), Some(CloseAnswer::Cancel));
}
#[test]
fn the_receipt_does_not_keep_the_decision_alive() {
let decision = CloseDecision::new();
let receipt = decision.receipt();
let _second_receipt = receipt.clone();
drop(decision);
assert_eq!(receipt.answer(), Some(CloseAnswer::Dropped));
}
#[test]
fn the_wake_runs_once_for_the_answer_and_not_for_an_ignored_one() {
let decision = CloseDecision::new();
let (count, wake) = counting_wake();
decision.receipt().on_answer(wake);
decision.proceed();
decision.cancel();
drop(decision);
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn the_wake_runs_for_a_drop_too() {
let decision = CloseDecision::new();
let (count, wake) = counting_wake();
decision.receipt().on_answer(wake);
drop(decision);
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn answered_from_another_thread() {
let decision = CloseDecision::new();
let receipt = decision.receipt();
let answer = decision.clone();
std::thread::spawn(move || answer.proceed())
.join()
.expect("the answering thread");
assert_eq!(receipt.answer(), Some(CloseAnswer::Proceed));
}
}