use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Instant;
use tokio::sync::Notify;
use liminal::channel::PublishRecord;
use liminal::protocol::MessageEnvelope;
use crate::ServerError;
use crate::console::ConsoleHub;
use crate::server::connection::services::{PublishOutcome, Reach};
use crate::server::self_streams::cost::CostWake;
use crate::server::self_streams::events::{PublishOrigin, ServerEvent};
use crate::server::self_streams::sink::{OriginPublication, SelfStreams};
#[derive(Clone, Debug, Default)]
pub(in crate::server) struct ServerSignals {
state: Arc<SignalState>,
}
#[derive(Debug, Default)]
struct SignalState {
wake: Arc<CostWake>,
streams: OnceLock<Arc<SelfStreams>>,
console: OnceLock<Arc<ConsoleHub>>,
console_wake: Arc<Notify>,
console_enabled: AtomicBool,
connections_opened: AtomicU64,
connections_closed: AtomicU64,
#[cfg(test)]
events_written: (std::sync::Mutex<u64>, std::sync::Condvar),
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SignalCounts {
pub console: crate::console::ConsoleCountsSnapshot,
pub tap_observations: u64,
pub tap_server_observations: u64,
pub connections_opened: u64,
pub connections_closed: u64,
}
impl ServerSignals {
pub(in crate::server) fn wake(&self) -> Arc<CostWake> {
Arc::clone(&self.state.wake)
}
pub(in crate::server) fn install(&self, streams: Arc<SelfStreams>) -> Result<(), ServerError> {
streams.start()?;
self.state
.streams
.set(streams)
.map_err(|_| SelfStreams::failure("self-stream publisher was already installed"))
}
pub(in crate::server) fn install_console(
&self,
console: Arc<ConsoleHub>,
) -> Result<(), ServerError> {
self.state
.console
.set(console)
.map_err(|_| SelfStreams::failure("console hub was already installed"))
}
pub(in crate::server) fn enable_console(&self) -> Result<(), ServerError> {
let console = self
.state
.console
.get()
.ok_or_else(|| SelfStreams::failure("console hub is not installed"))?;
let wake = Arc::clone(&self.state.console_wake);
console
.set_notifier(Arc::new(move || wake.notify_one()))
.map_err(|error| SelfStreams::failure(&error.to_string()))?;
self.state.console_enabled.store(true, Ordering::Release);
console
.wake_if_due()
.map_err(|error| SelfStreams::failure(&error.to_string()))
}
pub(in crate::server) fn console_enabled(&self) -> bool {
self.state.console_enabled.load(Ordering::Acquire)
}
pub(in crate::server) fn console_wake(&self) -> Option<Arc<Notify>> {
self.console_enabled()
.then(|| Arc::clone(&self.state.console_wake))
}
pub(in crate::server) fn topology(&self, change: crate::console::TopologyChange) {
if let Some(console) = self.state.console.get()
&& let Err(error) = console.emit(change)
{
tracing::warn!(%error, "TOPOLOGY_EMIT_FAILED");
}
}
pub(in crate::server) fn published(
&self,
channel: &str,
origin: PublishOrigin<'_>,
envelope: &MessageEnvelope,
result: &Result<OriginPublication, ServerError>,
recorded: Option<(&PublishRecord, Instant)>,
) {
if !channel.starts_with("console.") && result.is_ok() {
self.state.wake.arm(&origin);
}
if let Some(console) = self.state.console.get() {
tap(console, channel, origin, envelope, result, recorded);
}
}
pub(in crate::server) fn lend_recipients(&self) -> Option<PublishRecord> {
let console = self.state.console.get()?;
console.tap().lend_recipients().unwrap_or_else(|error| {
console.observe_failed();
tracing::warn!(%error, "TAP_RECIPIENTS_FAILED");
None
})
}
pub(in crate::server) fn give_back(&self, buffer: PublishRecord) {
if let Some(console) = self.state.console.get()
&& let Err(error) = console.tap().give_back(buffer)
{
console.observe_failed();
tracing::warn!(%error, "TAP_RECIPIENTS_FAILED");
}
}
pub(in crate::server) fn receiver(&self, subscriber: u64, connection: u64) {
if let Some(console) = self.state.console.get()
&& let Err(error) = console.tap().name_receiver(subscriber, connection)
{
console.observe_failed();
tracing::warn!(%error, "TAP_RECEIVER_FAILED");
}
}
pub(in crate::server) fn event(&self, event: ServerEvent<'_>) {
if let Some(streams) = self.state.streams.get() {
streams.event(event);
}
#[cfg(test)]
{
let (written, changed) = &self.state.events_written;
if let Ok(mut written) = written.lock() {
*written += 1;
changed.notify_all();
}
}
}
#[cfg(test)]
pub(in crate::server) fn await_events(&self, count: u64, timeout: std::time::Duration) -> bool {
let (written, changed) = &self.state.events_written;
written
.lock()
.ok()
.and_then(|guard| {
changed
.wait_timeout_while(guard, timeout, |written| *written < count)
.ok()
})
.is_some_and(|(written, _)| *written >= count)
}
#[cfg(test)]
pub(in crate::server) fn streams(&self) -> Option<&Arc<SelfStreams>> {
self.state.streams.get()
}
#[cfg(test)]
pub(in crate::server) fn counts(&self) -> SignalCounts {
let console = self.state.console.get();
SignalCounts {
console: console.map(|console| console.counts()).unwrap_or_default(),
tap_observations: console.map_or(0, |console| console.tap().observations()),
tap_server_observations: console
.map_or(0, |console| console.tap().server_observations()),
connections_opened: self.state.connections_opened.load(Ordering::Relaxed),
connections_closed: self.state.connections_closed.load(Ordering::Relaxed),
}
}
pub(in crate::server) fn with_flush_barrier<T>(
&self,
flush: impl FnOnce() -> Result<T, ServerError>,
) -> Result<T, ServerError> {
match self.state.streams.get() {
Some(streams) => streams.with_flush_barrier(flush),
None => flush(),
}
}
pub(in crate::server) fn connection(&self, connection: u64, opened: bool) {
let counter = if opened {
&self.state.connections_opened
} else {
&self.state.connections_closed
};
counter.fetch_add(1, Ordering::Relaxed);
self.event(if opened {
ServerEvent::ConnectionOpened { connection }
} else {
ServerEvent::ConnectionClosed { connection }
});
}
pub(in crate::server) fn lifecycle_batch<'a>(
&self,
conversation: u64,
receipts: impl IntoIterator<Item = (u64, &'a str)>,
) -> Result<(), ServerError> {
self.state.streams.get().map_or(Ok(()), |streams| {
streams.lifecycle_batch(conversation, receipts)
})
}
}
fn tap(
console: &ConsoleHub,
channel: &str,
origin: PublishOrigin<'_>,
envelope: &MessageEnvelope,
result: &Result<OriginPublication, ServerError>,
recorded: Option<(&PublishRecord, Instant)>,
) {
if !crate::console::tappable(channel) {
return;
}
let result = result.as_ref().map(|published| &published.outcome);
let Some(outcome) = tap_outcome(result) else {
if matches!(
result,
Ok(PublishOutcome {
reach: Reach::Suppressed,
..
})
) {
console.suppressed();
}
return;
};
let fact = crate::tap::PublishFact {
subject: channel,
payload: &envelope.payload,
outcome,
message_id: result.ok().map(|published| published.message_id),
source: match origin {
PublishOrigin::Connection { id } => crate::tap::TapSource::Connection(id),
PublishOrigin::Server { .. } => crate::tap::TapSource::Server,
},
reply_to: envelope
.causal_context
.parent_id
.as_ref()
.map(|parent| parent.0.as_str()),
recorded: recorded.map(|(record, received_at)| crate::tap::Recorded {
recipients: record.recipients(),
received_at,
appended_at: record.appended_at(),
}),
};
if let Err(error) = console.tap().observe(&fact) {
for _ in 0..error.failures() {
console.observe_failed();
}
tracing::warn!(%error, channel, "TAP_OBSERVE_FAILED");
}
}
const fn tap_outcome(
result: Result<&PublishOutcome, &ServerError>,
) -> Option<crate::tap::TapOutcome> {
match result {
Ok(PublishOutcome { reach, .. }) => match reach {
Reach::Delivered => Some(crate::tap::TapOutcome::Delivered),
Reach::NoSubscriber => Some(crate::tap::TapOutcome::NoSubscriber),
Reach::NoneAccepted => Some(crate::tap::TapOutcome::NoneAccepted),
Reach::Suppressed => None,
},
Err(ServerError::PublishUnresolved { .. }) => Some(crate::tap::TapOutcome::Unresolved),
Err(
ServerError::ReservedNamespace { .. }
| ServerError::SelfStreamPublisherAlreadyFinished { .. }
| ServerError::ConfigLoad { .. }
| ServerError::ConfigValidation { .. }
| ServerError::ListenerBind { .. }
| ServerError::ListenerAccept { .. }
| ServerError::ParticipantIncarnation { .. }
| ServerError::ParticipantServiceFatal { .. }
| ServerError::ConnectionFateRecoveryRequired { .. }
| ServerError::ServerIncarnationExhausted
| ServerError::ParticipantStartupRestore { .. }
| ServerError::ConnectionIncarnationExhausted { .. }
| ServerError::ConnectionPidCollision { .. }
| ServerError::UnsupportedOperation { .. }
| ServerError::PushReplyDisconnected { .. }
| ServerError::PushReplyTimeout { .. }
| ServerError::PushReplyExpired { .. }
| ServerError::PushFrameExceedsOutboundCapacity { .. }
| ServerError::ClusterJoin { .. }
| ServerError::ClusterSync { .. }
| ServerError::ShutdownTimeout { .. }
| ServerError::ShutdownFlush { .. }
| ServerError::ShutdownUnsettledConnectionFates { .. }
| ServerError::HealthEndpoint { .. }
| ServerError::ConnectionLimitReached { .. }
| ServerError::ConnectionCapReached { .. }
| ServerError::ParticipantLogMigration { .. },
) => Some(crate::tap::TapOutcome::Refused),
}
}