use crate::ServerError;
use crate::link_carrier::choice::{ChoiceLink as DialedLink, Interrupt as TcpStream};
use crate::server::connection::services::LiminalConnectionServices;
use subscriptions::{ExportSubscriptions, PendingDelivery};
mod counts;
mod crossing;
use crossing::cross;
mod fault;
mod life;
#[cfg(feature = "test-support")]
mod prepared;
mod start;
pub use fault::BridgeRegistry;
use fault::{Observation, Wake};
#[cfg(test)]
mod cancellation_tests;
#[cfg(test)]
mod fault_tests;
mod stored;
pub(crate) mod subscriptions;
use life::topology;
use std::collections::BTreeMap;
use std::net::Shutdown;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
const LINK_CROSSING_REFUSED_CODE: u16 = 0x0203;
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct BridgeCounters {
#[serde(flatten)]
pub lifecycle: crate::link::LinkLifeCounters,
pub crossed: u64,
pub dial_refused: u64,
pub refused: u64,
pub refused_subjects: BTreeMap<String, u64>,
pub too_large: u64,
pub resumed_from: BTreeMap<String, u64>,
pub last_refusal: Option<String>,
pub error: Option<String>,
}
fn reached(prefix: &str, subject: &str) -> bool {
subject == prefix
|| subject
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with('.'))
}
fn consumer_id(link: &str, prefix: &str) -> String {
let mut hash = blake3::Hasher::new();
hash.update(link.as_bytes());
hash.update(&[0]);
hash.update(prefix.as_bytes());
format!("link-bridge-{}", hash.finalize().to_hex())
}
fn failure(message: impl Into<String>) -> ServerError {
ServerError::ListenerAccept {
message: message.into(),
}
}
#[derive(Debug)]
pub struct LinkBridge {
link_id: String,
observation: Arc<Observation>,
wake: Arc<Wake>,
interrupt: TcpStream,
worker: Option<JoinHandle<Result<(), ServerError>>>,
}
impl LinkBridge {
pub fn start(
link: impl Into<DialedLink>,
services: Arc<LiminalConnectionServices>,
) -> Result<Self, ServerError> {
let link = link.into();
let peer = hex::encode(link.transport.peer().ed25519_public());
Self::start_named(link, services, peer)
}
#[must_use]
pub fn counters(&self) -> BridgeCounters {
self.observation.snapshot()
}
pub fn install_observations(&self, registry: &BridgeRegistry) {
registry.install(self.link_id.clone(), &self.observation);
}
#[cfg(feature = "test-support")]
pub fn stop_before_next_acknowledge(&self) {
self.observation
.stop_before_ack
.store(true, Ordering::Release);
}
#[cfg(feature = "test-support")]
#[must_use]
pub fn observe_answers(&self, count: u64, bound: std::time::Duration) -> bool {
self.observation.observe_answers(count, bound)
}
pub fn finish(mut self) -> Result<(), ServerError> {
self.stop_worker()
}
fn stop_worker(&mut self) -> Result<(), ServerError> {
self.wake.stop();
let ended = self.observation.end_life(crate::link::LinkEnd::Shutdown);
let interruption = self.interrupt.shutdown(Shutdown::Both);
if let Some(worker) = self.worker.take() {
worker
.join()
.map_err(|_| failure("link bridge worker panicked"))??;
}
let error = self.observation.snapshot().error;
if let Some(error) = error {
return Err(failure(error));
}
ended?;
match interruption {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {
tracing::info!(link = %self.link_id,
"bridge carrier already closed at stop; subscription position retained");
Ok(())
}
Err(error) => Err(failure(format!("link bridge interruption: {error}"))),
}
}
}
impl Drop for LinkBridge {
fn drop(&mut self) {
if self.worker.is_some() {
if let Err(error) = self.stop_worker() {
tracing::error!(link = %self.link_id, %error, "link bridge shutdown failed");
}
}
}
}
fn run_worker(
link: &mut DialedLink,
feeds: &ExportSubscriptions,
wake: &Wake,
observation: &Observation,
services: &LiminalConnectionServices,
peer: &str,
) -> Result<(), ServerError> {
topology(services, &link.link_id, peer, observation, None);
let result = forward(link, feeds, wake, observation, services, peer);
if let Err(error) = &result {
wake.refuse(error);
}
observation.worker_ended(&result)?;
topology(
services,
&link.link_id,
peer,
observation,
Some(
result
.as_ref()
.err()
.map_or_else(|| "stopped".to_owned(), ToString::to_string),
),
);
observation.notify_changes();
result
}
enum Crossing {
Acknowledged,
Refused(Option<String>),
TooLarge,
Stopped,
}
fn forward(
link: &mut DialedLink,
feeds: &ExportSubscriptions,
wake: &Wake,
observation: &Observation,
services: &LiminalConnectionServices,
peer: &str,
) -> Result<(), ServerError> {
loop {
wake.check()?;
if wake.stopping() || observation.ended()? {
wake.check()?;
return Ok(());
}
if let Err(error) = link.carrier.refuse_unsolicited() {
if wake.stopping() || observation.ended()? {
wake.check()?;
return Ok(());
}
return Err(failure(error.to_string()));
}
let Some(mut source) = feeds.next_ready()? else {
if !wake.wait() {
wake.check()?;
return Ok(());
}
continue;
};
#[cfg(feature = "test-support")]
let answer = match prepared::take(observation, &mut source)? {
Some(sealed) => crossing::exchange(link, &source.subject, &sealed, wake)?,
None => cross(link, &mut source, wake)?,
};
#[cfg(not(feature = "test-support"))]
let answer = cross(link, &mut source, wake)?;
if matches!(answer, Crossing::Stopped) {
return Ok(());
}
let mut counters = lock(&observation.counters, "counters")?;
#[cfg(feature = "test-support")]
let stop_before_ack = matches!(answer, Crossing::Acknowledged)
&& observation.stop_before_ack.swap(false, Ordering::AcqRel);
#[cfg(not(feature = "test-support"))]
let stop_before_ack = false;
match answer {
Crossing::Acknowledged => {
counters.crossed = counters
.crossed
.checked_add(1)
.ok_or_else(|| failure("bridge crossing counter overflow"))?;
}
Crossing::Refused(reason) => {
counters.refused = counters
.refused
.checked_add(1)
.ok_or_else(|| failure("bridge refusal counter overflow"))?;
let refused = counters
.refused_subjects
.entry(source.subject.clone())
.or_default();
*refused = refused
.checked_add(1)
.ok_or_else(|| failure("bridge subject refusal counter overflow"))?;
if *refused == 1 {
tracing::warn!(link = %link.link_id, subject = %source.subject, reason = ?reason,
"link crossing refused; subscription advances without retry");
}
counters.last_refusal = reason;
}
Crossing::TooLarge => {
counters.too_large = counters
.too_large
.checked_add(1)
.ok_or_else(|| failure("bridge too_large counter overflow"))?;
counters.last_refusal = Some("too_large".to_owned());
tracing::warn!(link = %link.link_id, subject = %source.subject, reason = "too_large",
"link crossing refused locally");
}
Crossing::Stopped => return Ok(()),
}
drop(counters);
if !stop_before_ack {
source.acknowledge()?;
}
topology(services, &link.link_id, peer, observation, None);
let guard = lock(&observation.counters, "settlement counters")?;
observation
.settled
.fetch_update(Ordering::Release, Ordering::Relaxed, |count| {
count.checked_add(1)
})
.map_err(|_| failure("bridge settled answer counter overflow"))?;
drop(guard);
observation.notify_changes();
if stop_before_ack {
return Ok(());
}
}
}
fn lock<'a, Value>(
mutex: &'a Mutex<Value>,
state: &str,
) -> Result<std::sync::MutexGuard<'a, Value>, ServerError> {
mutex
.lock()
.map_err(|_| failure(format!("link_bridge_poisoned: {state}")))
}