use std::collections::BTreeSet;
use std::error::Error;
use std::io;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use haematite::tree::Hash;
use liminal::durability::bridge::block_on;
use liminal::durability::{
DurabilityError, DurableStore, RoutedBatchItem, ScanCursor, ScanPage, StoredEntry,
};
use super::registry::REGISTRY_STREAM_KEY;
use super::registry_snapshot::{RegistrySnapshot, SNAPSHOT_KEY};
use super::routed_stream_codec::head_key;
use super::tests_memory_store::MemoryStore;
pub(super) const ROW_SCHEMA: u8 = 1;
pub(super) type TestResult<T = ()> = Result<T, Box<dyn Error>>;
pub(super) const KEY: &str = REGISTRY_STREAM_KEY;
#[derive(Debug, Default)]
pub(super) struct ReadCounts {
pub(super) row_reads: u64,
pub(super) rows: u64,
pub(super) head_reads: u64,
pub(super) routed_keys: Vec<Vec<u8>>,
}
#[derive(Debug)]
pub(super) struct Recording {
inner: Arc<dyn DurableStore>,
batches: Mutex<Vec<(String, Vec<RoutedBatchItem>)>>,
pub(super) flushes: AtomicU64,
reads: Mutex<ReadCounts>,
pub(super) refuse_snapshot: AtomicBool,
}
impl Recording {
pub(super) fn new(inner: Arc<dyn DurableStore>) -> Arc<Self> {
Arc::new(Self {
inner,
batches: Mutex::new(Vec::new()),
flushes: AtomicU64::new(0),
reads: Mutex::new(ReadCounts::default()),
refuse_snapshot: AtomicBool::new(false),
})
}
pub(super) fn shared(self: &Arc<Self>) -> Arc<dyn DurableStore> {
Arc::clone(self) as Arc<dyn DurableStore>
}
pub(super) fn take_batches(&self) -> TestResult<Vec<(String, Vec<RoutedBatchItem>)>> {
Ok(std::mem::take(
&mut *self.batches.lock().map_err(|_| "batch log poisoned")?,
))
}
pub(super) fn take_reads(&self) -> TestResult<ReadCounts> {
Ok(std::mem::take(
&mut *self.reads.lock().map_err(|_| "read log poisoned")?,
))
}
fn note(&self, work: impl FnOnce(&mut ReadCounts)) -> Result<(), DurabilityError> {
let mut reads = self
.reads
.lock()
.map_err(|_| DurabilityError::ConfigError("read log poisoned".to_owned()))?;
work(&mut reads);
drop(reads);
Ok(())
}
}
#[async_trait::async_trait]
impl DurableStore for Recording {
async fn apply_routed_batch(
&self,
route_key: &str,
items: Vec<RoutedBatchItem>,
) -> Result<(), DurabilityError> {
self.batches
.lock()
.map_err(|_| DurabilityError::ConfigError("batch log poisoned".to_owned()))?
.push((route_key.to_owned(), items.clone()));
if items.iter().any(|item| item.key == SNAPSHOT_KEY)
&& self.refuse_snapshot.swap(false, Ordering::SeqCst)
{
return Err(DurabilityError::ConfigError(
"the registry snapshot batch was refused before it applied".to_owned(),
));
}
self.inner.apply_routed_batch(route_key, items).await
}
async fn get_routed(
&self,
route_key: &str,
key: &[u8],
) -> Result<Option<Vec<u8>>, DurabilityError> {
if route_key == KEY {
self.note(|reads| reads.routed_keys.push(key.to_vec()))?;
}
self.inner.get_routed(route_key, key).await
}
async fn append(
&self,
stream_key: &str,
payload: Vec<u8>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
self.inner.append(stream_key, payload, expected_seq).await
}
async fn append_all(
&self,
stream_key: &str,
payloads: Vec<Vec<u8>>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
self.inner
.append_all(stream_key, payloads, expected_seq)
.await
}
async fn read_from(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Vec<StoredEntry>, DurabilityError> {
let rows = self.inner.read_from(stream_key, offset, limit).await?;
if stream_key == KEY {
let count = u64::try_from(rows.len()).unwrap_or(u64::MAX);
self.note(|reads| {
reads.row_reads += 1;
reads.rows += count;
})?;
}
Ok(rows)
}
async fn read_at(
&self,
stream_key: &str,
sequence: u64,
) -> Result<Option<StoredEntry>, DurabilityError> {
let row = self.inner.read_at(stream_key, sequence).await?;
if stream_key == KEY {
let count = u64::from(row.is_some());
self.note(|reads| {
reads.row_reads += 1;
reads.rows += count;
})?;
}
Ok(row)
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
self.inner.cas(key, old_value, new_value).await
}
async fn read_stream_next_seq(&self, stream_key: &str) -> Result<Option<u64>, DurabilityError> {
if stream_key == KEY {
self.note(|reads| reads.head_reads += 1)?;
}
self.inner.read_stream_next_seq(stream_key).await
}
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
self.inner.read_value(key).await
}
async fn scan_page(
&self,
prefix: &str,
cursor: Option<ScanCursor>,
limit: usize,
) -> Result<ScanPage, DurabilityError> {
self.inner.scan_page(prefix, cursor, limit).await
}
async fn flush(&self) -> Result<(), DurabilityError> {
self.flushes.fetch_add(1, Ordering::SeqCst);
self.inner.flush().await
}
async fn truncate_before(
&self,
stream_key: &str,
before_seq: u64,
) -> Result<u64, DurabilityError> {
self.inner.truncate_before(stream_key, before_seq).await
}
async fn scan_routed(
&self,
route_key: &str,
from: &[u8],
to: &[u8],
) -> Result<Vec<(Vec<u8>, Vec<u8>)>, DurabilityError> {
self.inner.scan_routed(route_key, from, to).await
}
async fn remove_routed_range(
&self,
route_key: &str,
from: &[u8],
to: &[u8],
) -> Result<u64, DurabilityError> {
self.inner.remove_routed_range(route_key, from, to).await
}
fn store_counts(&self) -> Option<liminal::durability::StoreCountsHandle> {
self.inner.store_counts()
}
}
#[derive(Clone, Default)]
struct Diagnostics(Arc<Mutex<Vec<u8>>>);
impl io::Write for Diagnostics {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0
.lock()
.map_err(|_| io::Error::other("diagnostic buffer poisoned"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for Diagnostics {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
pub(super) fn logged<T>(work: impl FnOnce() -> T) -> TestResult<(T, String)> {
let diagnostics = Diagnostics::default();
let subscriber = tracing_subscriber::fmt()
.without_time()
.with_ansi(false)
.with_max_level(tracing::Level::WARN)
.with_writer(diagnostics.clone())
.finish();
let answer = tracing::subscriber::with_default(subscriber, work);
let bytes = diagnostics
.0
.lock()
.map_err(|_| "diagnostic buffer poisoned")?
.clone();
Ok((answer, String::from_utf8(bytes)?))
}
pub(super) fn memory() -> Arc<dyn DurableStore> {
Arc::new(MemoryStore::default())
}
fn payload(id: u64) -> TestResult<Vec<u8>> {
Ok(serde_json::to_vec(&serde_json::json!({
"schema_version": 1,
"conversation_id": id,
}))?)
}
pub(super) fn append_flat(store: &dyn DurableStore, payloads: Vec<Vec<u8>>) -> TestResult<u64> {
let start = block_on(store.read_stream_next_seq(KEY))??.unwrap_or(0);
Ok(block_on(store.append_all(KEY, payloads, start))??)
}
pub(super) fn flat_ids(
store: &dyn DurableStore,
ids: impl Iterator<Item = u64>,
) -> TestResult<u64> {
append_flat(store, ids.map(payload).collect::<TestResult<Vec<_>>>()?)
}
pub(super) fn stored_snapshot(
store: &dyn DurableStore,
) -> TestResult<Option<(RegistrySnapshot, Hash)>> {
block_on(store.get_routed(KEY, SNAPSHOT_KEY))??
.map(|bytes| {
Ok((
RegistrySnapshot::decode(&bytes, ROW_SCHEMA)?,
Hash::of(&bytes),
))
})
.transpose()
}
pub(super) fn snapshot_in(
items: &[RoutedBatchItem],
) -> TestResult<(RegistrySnapshot, Option<Hash>)> {
let item = items
.iter()
.find(|item| item.key == SNAPSHOT_KEY)
.ok_or("the batch carries no snapshot")?;
Ok((
RegistrySnapshot::decode(&item.value, ROW_SCHEMA)?,
item.expected,
))
}
pub(super) fn row_keys(keys: &[Vec<u8>]) -> TestResult<Vec<Vec<u8>>> {
let head = head_key(KEY)?;
Ok(keys
.iter()
.filter(|key| key.as_slice() != SNAPSHOT_KEY && **key != head)
.cloned()
.collect())
}
pub(super) fn ascending(range: std::ops::Range<u64>) -> BTreeSet<u64> {
range.collect()
}