use std::{
sync::{Mutex, PoisonError},
time::Duration,
};
use polyc_state::{
conformance::{ConformanceAdapter, SyntheticCommand, SyntheticRecord},
context::CallContext,
deadline::MonotonicInstant,
error::StateError,
id::{CommandId, SnapshotId},
memory::MemoryState,
page::{Page, PageRequest},
receipt::Receipt,
stream::{StreamChunk, StreamContract, StreamRequest},
};
pub trait ConformanceBackend: Send + Sync + 'static {
fn submit(
&self,
command: SyntheticCommand,
context: &CallContext,
) -> Result<Receipt, StateError>;
fn receipt(&self, command_id: &CommandId) -> Result<Option<Receipt>, StateError>;
fn create_snapshot(&self, context: &CallContext) -> Result<SnapshotId, StateError>;
fn read_page(
&self,
request: PageRequest,
context: &CallContext,
) -> Result<Page<SyntheticRecord>, StateError>;
fn read_chunk(
&self,
request: StreamRequest,
context: &CallContext,
) -> Result<StreamChunk<SyntheticRecord>, StateError>;
fn stream_contract(&self) -> StreamContract;
fn applied_effects(&self) -> u64;
fn now(&self) -> MonotonicInstant;
fn advance_clock(&self, budget: Duration);
}
#[derive(Debug, Default)]
pub struct MemoryBackend {
state: Mutex<MemoryState>,
}
impl MemoryBackend {
#[must_use]
pub const fn new() -> Self {
Self {
state: Mutex::new(MemoryState::new()),
}
}
fn with<R>(&self, call: impl FnOnce(&mut MemoryState) -> R) -> R {
let mut guard = self.state.lock().unwrap_or_else(PoisonError::into_inner);
call(&mut guard)
}
}
impl ConformanceBackend for MemoryBackend {
fn submit(
&self,
command: SyntheticCommand,
context: &CallContext,
) -> Result<Receipt, StateError> {
self.with(|state| state.submit(command, context))
}
fn receipt(&self, command_id: &CommandId) -> Result<Option<Receipt>, StateError> {
self.with(|state| state.receipt(command_id))
}
fn create_snapshot(&self, context: &CallContext) -> Result<SnapshotId, StateError> {
self.with(|state| state.create_snapshot(context))
}
fn read_page(
&self,
request: PageRequest,
context: &CallContext,
) -> Result<Page<SyntheticRecord>, StateError> {
self.with(|state| state.read_page(request, context))
}
fn read_chunk(
&self,
request: StreamRequest,
context: &CallContext,
) -> Result<StreamChunk<SyntheticRecord>, StateError> {
self.with(|state| state.read_chunk(request, context))
}
fn stream_contract(&self) -> StreamContract {
self.with(|state| state.stream_contract())
}
fn applied_effects(&self) -> u64 {
self.with(|state| state.applied_effects())
}
fn now(&self) -> MonotonicInstant {
self.with(|state| state.now())
}
fn advance_clock(&self, budget: Duration) {
self.with(|state| state.advance_clock(budget));
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use super::*;
use polyc_state::{cancel::CancellationToken, conformance::SyntheticOp, deadline::Deadline};
fn context(backend: &MemoryBackend) -> CallContext {
CallContext::new(
Deadline::after(backend.now(), Duration::from_secs(30)),
CancellationToken::new(),
)
}
#[test]
fn the_backend_returns_what_the_module_decided() {
let backend = MemoryBackend::new();
let ctx = context(&backend);
let receipt = backend
.submit(
SyntheticCommand::new("cmd-1", SyntheticOp::Append { amount: 4 }),
&ctx,
)
.unwrap();
assert!(receipt.is_new());
assert_eq!(backend.applied_effects(), 1);
let replay = backend
.submit(
SyntheticCommand::new("cmd-1", SyntheticOp::Append { amount: 4 }),
&ctx,
)
.unwrap();
assert!(replay.is_deduplicated());
assert_eq!(backend.applied_effects(), 1);
let conflict = backend
.submit(
SyntheticCommand::new("cmd-1", SyntheticOp::Append { amount: 5 }),
&ctx,
)
.unwrap_err();
assert!(matches!(conflict, StateError::DigestConflict { .. }));
}
#[test]
fn the_clock_moves_only_when_a_caller_moves_it() {
let backend = MemoryBackend::new();
assert_eq!(backend.now(), MonotonicInstant::ORIGIN);
backend.advance_clock(Duration::from_secs(3));
assert_eq!(backend.now(), MonotonicInstant::from_nanos(3_000_000_000));
}
#[test]
fn a_poisoned_lock_still_answers() {
let backend = std::sync::Arc::new(MemoryBackend::new());
let poisoner = std::sync::Arc::clone(&backend);
let _ = std::thread::spawn(move || {
poisoner.with(|_| panic!("poison the lock"));
})
.join();
assert_eq!(backend.applied_effects(), 0);
}
}