use std::{
io::{Error as IoError, ErrorKind, Result as IoResult},
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Barrier,
},
thread,
time::Duration,
};
use bytes::BufMut;
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
use futures::future::BoxFuture;
use pi_async_rt::rt::{
multi_thread::{MultiTaskRuntime, MultiTaskRuntimeBuilder},
AsyncRuntime,
};
use pi_async_transaction::AsyncCommitLog;
use pi_db::KVDBCommitConfirm;
use pi_guid::Guid;
const TASK_DEADLINE: Duration = Duration::from_secs(5);
const NO_CONFIRM_OBSERVATION: Duration = Duration::from_millis(300);
#[derive(Clone)]
struct RecordingCommitLog {
confirmed_tx: Sender<Guid>,
confirm_count: Arc<AtomicUsize>,
}
impl RecordingCommitLog {
fn new() -> (Self, Receiver<Guid>) {
let (confirmed_tx, confirmed_rx) = bounded(16);
(
Self {
confirmed_tx,
confirm_count: Arc::new(AtomicUsize::new(0)),
},
confirmed_rx,
)
}
fn confirm_count(&self) -> usize {
self.confirm_count.load(Ordering::SeqCst)
}
}
impl AsyncCommitLog for RecordingCommitLog {
type C = ();
type Cid = Guid;
fn append<B>(&self, _commit_uid: Self::Cid, _log: B) -> BoxFuture<'static, IoResult<Self::C>>
where
B: BufMut + AsRef<[u8]> + Send + Sized + 'static,
{
Box::pin(async { Ok(()) })
}
fn flush(&self, _log_handle: Self::C) -> BoxFuture<'static, IoResult<()>> {
Box::pin(async { Ok(()) })
}
fn confirm(&self, commit_uid: Self::Cid) -> BoxFuture<'static, IoResult<()>> {
let confirmed_tx = self.confirmed_tx.clone();
let confirm_count = self.confirm_count.clone();
Box::pin(async move {
confirm_count.fetch_add(1, Ordering::SeqCst);
confirmed_tx.send(commit_uid).map_err(|error| {
IoError::new(
ErrorKind::BrokenPipe,
format!("recording confirm receiver closed: {error}"),
)
})
})
}
fn start_replay<B, F>(&self, _callback: Arc<F>) -> BoxFuture<'static, IoResult<(usize, usize)>>
where
B: BufMut + AsRef<[u8]> + From<Vec<u8>> + Send + Sized + 'static,
F: Fn(Self::Cid, B) -> IoResult<()> + Send + Sync + 'static,
{
Box::pin(async { Ok((0, 0)) })
}
fn append_replay<B>(
&self,
_commit_uid: Self::Cid,
_log: B,
) -> BoxFuture<'static, IoResult<Self::C>>
where
B: BufMut + AsRef<[u8]> + Send + Sized + 'static,
{
Box::pin(async { Ok(()) })
}
fn flush_replay(&self, _log_handle: Self::C) -> BoxFuture<'static, IoResult<()>> {
Box::pin(async { Ok(()) })
}
fn confirm_replay(&self, _commit_uid: Self::Cid) -> BoxFuture<'static, IoResult<()>> {
Box::pin(async { Ok(()) })
}
fn finish_replay(&self) -> BoxFuture<'static, IoResult<()>> {
Box::pin(async { Ok(()) })
}
fn check_point_of(&self, _commit_uid: Self::Cid) -> BoxFuture<'static, Option<usize>> {
Box::pin(async { None })
}
fn current_check_point(&self) -> BoxFuture<'static, usize> {
Box::pin(async { 0 })
}
fn append_check_point(&self) -> BoxFuture<'static, IoResult<usize>> {
Box::pin(async { Ok(0) })
}
fn waiting_confirm_count(&self) -> BoxFuture<'static, usize> {
Box::pin(async { 0 })
}
fn append_total_count(&self) -> usize {
0
}
fn confirm_total_count(&self) -> usize {
self.confirm_count()
}
}
struct ConfirmHarness {
rt: MultiTaskRuntime<()>,
log: RecordingCommitLog,
confirmed_rx: Receiver<Guid>,
transaction_uid: Guid,
commit_uid: Guid,
}
impl ConfirmHarness {
fn new() -> Self {
let rt = MultiTaskRuntimeBuilder::default()
.init_worker_size(1)
.build();
let (log, confirmed_rx) = RecordingCommitLog::new();
Self {
rt,
log,
confirmed_rx,
transaction_uid: Guid(0x1001),
commit_uid: Guid(0x2001),
}
}
fn confirmer(
&self,
persistent_child_count: usize,
) -> KVDBCommitConfirm<(), RecordingCommitLog> {
KVDBCommitConfirm::new(
self.rt.clone(),
self.log.clone(),
self.transaction_uid.clone(),
Some(self.commit_uid.clone()),
persistent_child_count,
)
}
fn assert_not_confirmed(&self, scenario: &str) {
let (fence_tx, fence_rx) = bounded(1);
self.rt
.spawn(async move {
let _ = fence_tx.send(());
})
.expect("the runtime must accept the fence task");
fence_rx
.recv_timeout(TASK_DEADLINE)
.expect("the runtime must execute the fence task before the deadline");
match self.confirmed_rx.recv_timeout(NO_CONFIRM_OBSERVATION) {
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
panic!("{scenario}: recording commit log disconnected unexpectedly")
}
Ok(commit_uid) => {
panic!("{scenario}: root WAL was confirmed early with {commit_uid:?}")
}
}
assert_eq!(
self.log.confirm_count(),
0,
"{scenario}: AsyncCommitLog::confirm must not be called"
);
}
fn assert_confirmed_once(&self, scenario: &str) {
let confirmed_uid = self
.confirmed_rx
.recv_timeout(TASK_DEADLINE)
.unwrap_or_else(|error| panic!("{scenario}: confirmation deadline exceeded: {error}"));
assert_eq!(confirmed_uid, self.commit_uid, "{scenario}: commit UID");
assert_eq!(self.log.confirm_count(), 1, "{scenario}: confirm count");
assert_eq!(
self.confirmed_rx.recv_timeout(NO_CONFIRM_OBSERVATION),
Err(RecvTimeoutError::Timeout),
"{scenario}: the root WAL must be confirmed exactly once"
);
}
}
#[test]
fn test_commit_confirm_waits_for_all_successful_children() {
let harness = ConfirmHarness::new();
let confirmer = harness.confirmer(3);
for _ in 0..2 {
assert!(confirmer(
harness.transaction_uid.clone(),
harness.commit_uid.clone(),
Ok(())
)
.is_ok());
}
harness.assert_not_confirmed("first two of three successful children");
assert!(confirmer(
harness.transaction_uid.clone(),
harness.commit_uid.clone(),
Ok(())
)
.is_ok());
harness.assert_confirmed_once("all three successful children");
}
#[test]
fn test_commit_confirm_concurrent_success_callbacks_confirm_once() {
const PERSISTENT_CHILD_COUNT: usize = 16;
let harness = ConfirmHarness::new();
let confirmer = harness.confirmer(PERSISTENT_CHILD_COUNT);
let start = Arc::new(Barrier::new(PERSISTENT_CHILD_COUNT));
let mut handles = Vec::with_capacity(PERSISTENT_CHILD_COUNT);
for _ in 0..PERSISTENT_CHILD_COUNT {
let confirmer = confirmer.clone();
let start = start.clone();
let transaction_uid = harness.transaction_uid.clone();
let commit_uid = harness.commit_uid.clone();
handles.push(thread::spawn(move || {
start.wait();
confirmer(transaction_uid, commit_uid, Ok(()))
}));
}
for handle in handles {
assert!(
handle
.join()
.expect("successful callback thread must not panic")
.is_ok(),
"every valid success signal must be accepted"
);
}
harness.assert_confirmed_once("concurrent successful children");
}
#[test]
fn test_commit_confirm_rejects_mismatched_ids_without_consuming_count() {
let harness = ConfirmHarness::new();
let confirmer = harness.confirmer(2);
assert!(
confirmer(Guid(0xdead), harness.commit_uid.clone(), Ok(())).is_err(),
"a mismatched transaction UID must be rejected"
);
assert!(
confirmer(harness.transaction_uid.clone(), Guid(0xbeef), Ok(())).is_err(),
"a mismatched commit UID must be rejected"
);
harness.assert_not_confirmed("mismatched transaction identities");
for _ in 0..2 {
assert!(confirmer(
harness.transaction_uid.clone(),
harness.commit_uid.clone(),
Ok(())
)
.is_ok());
}
harness.assert_confirmed_once("valid callbacks after rejected identities");
}