use std::{
future::Future,
sync::{Arc, OnceLock},
};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use super::{Frame, FramedRecv, SinkReject};
use crate::zakura::transport::guard::{Admit, SessionGuard};
use crate::zakura::ZakuraPeerId;
#[derive(Clone, Debug)]
pub(crate) struct CloseCause {
reason: Arc<OnceLock<&'static str>>,
}
impl CloseCause {
pub(crate) fn new() -> Self {
Self {
reason: Arc::new(OnceLock::new()),
}
}
pub(crate) fn record(&self, reason: &'static str) {
let _ = self.reason.set(reason);
}
pub(crate) fn get_or(&self, default: &'static str) -> &'static str {
self.reason.get().copied().unwrap_or(default)
}
}
impl Default for CloseCause {
fn default() -> Self {
Self::new()
}
}
pub(crate) enum Flow<T> {
Continue(T),
Done,
Reject(SinkReject),
}
pub(crate) struct PipeCx<'a, S, Env> {
pub(crate) peer_id: &'a ZakuraPeerId,
pub(crate) local: &'a mut S,
pub(crate) env: &'a Env,
}
pub(crate) type PipeEntry<S, Env> = fn(&mut PipeCx<'_, S, Env>, Frame) -> Flow<()>;
#[derive(Copy, Clone, Debug)]
pub(crate) enum NodeKind {
Guard,
Decode,
Branch,
Mutate,
Emit,
}
#[derive(Copy, Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct Node {
pub(crate) id: &'static str,
pub(crate) kind: NodeKind,
}
#[derive(Copy, Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct Edge {
pub(crate) from: &'static str,
pub(crate) to: &'static str,
pub(crate) on: &'static str,
}
#[derive(Copy, Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct PipeShape {
pub(crate) service: &'static str,
pub(crate) nodes: &'static [Node],
pub(crate) edges: &'static [Edge],
}
impl PipeShape {
#[allow(dead_code)] pub(crate) fn validate(&self) -> Result<(), String> {
for edge in self.edges {
if !self.nodes.iter().any(|node| node.id == edge.from) {
return Err(format!(
"{}: edge from unknown node {:?}",
self.service, edge.from
));
}
if !self.nodes.iter().any(|node| node.id == edge.to) {
return Err(format!(
"{}: edge to unknown node {:?}",
self.service, edge.to
));
}
}
Ok(())
}
}
pub(crate) struct Pipe<S, Env> {
peer_id: ZakuraPeerId,
local: S,
env: Env,
guard: SessionGuard,
entry: PipeEntry<S, Env>,
#[allow(dead_code)] shape: &'static PipeShape,
}
impl<S, Env> Pipe<S, Env> {
pub(crate) fn new(
peer_id: ZakuraPeerId,
local: S,
env: Env,
guard: SessionGuard,
entry: PipeEntry<S, Env>,
shape: &'static PipeShape,
) -> Self {
Self {
peer_id,
local,
env,
guard,
entry,
shape,
}
}
pub(crate) fn run_one(&mut self, frame: Frame) -> Flow<()> {
match self.guard.admit(&frame) {
Admit::Throttle => Flow::Done,
Admit::Reject(reason) => Flow::Reject(SinkReject::protocol(reason)),
Admit::Pass => {
let mut cx = PipeCx {
peer_id: &self.peer_id,
local: &mut self.local,
env: &self.env,
};
(self.entry)(&mut cx, frame)
}
}
}
pub(crate) fn local_mut(&mut self) -> &mut S {
&mut self.local
}
}
#[allow(dead_code)]
pub(crate) struct PipeSink<S, Env> {
pipe: Pipe<S, Env>,
recv: FramedRecv,
cancel: CancellationToken,
}
#[allow(dead_code)]
impl<S, Env> PipeSink<S, Env>
where
S: Send + 'static,
Env: Send + Sync + 'static,
{
pub(crate) fn new(pipe: Pipe<S, Env>, recv: FramedRecv, cancel: CancellationToken) -> Self {
Self { pipe, recv, cancel }
}
pub(crate) async fn run(mut self) -> Result<(), SinkReject> {
loop {
let frame = tokio::select! {
() = self.cancel.cancelled() => return Ok(()),
frame = self.recv.recv() => frame,
};
let Some(frame) = frame else {
return Ok(());
};
match self.pipe.run_one(frame) {
Flow::Continue(()) | Flow::Done => continue,
Flow::Reject(reject) => return Err(reject),
}
}
}
}
struct PipeTeardown<F: FnOnce(), P: FnOnce()> {
peer_id: ZakuraPeerId,
cancel: CancellationToken,
on_teardown: Option<F>,
on_panic: Option<P>,
}
impl<F: FnOnce(), P: FnOnce()> Drop for PipeTeardown<F, P> {
fn drop(&mut self) {
if std::thread::panicking() {
metrics::counter!("zakura.pipe.panic").increment(1);
tracing::error!(
peer_id = ?self.peer_id,
"Zakura peer pipe panicked; disconnecting peer only"
);
if let Some(on_panic) = self.on_panic.take() {
on_panic();
}
}
self.cancel.cancel();
if let Some(on_teardown) = self.on_teardown.take() {
on_teardown();
}
}
}
#[cfg(panic = "abort")]
compile_error!(
"Zakura peer-pipe panic containment requires `panic = \"unwind\"` \
(security_requirements.md SR-1); this build sets `panic = \"abort\"`. \
Set panic = \"unwind\" in the workspace [profile.dev] and [profile.release]."
);
pub(crate) fn spawn_supervised_pipe(
peer_id: ZakuraPeerId,
cancel: CancellationToken,
on_teardown: impl FnOnce() + Send + 'static,
on_panic: impl FnOnce() + Send + 'static,
pipe: impl Future<Output = ()> + Send + 'static,
) -> JoinHandle<()> {
tokio::spawn(async move {
let _teardown = PipeTeardown {
peer_id,
cancel,
on_teardown: Some(on_teardown),
on_panic: Some(on_panic),
};
pipe.await;
})
}
struct PeerTaskTeardown<F: FnOnce(), P: FnOnce()> {
peer_id: ZakuraPeerId,
on_teardown: Option<F>,
on_panic: Option<P>,
}
impl<F: FnOnce(), P: FnOnce()> Drop for PeerTaskTeardown<F, P> {
fn drop(&mut self) {
if std::thread::panicking() {
metrics::counter!("zakura.pipe.panic").increment(1);
tracing::error!(
peer_id = ?self.peer_id,
"Zakura peer task panicked; disconnecting peer only"
);
if let Some(on_panic) = self.on_panic.take() {
on_panic();
}
}
if let Some(on_teardown) = self.on_teardown.take() {
on_teardown();
}
}
}
pub(crate) fn spawn_supervised_peer_task(
peer_id: ZakuraPeerId,
on_teardown: impl FnOnce() + Send + 'static,
on_panic: impl FnOnce() + Send + 'static,
task: impl Future<Output = ()> + Send + 'static,
) -> JoinHandle<()> {
tokio::spawn(async move {
let _teardown = PeerTaskTeardown {
peer_id,
on_teardown: Some(on_teardown),
on_panic: Some(on_panic),
};
task.await;
})
}
pub(crate) fn handle_pipe_exit(
service: &'static str,
connection_cancel: &CancellationToken,
close_cause: &CloseCause,
result: Result<(), SinkReject>,
) {
match result {
Ok(()) => {}
Err(SinkReject::Protocol(error)) => {
tracing::debug!(
?error,
service,
"Zakura stream rejected protocol-invalid frame"
);
close_cause.record("service_protocol_reject");
connection_cancel.cancel();
}
Err(SinkReject::Local(error)) => {
tracing::debug!(?error, service, "Zakura stream stopped on local error");
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use super::*;
use crate::zakura::transport::framed_channel;
use crate::zakura::transport::guard::ByteBudget;
const SHAPE: PipeShape = PipeShape {
service: "test",
nodes: &[
Node {
id: "guard",
kind: NodeKind::Guard,
},
Node {
id: "decode",
kind: NodeKind::Decode,
},
],
edges: &[Edge {
from: "guard",
to: "decode",
on: "Pass",
}],
};
const DANGLING_SHAPE: PipeShape = PipeShape {
service: "test-dangling",
nodes: &[Node {
id: "guard",
kind: NodeKind::Guard,
}],
edges: &[Edge {
from: "guard",
to: "missing",
on: "Pass",
}],
};
fn peer_id() -> ZakuraPeerId {
ZakuraPeerId::new(vec![1, 2, 3]).expect("3-byte id is within the node-id bound")
}
fn noop_entry(_cx: &mut PipeCx<'_, (), ()>, _frame: Frame) -> Flow<()> {
Flow::Continue(())
}
fn pass_guard() -> SessionGuard {
SessionGuard::new(&[1], 1_024, None)
}
fn frame(message_type: u16) -> Frame {
Frame {
message_type,
flags: 0,
payload: Vec::new(),
}
}
#[test]
fn pipe_shape_validate_accepts_consistent_graph() {
assert!(SHAPE.validate().is_ok());
}
#[test]
fn pipe_shape_validate_rejects_dangling_edge() {
let err = DANGLING_SHAPE
.validate()
.expect_err("dangling edge target must fail validation");
assert!(err.contains("missing"), "error names the bad node: {err}");
}
#[test]
fn run_one_passes_admitted_frame_to_entry() {
let mut pipe = Pipe::new(peer_id(), (), (), pass_guard(), noop_entry, &SHAPE);
assert!(matches!(pipe.run_one(frame(1)), Flow::Continue(())));
}
#[test]
fn run_one_throttles_when_budget_exhausted() {
let guard = SessionGuard::new(&[1], 1_024, Some(ByteBudget::new(4)));
let mut pipe = Pipe::new(peer_id(), (), (), guard, noop_entry, &SHAPE);
let mut framed = frame(1);
framed.payload = vec![0u8; 4];
assert!(matches!(pipe.run_one(framed.clone()), Flow::Continue(())));
assert!(matches!(pipe.run_one(framed), Flow::Done));
}
#[test]
fn run_one_rejects_disallowed_type() {
let mut pipe = Pipe::new(peer_id(), (), (), pass_guard(), noop_entry, &SHAPE);
assert!(matches!(pipe.run_one(frame(2)), Flow::Reject(_)));
}
#[test]
fn close_cause_keeps_first_recorded_reason() {
let cause = CloseCause::new();
cause.record("first");
cause.record("second");
assert_eq!(cause.get_or("fallback"), "first");
}
#[test]
fn close_cause_uses_default_without_recorded_reason() {
let cause = CloseCause::new();
assert_eq!(cause.get_or("fallback"), "fallback");
}
#[test]
fn protocol_pipe_exit_records_close_cause_and_cancels_connection() {
let cancel = CancellationToken::new();
let cause = CloseCause::new();
handle_pipe_exit(
"test",
&cancel,
&cause,
Err(SinkReject::protocol("bad frame")),
);
assert!(cancel.is_cancelled());
assert_eq!(cause.get_or("fallback"), "service_protocol_reject");
}
#[tokio::test]
async fn pipe_sink_run_returns_ok_on_stream_end() {
let (send, recv) = framed_channel(4);
let cancel = CancellationToken::new();
let pipe = Pipe::new(peer_id(), (), (), pass_guard(), noop_entry, &SHAPE);
let sink = PipeSink::new(pipe, recv, cancel);
send.send(frame(1)).await.expect("channel has capacity");
drop(send);
assert!(sink.run().await.is_ok());
}
#[tokio::test]
async fn pipe_sink_run_returns_err_on_reject() {
let (send, recv) = framed_channel(4);
let cancel = CancellationToken::new();
let pipe = Pipe::new(peer_id(), (), (), pass_guard(), noop_entry, &SHAPE);
let sink = PipeSink::new(pipe, recv, cancel);
send.send(frame(2)).await.expect("channel has capacity");
assert!(sink.run().await.is_err());
}
#[tokio::test]
async fn supervised_pipe_runs_teardown_on_panic() {
let cancel = CancellationToken::new();
let torn_down = Arc::new(AtomicBool::new(false));
let flag = torn_down.clone();
let panic_disconnected = Arc::new(AtomicBool::new(false));
let panic_flag = panic_disconnected.clone();
let handle = spawn_supervised_pipe(
peer_id(),
cancel.clone(),
move || flag.store(true, Ordering::SeqCst),
move || panic_flag.store(true, Ordering::SeqCst),
async {
panic!("peer pipe panics");
},
);
let join_error = handle
.await
.expect_err("a panicking pipe surfaces a join error");
assert!(
join_error.is_panic(),
"the pipe panic is reported as a panic, not a cancellation"
);
assert!(
torn_down.load(Ordering::SeqCst),
"teardown runs even when the pipe panics"
);
assert!(
panic_disconnected.load(Ordering::SeqCst),
"panic-only disconnect hook runs when the pipe panics"
);
assert!(cancel.is_cancelled(), "the peer connection is cancelled");
}
#[tokio::test]
async fn supervised_peer_task_runs_teardown_and_disconnect_on_panic() {
let torn_down = Arc::new(AtomicBool::new(false));
let teardown_flag = torn_down.clone();
let disconnected = Arc::new(AtomicBool::new(false));
let disconnect_flag = disconnected.clone();
let handle = spawn_supervised_peer_task(
peer_id(),
move || teardown_flag.store(true, Ordering::SeqCst),
move || disconnect_flag.store(true, Ordering::SeqCst),
async {
panic!("peer task panics after state registration");
},
);
let join_error = handle
.await
.expect_err("a panicking peer task surfaces a join error");
assert!(
join_error.is_panic(),
"the task panic is reported as a panic, not a cancellation"
);
assert!(
torn_down.load(Ordering::SeqCst),
"teardown runs even when the peer task panics"
);
assert!(
disconnected.load(Ordering::SeqCst),
"panic-only disconnect hook runs when the peer task panics"
);
}
#[tokio::test]
async fn supervised_peer_task_skips_disconnect_on_normal_exit() {
let torn_down = Arc::new(AtomicBool::new(false));
let teardown_flag = torn_down.clone();
let disconnected = Arc::new(AtomicBool::new(false));
let disconnect_flag = disconnected.clone();
let handle = spawn_supervised_peer_task(
peer_id(),
move || teardown_flag.store(true, Ordering::SeqCst),
move || disconnect_flag.store(true, Ordering::SeqCst),
async {},
);
handle
.await
.expect("a normal peer task exit does not panic");
assert!(
torn_down.load(Ordering::SeqCst),
"teardown runs on a normal exit"
);
assert!(
!disconnected.load(Ordering::SeqCst),
"the panic-only disconnect hook must not fire on a normal exit"
);
}
}