use bytes::Bytes;
use futures::future::BoxFuture;
use crate::id::{InstanceId, PeerInfo, TransportKey, WorkerAddress};
use crate::observability::TransportObservability;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::{Context, Poll};
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)
}
pub fn begin_drain(&self) {
self.inner.draining.store(true, Ordering::Release);
}
pub fn acquire(&self) -> InFlightGuard {
self.inner.in_flight.fetch_add(1, Ordering::AcqRel);
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 {
if self.inner.in_flight.load(Ordering::Acquire) == 0 {
return;
}
self.inner.drain_complete.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 struct SendBackpressure {
fut: BoxFuture<'static, ()>,
}
impl SendBackpressure {
pub fn new(fut: BoxFuture<'static, ()>) -> Self {
Self { fut }
}
}
impl Future for SendBackpressure {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.fut.as_mut().poll(cx)
}
}
impl std::fmt::Debug for SendBackpressure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SendBackpressure").finish_non_exhaustive()
}
}
#[inline]
pub fn try_send_or_backpressure<T, FDisc, FClosed>(
tx: &flume::Sender<T>,
task: T,
on_disconnected: FDisc,
on_closed_during_bp: FClosed,
) -> Result<(), SendBackpressure>
where
T: Send + 'static,
FDisc: FnOnce(T),
FClosed: FnOnce(T) + Send + 'static,
{
match tx.try_send(task) {
Ok(()) => Ok(()),
Err(flume::TrySendError::Full(task)) => {
let tx = tx.clone();
Err(SendBackpressure::new(Box::pin(async move {
if let Err(flume::SendError(task)) = tx.send_async(task).await {
on_closed_during_bp(task);
}
})))
}
Err(flume::TrySendError::Disconnected(task)) => {
on_disconnected(task);
Ok(())
}
}
}
#[derive(Debug)]
pub enum SendOutcome {
Enqueued,
Backpressured(SendBackpressure),
}
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>,
) -> Result<(), SendBackpressure>;
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
}
}
#[derive(Clone)]
pub struct TransportAdapter {
pub message_stream: flume::Sender<(Bytes, Bytes)>,
pub response_stream: flume::Sender<(Bytes, Bytes)>,
pub event_stream: flume::Sender<(Bytes, Bytes)>,
pub shutdown_state: ShutdownState,
}
pub struct DataStreams {
pub message_stream: flume::Receiver<(Bytes, Bytes)>,
pub response_stream: flume::Receiver<(Bytes, Bytes)>,
pub event_stream: flume::Receiver<(Bytes, Bytes)>,
pub shutdown_state: ShutdownState,
}
type DataStreamTuple = (
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)
}
pub async fn recv_message_tracked(
&self,
) -> Result<(Bytes, Bytes, InFlightGuard), flume::RecvError> {
let (header, payload) = self.message_stream.recv_async().await?;
let guard = self.shutdown_state.acquire();
Ok((header, payload, guard))
}
}
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();
(
TransportAdapter {
message_stream: message_tx,
response_stream: response_tx,
event_stream: event_tx,
shutdown_state: shutdown_state.clone(),
},
DataStreams {
message_stream: message_rx,
response_stream: response_rx,
event_stream: event_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());
}
}