use bytes::Bytes;
use futures::future::BoxFuture;
use crate::admission::SendOutcome;
use crate::id::{InstanceId, PeerInfo, TransportKey, WorkerAddress};
use crate::observability::TransportObservability;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::{sync::Arc, time::Duration};
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
#[derive(thiserror::Error, Debug)]
pub enum TransportError {
#[error("No endpoint found for transport")]
NoEndpoint,
#[error("Invalid endpoint format")]
InvalidEndpoint,
#[error("Peer not registered: {0}")]
PeerNotRegistered(InstanceId),
#[error("Transport not started")]
NotStarted,
#[error("No responders for peer")]
NoResponders,
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum HealthCheckError {
#[error("Peer not registered with transport")]
PeerNotRegistered,
#[error("Transport not started")]
TransportNotStarted,
#[error("Connection never established to peer")]
NeverConnected,
#[error("Connection failed or peer unreachable")]
ConnectionFailed,
#[error("Health check timed out")]
Timeout,
}
#[derive(Clone)]
pub struct ShutdownState {
inner: Arc<ShutdownStateInner>,
}
struct ShutdownStateInner {
draining: AtomicBool,
in_flight: AtomicUsize,
drain_complete: Notify,
teardown_token: CancellationToken,
}
impl ShutdownState {
pub fn new() -> Self {
Self {
inner: Arc::new(ShutdownStateInner {
draining: AtomicBool::new(false),
in_flight: AtomicUsize::new(0),
drain_complete: Notify::new(),
teardown_token: CancellationToken::new(),
}),
}
}
#[inline]
pub fn is_draining(&self) -> bool {
self.inner.draining.load(Ordering::Relaxed)
}
#[inline]
fn is_draining_for_admission(&self) -> bool {
self.inner.draining.load(Ordering::SeqCst)
}
pub fn begin_drain(&self) {
self.inner.draining.store(true, Ordering::SeqCst);
}
pub fn acquire(&self) -> InFlightGuard {
self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
InFlightGuard {
inner: self.inner.clone(),
}
}
pub fn in_flight_count(&self) -> usize {
self.inner.in_flight.load(Ordering::Acquire)
}
pub async fn wait_for_drain(&self) {
loop {
let notified = self.inner.drain_complete.notified();
if self.inner.in_flight.load(Ordering::SeqCst) == 0 {
return;
}
notified.await;
}
}
pub fn teardown_token(&self) -> &CancellationToken {
&self.inner.teardown_token
}
}
impl Default for ShutdownState {
fn default() -> Self {
Self::new()
}
}
pub struct InFlightGuard {
inner: Arc<ShutdownStateInner>,
}
impl InFlightGuard {
pub fn complete(self) {
}
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
let prev = self.inner.in_flight.fetch_sub(1, Ordering::AcqRel);
if prev == 1 {
self.inner.drain_complete.notify_waiters();
}
}
}
#[derive(Debug, Clone)]
pub enum ShutdownPolicy {
WaitForever,
Timeout(Duration),
}
pub trait Transport: Send + Sync {
fn key(&self) -> TransportKey;
fn address(&self) -> WorkerAddress;
fn register(&self, peer_info: PeerInfo) -> Result<(), TransportError>;
fn send_message(
&self,
instance_id: InstanceId,
header: Bytes,
payload: Bytes,
message_type: MessageType,
on_error: Arc<dyn TransportErrorHandler>,
) -> SendOutcome;
fn max_message_size(&self, _target: InstanceId) -> Option<usize> {
None
}
fn start(
&self,
instance_id: InstanceId,
channels: TransportAdapter,
rt: tokio::runtime::Handle,
) -> BoxFuture<'_, anyhow::Result<()>>;
fn shutdown(&self);
fn set_observability(&self, _observability: std::sync::Arc<dyn TransportObservability>) {}
fn begin_drain(&self) {}
fn check_health(
&self,
instance_id: InstanceId,
timeout: Duration,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), HealthCheckError>> + Send + '_>,
>;
}
pub trait TransportErrorHandler: Send + Sync {
fn on_error(&self, header: Bytes, payload: Bytes, error: String);
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageType {
#[allow(missing_docs)]
Message = 0,
#[allow(missing_docs)]
Response = 1,
#[allow(missing_docs)]
Ack = 2,
#[allow(missing_docs)]
Event = 3,
ShuttingDown = 4,
}
impl MessageType {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(MessageType::Message),
1 => Some(MessageType::Response),
2 => Some(MessageType::Ack),
3 => Some(MessageType::Event),
4 => Some(MessageType::ShuttingDown),
_ => None,
}
}
pub fn as_u8(self) -> u8 {
self as u8
}
}
#[non_exhaustive]
pub struct InboundMessage {
pub header: Bytes,
pub payload: Bytes,
pub guard: InFlightGuard,
}
impl InboundMessage {
pub fn new(header: Bytes, payload: Bytes, guard: InFlightGuard) -> Self {
Self {
header,
payload,
guard,
}
}
}
#[derive(Debug)]
#[must_use = "the rejecting variants hand back a frame the caller still owes its peer a reply for"]
pub enum AdmitOutcome {
Admitted,
Draining {
header: Bytes,
payload: Bytes,
},
Disconnected {
header: Bytes,
payload: Bytes,
},
}
#[derive(Clone)]
pub struct TransportAdapter {
message_stream: flume::Sender<InboundMessage>,
pub response_stream: flume::Sender<(Bytes, Bytes)>,
pub event_stream: flume::Sender<(Bytes, Bytes)>,
pub shutdown_stream: flume::Sender<(Bytes, Bytes)>,
pub shutdown_state: ShutdownState,
}
impl TransportAdapter {
pub fn admit_message(&self, header: Bytes, payload: Bytes) -> AdmitOutcome {
let guard = self.shutdown_state.acquire();
if self.shutdown_state.is_draining_for_admission() {
drop(guard);
return AdmitOutcome::Draining { header, payload };
}
match self
.message_stream
.send(InboundMessage::new(header, payload, guard))
{
Ok(()) => AdmitOutcome::Admitted,
Err(flume::SendError(InboundMessage {
header,
payload,
guard,
..
})) => {
drop(guard);
AdmitOutcome::Disconnected { header, payload }
}
}
}
}
pub struct DataStreams {
pub message_stream: flume::Receiver<InboundMessage>,
pub response_stream: flume::Receiver<(Bytes, Bytes)>,
pub event_stream: flume::Receiver<(Bytes, Bytes)>,
pub shutdown_stream: flume::Receiver<(Bytes, Bytes)>,
pub shutdown_state: ShutdownState,
}
type DataStreamTuple = (
flume::Receiver<InboundMessage>,
flume::Receiver<(Bytes, Bytes)>,
flume::Receiver<(Bytes, Bytes)>,
flume::Receiver<(Bytes, Bytes)>,
);
impl DataStreams {
pub fn into_parts(self) -> DataStreamTuple {
(
self.message_stream,
self.response_stream,
self.event_stream,
self.shutdown_stream,
)
}
}
pub fn make_channels() -> (TransportAdapter, DataStreams) {
let shutdown_state = ShutdownState::new();
let (message_tx, message_rx) = flume::unbounded();
let (response_tx, response_rx) = flume::unbounded();
let (event_tx, event_rx) = flume::unbounded();
let (shutdown_tx, shutdown_rx) = flume::unbounded();
(
TransportAdapter {
message_stream: message_tx,
response_stream: response_tx,
event_stream: event_tx,
shutdown_stream: shutdown_tx,
shutdown_state: shutdown_state.clone(),
},
DataStreams {
message_stream: message_rx,
response_stream: response_rx,
event_stream: event_rx,
shutdown_stream: shutdown_rx,
shutdown_state,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{sleep, timeout};
#[test]
fn test_shutdown_state_initial() {
let state = ShutdownState::new();
assert!(!state.is_draining());
assert_eq!(state.in_flight_count(), 0);
}
#[test]
fn test_begin_drain_flips_flag() {
let state = ShutdownState::new();
state.begin_drain();
assert!(state.is_draining());
}
#[test]
fn test_acquire_increments_inflight() {
let state = ShutdownState::new();
let _g1 = state.acquire();
assert_eq!(state.in_flight_count(), 1);
}
#[test]
fn test_guard_drop_decrements_inflight() {
let state = ShutdownState::new();
let g = state.acquire();
assert_eq!(state.in_flight_count(), 1);
drop(g);
assert_eq!(state.in_flight_count(), 0);
}
#[tokio::test]
async fn test_wait_for_drain_blocks_then_completes() {
let state = ShutdownState::new();
let guard = state.acquire();
let state_clone = state.clone();
let handle = tokio::spawn(async move {
state_clone.wait_for_drain().await;
});
sleep(Duration::from_millis(50)).await;
assert!(!handle.is_finished());
drop(guard);
timeout(Duration::from_millis(100), handle)
.await
.expect("should complete after guard drop")
.unwrap();
}
#[test]
fn test_message_type_roundtrip() {
for v in 0..=4 {
let mt = MessageType::from_u8(v).unwrap();
assert_eq!(mt.as_u8(), v);
}
assert_eq!(MessageType::from_u8(5), None);
}
#[test]
fn test_make_channels_includes_shutdown_state() {
let (adapter, streams) = make_channels();
assert!(!adapter.shutdown_state.is_draining());
adapter.shutdown_state.begin_drain();
assert!(streams.shutdown_state.is_draining());
}
#[tokio::test]
async fn queued_message_holds_drain() {
let (adapter, streams) = make_channels();
let outcome = adapter.admit_message(
Bytes::from_static(b"queued-header"),
Bytes::from_static(b"queued-payload"),
);
assert!(matches!(outcome, AdmitOutcome::Admitted));
assert_eq!(
adapter.shutdown_state.in_flight_count(),
1,
"a queued message must be counted work the moment it is admitted"
);
adapter.shutdown_state.begin_drain();
let waiter_state = adapter.shutdown_state.clone();
let waiter = tokio::spawn(async move { waiter_state.wait_for_drain().await });
sleep(Duration::from_millis(50)).await;
assert!(
!waiter.is_finished(),
"wait_for_drain completed while a message was still queued and undispatched"
);
let queued = timeout(
Duration::from_millis(500),
streams.message_stream.recv_async(),
)
.await
.expect("the admitted message must still be on the queue")
.expect("recv");
assert_eq!(&queued.header[..], b"queued-header");
assert_eq!(&queued.payload[..], b"queued-payload");
drop(queued);
timeout(Duration::from_millis(500), waiter)
.await
.expect("wait_for_drain must complete once the queued message is released")
.expect("waiter task panicked");
assert_eq!(streams.shutdown_state.in_flight_count(), 0);
}
#[tokio::test]
async fn admit_message_rejects_during_drain() {
let (adapter, streams) = make_channels();
adapter.shutdown_state.begin_drain();
match adapter.admit_message(
Bytes::from_static(b"reject-header"),
Bytes::from_static(b"reject-payload"),
) {
AdmitOutcome::Draining { header, payload } => {
assert_eq!(&header[..], b"reject-header");
assert_eq!(&payload[..], b"reject-payload");
}
AdmitOutcome::Admitted => panic!("a draining instance must not admit a Message"),
AdmitOutcome::Disconnected { .. } => panic!("the receiver is still alive"),
}
assert!(
streams.message_stream.is_empty(),
"a rejected message must not reach the queue"
);
assert_eq!(
adapter.shutdown_state.in_flight_count(),
0,
"the acquire-then-check probe guard must not outlive the rejection"
);
timeout(
Duration::from_millis(500),
adapter.shutdown_state.wait_for_drain(),
)
.await
.expect("wait_for_drain must complete after a rejected admission");
}
#[test]
fn dropped_channel_releases_queued_guards() {
let (adapter, streams) = make_channels();
let state = adapter.shutdown_state.clone();
for i in 0..8u8 {
let outcome = adapter.admit_message(
Bytes::from(vec![b'h', i]),
Bytes::from_static(b"queued-payload"),
);
assert!(matches!(outcome, AdmitOutcome::Admitted));
}
assert_eq!(state.in_flight_count(), 8);
drop(streams);
drop(adapter);
assert_eq!(
state.in_flight_count(),
0,
"discarding the inbound queue must release every guard it was holding"
);
}
#[tokio::test]
async fn admit_message_disconnected_returns_frames() {
let (adapter, streams) = make_channels();
drop(streams);
match adapter.admit_message(
Bytes::from_static(b"orphan-header"),
Bytes::from_static(b"orphan-payload"),
) {
AdmitOutcome::Disconnected { header, payload } => {
assert_eq!(&header[..], b"orphan-header");
assert_eq!(&payload[..], b"orphan-payload");
}
AdmitOutcome::Admitted => panic!("there is no receiver left to admit to"),
AdmitOutcome::Draining { .. } => panic!("the instance is not draining"),
}
assert_eq!(
adapter.shutdown_state.in_flight_count(),
0,
"an undeliverable frame must not strand the drain"
);
timeout(
Duration::from_millis(500),
adapter.shutdown_state.wait_for_drain(),
)
.await
.expect("wait_for_drain must complete after an undeliverable admission");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn wait_for_drain_survives_guard_dropped_at_the_check() {
const ITERATIONS: usize = 32768;
const SPIN_SWEEP: usize = 1024;
const WAITER_LEAD: usize = 600;
const GRACE: Duration = Duration::from_secs(2);
fn burn(rounds: usize) {
let mut sink = 0usize;
for k in 0..rounds {
sink = std::hint::black_box(sink.wrapping_add(k));
}
}
for iteration in 0..ITERATIONS {
let state = ShutdownState::new();
let guard = state.acquire();
let armed = Arc::new(AtomicBool::new(false));
let spins = iteration % SPIN_SWEEP;
let dropper_armed = armed.clone();
let dropper = std::thread::spawn(move || {
while !dropper_armed.load(Ordering::Acquire) {
std::hint::spin_loop();
}
burn(spins);
drop(guard);
});
let waiter_state = state.clone();
let mut waiter = tokio::spawn(async move {
armed.store(true, Ordering::Release);
burn(WAITER_LEAD);
waiter_state.wait_for_drain().await;
});
let finished = timeout(Duration::from_millis(200), &mut waiter).await;
dropper.join().expect("dropper thread panicked");
let joined = match finished {
Ok(joined) => joined,
Err(_) => timeout(GRACE, &mut waiter).await.unwrap_or_else(|_| {
panic!(
"wait_for_drain lost the drain wakeup (iteration {iteration}, spins {spins})"
)
}),
};
joined.expect("waiter task panicked");
}
}
}