use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use std::sync::{Arc, Mutex};
use liminal::durability::{DurableStore, bridge::block_on, open_ephemeral};
use liminal_protocol::wire::{ClientRequest, ConnectionIncarnation, ServerValue};
use tracing_subscriber::prelude::*;
use super::incarnation::{ConnectionIncarnationAuthority, IncarnationStartInputs};
use crate::ServerError;
use crate::health::AdmissionReadiness;
use crate::server::participant::incarnation_stream::{
ConnectionFateClass, IncarnationStream, encode_allocate_event_fixture,
encode_open_connection_fate_event_fixture, encode_startup_event_fixture,
};
use crate::server::participant::{
ConnectionFateWorkItem, ParticipantConnectionContext, ParticipantConnectionConversations,
ParticipantSemanticError, ParticipantSemanticHandler,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn internal(error: impl std::fmt::Display) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: error.to_string(),
}
}
#[derive(Debug)]
struct GatedRepair {
entered: Mutex<Sender<()>>,
release: Mutex<Receiver<bool>>,
}
impl ParticipantSemanticHandler for GatedRepair {
fn handle(
&self,
_context: ParticipantConnectionContext,
_conversations: &mut ParticipantConnectionConversations,
_request: ClientRequest,
) -> Result<ServerValue, ParticipantSemanticError> {
Err(ParticipantSemanticError::Unavailable)
}
fn handle_connection_fate(
&self,
_work_item: ConnectionFateWorkItem,
) -> Result<(), ParticipantSemanticError> {
Ok(())
}
fn repair_unclean_server_restart(
&self,
_current_server_incarnation: u64,
) -> Result<(), ParticipantSemanticError> {
self.entered
.lock()
.map_err(internal)?
.send(())
.map_err(internal)?;
let succeeds = self
.release
.lock()
.map_err(internal)?
.recv()
.map_err(internal)?;
if succeeds {
Ok(())
} else {
Err(internal("injected restart repair refusal"))
}
}
}
struct Held {
authority: Arc<ConnectionIncarnationAuthority>,
admission: AdmissionReadiness,
release: Sender<bool>,
starter: std::thread::JoinHandle<Result<(), ServerError>>,
admitted: Receiver<Result<ConnectionIncarnation, ServerError>>,
admitting: std::thread::JoinHandle<()>,
}
fn held() -> Result<Held, Box<dyn std::error::Error>> {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let (entered_send, entered) = channel();
let (release, released) = channel();
let admission = AdmissionReadiness::available();
let authority = Arc::new(ConnectionIncarnationAuthority::starting(
IncarnationStartInputs {
store,
maximum_references: 4,
maximum_conversations: 3,
handler: Arc::new(GatedRepair {
entered: Mutex::new(entered_send),
release: Mutex::new(released),
}),
},
admission.clone(),
)?);
assert!(!admission.is_available(), "ready is false before the start");
let starting = Arc::clone(&authority);
let starter = std::thread::spawn(move || starting.ensure_started());
entered.recv()?;
let (waiting_send, waiting) = channel();
authority.on_wait_for_test(waiting_send);
let (admitted_send, admitted) = channel();
let allocating = Arc::clone(&authority);
let admitting = std::thread::spawn(move || {
let _sent = admitted_send.send(allocating.allocate(&[]));
});
waiting.recv()?;
assert!(
matches!(admitted.try_recv(), Err(TryRecvError::Empty)),
"an admission during the start waits; it is never refused for starting"
);
assert!(!admission.is_available(), "ready is false while starting");
Ok(Held {
authority,
admission,
release,
starter,
admitted,
admitting,
})
}
#[test]
fn an_admission_during_the_start_waits_and_is_admitted_when_it_finishes() -> TestResult {
let held = held()?;
held.release.send(true)?;
assert_eq!(held.admitted.recv()??, ConnectionIncarnation::new(1, 0));
held.starter.join().map_err(|_| "the start panicked")??;
held.admitting
.join()
.map_err(|_| "the admission panicked")?;
assert!(held.admission.is_available());
assert!(held.authority.start_failure().is_none());
Ok(())
}
fn repair_refused(result: &Result<impl std::fmt::Debug, ServerError>) -> bool {
matches!(
result,
Err(ServerError::ParticipantIncarnation {
phase: "server restart repair",
..
})
)
}
#[test]
fn a_failed_start_refuses_the_waiting_admission_with_its_error() -> TestResult {
let held = held()?;
held.release.send(false)?;
let refused = held.admitted.recv()?;
assert!(repair_refused(&refused), "{refused:?}");
let started = held.starter.join().map_err(|_| "the start panicked")?;
assert!(repair_refused(&started), "{started:?}");
held.admitting
.join()
.map_err(|_| "the admission panicked")?;
let later = held.authority.allocate(&[]);
assert!(repair_refused(&later), "a later admission: {later:?}");
let failure = held.authority.start_failure().ok_or("no start failure")?;
assert!(repair_refused(&Err::<(), _>(failure)));
assert!(!held.admission.is_available());
Ok(())
}
#[derive(Debug)]
struct Absorbing;
impl ParticipantSemanticHandler for Absorbing {
fn handle(
&self,
_context: ParticipantConnectionContext,
_conversations: &mut ParticipantConnectionConversations,
_request: ClientRequest,
) -> Result<ServerValue, ParticipantSemanticError> {
Err(ParticipantSemanticError::Unavailable)
}
fn handle_connection_fate(
&self,
_work_item: ConnectionFateWorkItem,
) -> Result<(), ParticipantSemanticError> {
Ok(())
}
}
#[derive(Default)]
struct Names(Mutex<Vec<String>>);
struct Capture(Arc<Names>);
#[derive(Default)]
struct Name(Option<String>);
impl tracing::field::Visit for Name {
fn record_debug(&mut self, _: &tracing::field::Field, _: &dyn std::fmt::Debug) {}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "name" {
self.0 = Some(value.to_owned());
}
}
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Capture {
fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
if event.metadata().target() != "liminal::boot" {
return;
}
let mut name = Name::default();
event.record(&mut name);
if let (Some(name), Ok(mut names)) = (name.0, self.0.0.lock()) {
names.push(name);
}
}
}
#[test]
fn a_start_after_an_unclean_stop_names_its_recovery() -> TestResult {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let payloads = [
encode_startup_event_fixture()?,
encode_allocate_event_fixture(4, &[])?,
encode_open_connection_fate_event_fixture(
ConnectionIncarnation::new(1, 0),
ConnectionFateClass::ConnectionLost,
3,
&[5, 8],
)?,
];
for (sequence, payload) in payloads.into_iter().enumerate() {
let sequence = u64::try_from(sequence)?;
block_on(store.append(IncarnationStream::stream_key(), payload, sequence))??;
}
block_on(store.flush())??;
let names = Arc::new(Names::default());
let subscriber = tracing_subscriber::registry().with(Capture(Arc::clone(&names)));
let (built, started) = tracing::subscriber::with_default(subscriber, || {
let authority = ConnectionIncarnationAuthority::starting(
IncarnationStartInputs {
store,
maximum_references: 4,
maximum_conversations: 3,
handler: Arc::new(Absorbing),
},
AdmissionReadiness::available(),
)?;
let built = names.0.lock().map(|logged| logged.clone()).ok();
authority.ensure_started()?;
Ok::<_, ServerError>((built, authority.allocate(&[])?))
})?;
assert_eq!(
built.ok_or("captured names poisoned")?,
Vec::<String>::new()
);
assert_eq!(started, ConnectionIncarnation::new(2, 0));
let logged = names
.0
.lock()
.map_err(|_| "captured names poisoned")?
.clone();
for name in [
"incarnation_startup",
"incarnation_recovery",
"restart_repair",
] {
assert_eq!(
logged.iter().filter(|step| *step == name).count(),
1,
"{name} once: {logged:?}"
);
}
Ok(())
}