use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use anyhow::{Result, bail};
use yaiba_core::Store;
pub(crate) struct Gate {
store: Arc<Mutex<Store>>,
closed: AtomicBool,
}
impl Gate {
pub(crate) fn new(store: Arc<Mutex<Store>>) -> Self {
Self {
store,
closed: AtomicBool::new(false),
}
}
pub(crate) fn with_store<T>(
&self,
f: impl FnOnce(&mut Store) -> yaiba_core::Result<T>,
) -> Result<T> {
let mut db = self.store.lock().unwrap_or_else(|e| e.into_inner());
Ok(f(&mut db)?)
}
pub(crate) fn merge(
&self,
entries: &[yaiba_core::Entry],
vv: &yaiba_core::VersionVector,
) -> Result<usize> {
let mut db = self.store.lock().unwrap_or_else(|e| e.into_inner());
if self.closed.load(Ordering::SeqCst) {
bail!("refused a merge: this node has been shut down");
}
Ok(db.merge(entries, vv)?)
}
pub(crate) fn close(&self) {
let _db = self.store.lock().unwrap_or_else(|e| e.into_inner());
self.closed.store(true, Ordering::SeqCst);
}
pub(crate) fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use yaiba_core::{NewTask, VersionVector};
fn gate_and_batch() -> (Gate, Vec<yaiba_core::Entry>, VersionVector) {
let mut peer = Store::open_in_memory().unwrap();
peer.create_task(NewTask {
title: "written by a peer".into(),
..Default::default()
})
.unwrap();
let vv = peer.version_vector().unwrap();
let entries = peer.entries_since(&VersionVector::default()).unwrap();
assert!(!entries.is_empty(), "the batch has to be worth merging");
let store = Store::open_in_memory().unwrap();
(Gate::new(Arc::new(Mutex::new(store))), entries, vv)
}
fn task_count(gate: &Gate) -> usize {
gate.with_store(|db| db.snapshot()).unwrap().tasks.len()
}
#[test]
fn merges_while_open() {
let (gate, entries, vv) = gate_and_batch();
assert!(gate.merge(&entries, &vv).unwrap() > 0);
assert_eq!(task_count(&gate), 1);
}
#[test]
fn refuses_merges_after_close() {
let (gate, entries, vv) = gate_and_batch();
gate.close();
let refused = gate.merge(&entries, &vv).unwrap_err();
assert!(
refused.to_string().contains("shut down"),
"unexpected error: {refused:#}"
);
assert_eq!(task_count(&gate), 0);
}
#[test]
fn close_is_idempotent() {
let (gate, entries, vv) = gate_and_batch();
gate.close();
gate.close();
assert!(gate.is_closed());
assert!(gate.merge(&entries, &vv).is_err());
}
#[test]
fn close_waits_for_a_merge_already_running() {
let (gate, _entries, _vv) = gate_and_batch();
let (closed_tx, closed_rx) = mpsc::channel();
thread::scope(|scope| {
let held = gate.store.lock().unwrap();
scope.spawn(|| {
gate.close();
let _ = closed_tx.send(());
});
thread::sleep(Duration::from_millis(50));
assert!(!gate.is_closed(), "closed while a merge held the store");
drop(held);
closed_rx
.recv_timeout(Duration::from_secs(5))
.expect("close never completed after the store was released");
assert!(gate.is_closed());
});
}
}