use std::error::Error;
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::time::{Duration, Instant};
use liminal::durability::{DurableStore, open_ephemeral};
use liminal_protocol::wire::{ClientRequest, ServerValue};
use super::ConnectionProcess;
use crate::ServerError;
use crate::config::types::LimitsConfig;
use crate::server::connection::ConnectionSupervisor;
use crate::server::connection::services::{ConnectionServices, LiminalConnectionServices};
use crate::server::connection::supervisor::ConnectionRuntime;
use crate::server::participant::{
ConnectionFateClass, ConnectionFateWorkItem, InstalledParticipantService,
ParticipantConnectionContext, ParticipantConnectionConversations, ParticipantSemanticError,
ParticipantSemanticHandler, ParticipantServiceFatal,
};
use crate::server::shutdown::ShutdownHandle;
const CONVERSATION: u64 = 0x_C0_17;
#[derive(Debug, Default)]
struct RecordingFates {
fates: Mutex<Vec<ConnectionFateWorkItem>>,
runtime: OnceLock<Weak<ConnectionRuntime>>,
departures_at_fate: Mutex<Vec<u64>>,
}
impl RecordingFates {
fn recorded(&self) -> Result<Vec<ConnectionFateWorkItem>, Box<dyn Error>> {
Ok(self
.fates
.lock()
.map_err(|_| "the recorded fates' lock was poisoned")?
.clone())
}
fn departures_at_fate(&self) -> Result<Vec<u64>, Box<dyn Error>> {
Ok(self
.departures_at_fate
.lock()
.map_err(|_| "the departure snapshots' lock was poisoned")?
.clone())
}
}
impl ParticipantSemanticHandler for RecordingFates {
fn service_fatal(&self) -> Result<Option<ParticipantServiceFatal>, ParticipantSemanticError> {
Ok(None)
}
fn latch_connection_fate_intent_incomplete(
&self,
open_sequence: u64,
conversation_id: u64,
) -> Result<ParticipantServiceFatal, ParticipantSemanticError> {
Ok(ParticipantServiceFatal::ConnectionFateIntentIncomplete {
open_sequence,
conversation_id,
})
}
fn handle_connection_fate(
&self,
work_item: ConnectionFateWorkItem,
) -> Result<(), ParticipantSemanticError> {
if let Some(runtime) = self.runtime.get().and_then(Weak::upgrade) {
let departures =
runtime
.departures()
.map_err(|error| ParticipantSemanticError::Internal {
message: error.to_string(),
})?;
self.departures_at_fate
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "the departure snapshots' lock was poisoned".to_owned(),
})?
.push(departures);
}
self.fates
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "the recorded fates' lock was poisoned".to_owned(),
})?
.push(work_item);
Ok(())
}
fn publication_conversation_limit(&self) -> u64 {
1
}
fn handle(
&self,
_context: ParticipantConnectionContext,
_conversations: &mut ParticipantConnectionConversations,
_request: ClientRequest,
) -> Result<ServerValue, ParticipantSemanticError> {
Err(ParticipantSemanticError::Unavailable)
}
}
fn tcp_pair() -> Result<(TcpStream, TcpStream), Box<dyn Error>> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let client = TcpStream::connect(listener.local_addr()?)?;
let (server, _) = listener.accept()?;
Ok((client, server))
}
struct Fixture {
supervisor: ConnectionSupervisor,
fates: Arc<RecordingFates>,
pid: u64,
_client: TcpStream,
process: ConnectionProcess,
}
impl Fixture {
fn new() -> Result<Self, Box<dyn Error>> {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let fates = Arc::new(RecordingFates::default());
let service = InstalledParticipantService::new(
Arc::clone(&fates) as Arc<dyn ParticipantSemanticHandler>,
store,
u64::MAX,
)
.map_err(|error| format!("participant service configuration failed: {error:?}"))?;
let services: Arc<dyn ConnectionServices> =
Arc::new(LiminalConnectionServices::empty()?.with_participant_service(service));
let supervisor = ConnectionSupervisor::with_fatal_shutdown(
services,
None,
None,
LimitsConfig::default(),
ShutdownHandle::new(),
)?;
let (client, server) = tcp_pair()?;
let connection = supervisor.spawn_connection(server)?;
let pid = connection.pid();
let incarnation = connection
.connection_incarnation()
.ok_or("participant connection lacks a durable incarnation")?;
let holder = Arc::new(Mutex::new(None));
let mut process = ConnectionProcess::from_holder(
supervisor.runtime_for_test(),
None,
&holder,
Some(incarnation),
);
process.state.participant_conversations.track(CONVERSATION);
Ok(Self {
supervisor,
fates,
pid,
_client: client,
process,
})
}
fn the_fate(&self) -> Result<ConnectionFateWorkItem, Box<dyn Error>> {
let recorded = self.fates.recorded()?;
let [fate] = recorded.as_slice() else {
return Err(format!("expected one completed fate, recorded {recorded:?}").into());
};
Ok(fate.clone())
}
}
impl Drop for Fixture {
fn drop(&mut self) {
self.supervisor.shutdown();
}
}
#[test]
fn a_readiness_probe_failure_completes_the_connection_lost_fate() -> Result<(), Box<dyn Error>> {
let mut fixture = Fixture::new()?;
let _outcome = fixture.process.fail_slice(
u64::MAX,
&ServerError::ListenerAccept {
message: "connection readiness probe failed: injected reset".to_owned(),
},
);
let fate = fixture.the_fate()?;
assert_eq!(fate.class, ConnectionFateClass::ConnectionLost);
assert_eq!(fate.tracked_conversations, vec![CONVERSATION]);
Ok(())
}
#[test]
fn a_publication_teardown_completes_the_connection_lost_fate() -> Result<(), Box<dyn Error>> {
let mut fixture = Fixture::new()?;
fixture.process.fail_next_pump = true;
let outcome = fixture.process.service_participant_pushes(u64::MAX);
assert!(outcome.is_some(), "the publication fault did not tear down");
let fate = fixture.the_fate()?;
assert_eq!(fate.class, ConnectionFateClass::ConnectionLost);
assert_eq!(fate.tracked_conversations, vec![CONVERSATION]);
Ok(())
}
#[test]
fn a_departure_is_raised_only_after_its_fate_completes() -> Result<(), Box<dyn Error>> {
let mut fixture = Fixture::new()?;
fixture
.fates
.runtime
.set(Arc::downgrade(&fixture.supervisor.runtime_for_test()))
.map_err(|_| "the fate recorder already observes a runtime")?;
let before = fixture.supervisor.departures()?;
let deadline = Instant::now() + Duration::from_secs(30);
let waiter = {
let supervisor = fixture.supervisor.clone();
let fates = Arc::clone(&fixture.fates);
std::thread::spawn(move || {
let departed = supervisor
.await_departures(before, 1, deadline)
.map_err(|error| error.to_string())?;
fates
.recorded()
.map(|recorded| (departed, recorded.len()))
.map_err(|error| error.to_string())
})
};
let pid = fixture.pid;
let _outcome = fixture.process.fail_slice(
pid,
&ServerError::ListenerAccept {
message: "connection readiness probe failed: injected reset".to_owned(),
},
);
assert_eq!(
fixture.fates.departures_at_fate()?,
vec![before],
"the connection had already departed when its fate ran"
);
let (departed, fates_at_wake) = waiter
.join()
.map_err(|_| "the departure waiter panicked")??;
assert!(departed, "no departure was raised before the deadline");
assert_eq!(
fates_at_wake, 1,
"the waiter woke before the fate was recorded"
);
assert_eq!(fixture.supervisor.departures()?, before.wrapping_add(1));
Ok(())
}