use agent_mesh_protocol::Fingerprint;
use rand::RngCore;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::task::{Context, Poll};
use tokio::sync::oneshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CorrelationId(pub [u8; 16]);
impl CorrelationId {
#[must_use]
pub fn new_random() -> Self {
let mut id = [0u8; 16];
rand::thread_rng().fill_bytes(&mut id);
Self(id)
}
#[must_use]
pub fn hex(&self) -> String {
hex::encode(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplyDelivery {
Delivered,
Unknown,
PeerMismatch {
expected_peer_fp: Fingerprint,
actual_peer_fp: Fingerprint,
},
ReceiverDropped,
}
struct PendingReply {
token: u64,
expected_peer_fp: Fingerprint,
sender: oneshot::Sender<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReplyPeerCheck {
Expected,
Unknown,
PeerMismatch {
expected_peer_fp: Fingerprint,
actual_peer_fp: Fingerprint,
},
}
#[derive(Default)]
pub struct ReplyWaiter {
waiters: Mutex<HashMap<CorrelationId, PendingReply>>,
next_token: AtomicU64,
}
impl ReplyWaiter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, id: CorrelationId, expected_peer_fp: Fingerprint) -> PendingReplyRx<'_> {
let (tx, rx) = oneshot::channel();
let token = self.next_token.fetch_add(1, Ordering::Relaxed);
self.waiters.lock().expect("reply waiter poisoned").insert(
id,
PendingReply {
token,
expected_peer_fp,
sender: tx,
},
);
PendingReplyRx {
waiters: self,
id,
token,
rx,
}
}
pub fn deliver(
&self,
id: CorrelationId,
actual_peer_fp: Fingerprint,
payload: Vec<u8>,
) -> ReplyDelivery {
let pending = {
let mut waiters = self.waiters.lock().expect("reply waiter poisoned");
let Some(pending) = waiters.get(&id) else {
return ReplyDelivery::Unknown;
};
if pending.expected_peer_fp != actual_peer_fp {
return ReplyDelivery::PeerMismatch {
expected_peer_fp: pending.expected_peer_fp,
actual_peer_fp,
};
}
waiters
.remove(&id)
.expect("reply waiter disappeared while lock was held")
};
if pending.sender.send(payload).is_ok() {
ReplyDelivery::Delivered
} else {
ReplyDelivery::ReceiverDropped
}
}
pub(crate) fn check_peer(
&self,
id: CorrelationId,
actual_peer_fp: Fingerprint,
) -> ReplyPeerCheck {
let waiters = self.waiters.lock().expect("reply waiter poisoned");
match waiters.get(&id) {
None => ReplyPeerCheck::Unknown,
Some(pending) if pending.expected_peer_fp == actual_peer_fp => ReplyPeerCheck::Expected,
Some(pending) => ReplyPeerCheck::PeerMismatch {
expected_peer_fp: pending.expected_peer_fp,
actual_peer_fp,
},
}
}
fn cancel(&self, id: &CorrelationId, token: u64) {
let mut waiters = self.waiters.lock().expect("reply waiter poisoned");
if waiters
.get(id)
.is_some_and(|pending| pending.token == token)
{
waiters.remove(id);
}
}
#[must_use]
pub fn pending(&self) -> usize {
self.waiters.lock().expect("reply waiter poisoned").len()
}
}
pub struct PendingReplyRx<'a> {
waiters: &'a ReplyWaiter,
id: CorrelationId,
token: u64,
rx: oneshot::Receiver<Vec<u8>>,
}
impl Future for PendingReplyRx<'_> {
type Output = Result<Vec<u8>, oneshot::error::RecvError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.rx).poll(cx)
}
}
impl Drop for PendingReplyRx<'_> {
fn drop(&mut self) {
self.waiters.cancel(&self.id, self.token);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn peer(seed: u8) -> Fingerprint {
Fingerprint([seed; 32])
}
#[test]
fn correlation_ids_are_distinct() {
let a = CorrelationId::new_random();
let b = CorrelationId::new_random();
assert_ne!(a, b, "two random ids must not collide");
assert_eq!(a.hex().len(), 32);
}
#[test]
fn correlation_id_hex_is_deterministic() {
let id = CorrelationId([0xab; 16]);
assert_eq!(id.hex(), "abababababababababababababababab");
}
#[tokio::test]
async fn register_returns_receiver_completed_on_deliver() {
let w = ReplyWaiter::new();
let id = CorrelationId::new_random();
let expected = peer(1);
let rx = w.register(id, expected);
assert_eq!(w.pending(), 1);
assert_eq!(
w.deliver(id, expected, b"reply".to_vec()),
ReplyDelivery::Delivered
);
assert_eq!(rx.await.unwrap(), b"reply");
assert_eq!(w.pending(), 0, "deliver removes the waiter");
}
#[test]
fn deliver_unknown_id_is_reported() {
let w = ReplyWaiter::new();
assert_eq!(
w.deliver(CorrelationId::new_random(), peer(1), b"orphan".to_vec()),
ReplyDelivery::Unknown
);
}
#[tokio::test]
async fn mismatched_peer_does_not_consume_waiter() {
let w = ReplyWaiter::new();
let id = CorrelationId::new_random();
let expected = peer(1);
let attacker = peer(2);
let rx = w.register(id, expected);
assert_eq!(
w.deliver(id, attacker, b"forged".to_vec()),
ReplyDelivery::PeerMismatch {
expected_peer_fp: expected,
actual_peer_fp: attacker,
}
);
assert_eq!(w.pending(), 1, "mismatch must leave the waiter pending");
assert_eq!(
w.deliver(id, expected, b"legitimate".to_vec()),
ReplyDelivery::Delivered
);
assert_eq!(rx.await.unwrap(), b"legitimate");
assert_eq!(w.pending(), 0);
}
#[tokio::test]
async fn deliver_twice_only_first_wins() {
let w = ReplyWaiter::new();
let id = CorrelationId::new_random();
let expected = peer(1);
let rx = w.register(id, expected);
assert_eq!(
w.deliver(id, expected, b"first".to_vec()),
ReplyDelivery::Delivered
);
assert_eq!(
w.deliver(id, expected, b"second".to_vec()),
ReplyDelivery::Unknown
);
assert_eq!(rx.await.unwrap(), b"first");
}
#[tokio::test]
async fn dropping_a_replaced_receiver_leaves_the_newer_waiter() {
let w = ReplyWaiter::new();
let id = CorrelationId::new_random();
let expected = peer(1);
let first = w.register(id, expected);
let second = w.register(id, expected);
drop(first);
assert_eq!(w.pending(), 1, "the newer registration survives");
assert_eq!(
w.deliver(id, expected, b"reply".to_vec()),
ReplyDelivery::Delivered
);
assert_eq!(second.await.unwrap(), b"reply");
}
#[test]
fn dropping_the_receiver_removes_the_waiter() {
let w = ReplyWaiter::new();
let id = CorrelationId::new_random();
let expected = peer(1);
drop(w.register(id, expected));
assert_eq!(w.pending(), 0);
assert_eq!(
w.deliver(id, expected, b"late".to_vec()),
ReplyDelivery::Unknown,
"a late reply finds no waiter"
);
}
}