use std::fmt::Write as _;
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use liminal::durability::{
DurabilityError, DurableStore, RoutedBatchItem, ScanCursor, ScanPage, StoredEntry,
open_ephemeral,
};
use liminal_protocol::wire::ConnectionIncarnation;
use tracing_subscriber::prelude::*;
use super::ConnectionSupervisor;
use super::participant_only_services::supervisor_over;
use crate::ServerError;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const WITHIN: Duration = Duration::from_secs(5);
const ABANDONED: &str = "incarnation start abandoned at shutdown";
#[derive(Debug)]
struct Gated {
inner: Arc<dyn DurableStore>,
read_armed: AtomicBool,
write_armed: AtomicBool,
entered: Mutex<Sender<()>>,
release: Mutex<Receiver<()>>,
writes: AtomicUsize,
}
impl Gated {
fn new(inner: Arc<dyn DurableStore>) -> (Arc<Self>, Receiver<()>, Sender<()>) {
let (entered_send, entered) = channel();
let (release, released) = channel();
let store = Arc::new(Self {
inner,
read_armed: AtomicBool::new(false),
write_armed: AtomicBool::new(false),
entered: Mutex::new(entered_send),
release: Mutex::new(released),
writes: AtomicUsize::new(0),
});
(store, entered, release)
}
fn writes(&self) -> usize {
self.writes.load(Ordering::SeqCst)
}
fn park(&self, armed: &AtomicBool) -> Result<(), DurabilityError> {
if !armed.swap(false, Ordering::SeqCst) {
return Ok(());
}
let failed = |error: String| DurabilityError::ConfigError(error);
self.entered
.lock()
.map_err(|error| failed(error.to_string()))?
.send(())
.map_err(|error| failed(error.to_string()))?;
self.release
.lock()
.map_err(|error| failed(error.to_string()))?
.recv()
.map_err(|error| failed(error.to_string()))
}
fn before_read(&self) -> Result<(), DurabilityError> {
self.park(&self.read_armed)
}
fn wrote<T>(&self, result: Result<T, DurabilityError>) -> Result<T, DurabilityError> {
let value = result?;
self.park(&self.write_armed)?;
Ok(value)
}
fn begin_write(&self) {
self.writes.fetch_add(1, Ordering::SeqCst);
}
}
#[async_trait::async_trait]
impl DurableStore for Gated {
async fn apply_routed_batch(
&self,
route_key: &str,
items: Vec<RoutedBatchItem>,
) -> Result<(), DurabilityError> {
self.begin_write();
self.wrote(self.inner.apply_routed_batch(route_key, items).await)
}
async fn get_routed(
&self,
route_key: &str,
key: &[u8],
) -> Result<Option<Vec<u8>>, DurabilityError> {
self.before_read()?;
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.begin_write();
self.wrote(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.begin_write();
self.wrote(
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> {
self.before_read()?;
self.inner.read_from(stream_key, offset, limit).await
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
self.begin_write();
self.wrote(self.inner.cas(key, old_value, new_value).await)
}
async fn read_stream_next_seq(&self, stream_key: &str) -> Result<Option<u64>, DurabilityError> {
self.before_read()?;
self.inner.read_stream_next_seq(stream_key).await
}
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
self.before_read()?;
self.inner.read_value(key).await
}
async fn scan_page(
&self,
prefix: &str,
cursor: Option<ScanCursor>,
limit: usize,
) -> Result<ScanPage, DurabilityError> {
self.before_read()?;
self.inner.scan_page(prefix, cursor, limit).await
}
async fn flush(&self) -> Result<(), DurabilityError> {
self.begin_write();
self.wrote(self.inner.flush().await)
}
}
#[derive(Default)]
struct Errors(Arc<Mutex<Vec<String>>>);
struct Line(String);
impl tracing::field::Visit for Line {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if write!(self.0, " {}={value:?}", field.name()).is_err() {
self.0.push_str(" <unwritten>");
}
}
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Errors {
fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
if *event.metadata().level() != tracing::Level::ERROR {
return;
}
let mut line = Line(format!("{}:", event.metadata().target()));
event.record(&mut line);
if let Ok(mut errors) = self.0.lock() {
errors.push(line.0);
}
}
}
fn capture_errors() -> Result<Arc<Mutex<Vec<String>>>, Box<dyn std::error::Error>> {
let errors = Errors::default();
let lines = Arc::clone(&errors.0);
tracing::subscriber::set_global_default(tracing_subscriber::registry().with(errors))?;
Ok(lines)
}
fn admit_one(
supervisor: &ConnectionSupervisor,
) -> Result<ConnectionIncarnation, Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let _client = TcpStream::connect(listener.local_addr()?)?;
let (server, _) = listener.accept()?;
let handle = supervisor.spawn_connection(server)?;
Ok(handle
.connection_incarnation()
.ok_or("an admitted connection carries its incarnation")?)
}
fn boot_once(
store: Arc<dyn DurableStore>,
) -> Result<ConnectionIncarnation, Box<dyn std::error::Error>> {
let supervisor = supervisor_over(store)?;
let allocated = admit_one(&supervisor)?;
supervisor.shutdown();
supervisor.finish_store_work()?;
Ok(allocated)
}
fn is_abandoned(error: &ServerError) -> bool {
matches!(error, ServerError::ParticipantIncarnation { phase, .. } if *phase == ABANDONED)
}
fn a_stop_while_parked(arm: impl FnOnce(&Gated)) -> TestResult {
let errors = capture_errors()?;
let inner: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let first = boot_once(Arc::clone(&inner))?;
let (gated, entered, release) = Gated::new(Arc::clone(&inner));
let store: Arc<dyn DurableStore> = gated.clone();
let supervisor = supervisor_over(store)?;
arm(&gated);
supervisor.listening()?;
entered.recv_timeout(WITHIN)?;
supervisor.shutdown();
let writes_at_shutdown = gated.writes();
release.send(())?;
let finished = supervisor.finish_store_work();
assert_eq!(
gated.writes(),
writes_at_shutdown,
"the start began a write after the shutdown began"
);
assert!(
finished.is_ok(),
"an abandoned start is a clean stop: {finished:?}"
);
let failure = supervisor.incarnation_start_failure();
assert!(
failure.as_ref().is_some_and(is_abandoned),
"the start answers {ABANDONED:?}: {failure:?}"
);
let later = admit_one(&supervisor);
assert!(
later
.as_ref()
.is_err_and(|error| error.downcast_ref().is_some_and(is_abandoned)),
"a later admission is refused with {ABANDONED:?}: {later:?}"
);
drop(supervisor);
let next = boot_once(inner)?;
assert!(
next.server_incarnation > first.server_incarnation,
"the next start allocates above every published incarnation: {next:?} after {first:?}"
);
let logged = errors.lock().map_err(|error| error.to_string())?.clone();
assert!(
logged.is_empty(),
"an abandoned start, and a stop over a fresh connection, log no error: {logged:?}"
);
Ok(())
}
#[test]
fn a_shutdown_during_the_replay_ends_the_start_before_its_successor_epoch() -> TestResult {
a_stop_while_parked(|gated| gated.read_armed.store(true, Ordering::SeqCst))
}
#[test]
fn a_shutdown_after_the_successor_append_landed_ends_the_start_and_the_next_start_boots_above_it()
-> TestResult {
a_stop_while_parked(|gated| gated.write_armed.store(true, Ordering::SeqCst))
}