use std::collections::VecDeque;
use std::time::Duration;
use plaza::agent::AgentId;
use plaza::session::ConnectionId;
use plaza_wire::frame;
use tokio::time::Instant;
use tracing::{debug, trace};
use crate::codec::WireCodec;
use crate::gate::Verdict;
use crate::manager::{ConnectionManager, Frame, Probes, SessionClock};
pub const DOWN_SEED_FLIP: u64 = 0x5DEE_CE66_A5A5_1234;
pub fn earliest(a: Option<Instant>, b: Option<Instant>) -> Option<Instant> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub fn far_future() -> Instant {
Instant::now() + Duration::from_secs(86_400 * 365)
}
pub struct ProbeState {
outstanding: VecDeque<(u64, Instant)>,
seq: u64,
sent: u32,
schedule: Probes,
}
impl ProbeState {
pub fn forget_in_flight(&mut self) {
self.outstanding.clear();
}
}
impl Default for ProbeState {
fn default() -> Self {
Self::new(&Probes::default())
}
}
impl ProbeState {
pub fn new(schedule: &Probes) -> Self {
Self {
outstanding: VecDeque::new(),
seq: 0,
sent: 0,
schedule: schedule.clone(),
}
}
pub fn interval(&self) -> Option<Duration> {
if !self.schedule.enabled {
return None;
}
Some(if self.sent < self.schedule.fast_pings {
self.schedule.fast_interval
} else {
self.schedule.idle_interval
})
}
pub fn first_due(&self, now: Instant) -> Option<Instant> {
self.interval().map(|gap| now + gap)
}
}
pub enum Inbound {
Forward(Frame),
Reply(Frame),
Consumed,
Shed,
Eject,
}
pub fn make_probe<C: WireCodec>(codec: &C, probe: &mut ProbeState, now: Instant) -> Frame {
probe.seq = probe.seq.wrapping_add(1);
probe.sent = probe.sent.saturating_add(1);
if probe.outstanding.len() >= probe.schedule.slots {
probe.outstanding.pop_front();
}
probe.outstanding.push_back((probe.seq, now));
let mut buf = Vec::with_capacity(frame::PROBE_FRAME_HINT);
frame::begin(frame::Kind::Ping, &mut buf);
codec
.encode_into(&frame::Ping { origin: probe.seq }, &mut buf)
.expect("a u64 always encodes");
Frame::from(buf)
}
pub fn handle_inbound<ID: AgentId, C: WireCodec>(
frame_bytes: Frame,
codec: &C,
clock: Option<&SessionClock>,
probe: &mut ProbeState,
conn_id: ConnectionId,
manager: &ConnectionManager<ID>,
) -> Inbound {
let Some((tag, body)) = frame::split(&frame_bytes) else {
return gated(frame_bytes, conn_id, manager);
};
match frame::Kind::from_byte(tag) {
Some(frame::Kind::Ping) => match frame::answer_ping(codec, body, clock.map(|c| c())) {
Some(reply) => Inbound::Reply(Frame::from(reply)),
None => {
trace!(conn_id, "Discarding a malformed Ping.");
Inbound::Consumed
}
},
Some(frame::Kind::Pong) => {
if let Ok(pong) = codec.decode::<frame::Pong>(body) {
match probe.outstanding.iter().position(|(origin, _)| *origin == pong.origin) {
Some(index) => {
let (_, sent) = probe.outstanding.drain(..=index).next_back().expect("index is in range");
manager.record_link_rtt(conn_id, sent.elapsed());
}
None => trace!(conn_id, "Discarding a Pong that answers no open probe."),
}
}
Inbound::Consumed
}
_ => gated(frame_bytes, conn_id, manager),
}
}
fn gated<ID: AgentId>(frame_bytes: Frame, conn_id: ConnectionId, manager: &ConnectionManager<ID>) -> Inbound {
match manager.record_inbound_activity(conn_id, frame_bytes.len()) {
Verdict::Admit => Inbound::Forward(frame_bytes),
Verdict::Shed => Inbound::Shed,
Verdict::Close => Inbound::Eject,
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub enum Routed {
#[default]
Nothing,
Reply(Frame),
Eject,
}
pub async fn route_inbound<ID: AgentId, C: WireCodec>(
frame_bytes: Frame,
codec: &C,
clock: Option<&SessionClock>,
probe: &mut ProbeState,
conn_id: ConnectionId,
manager: &ConnectionManager<ID>,
agent: &plaza::agent::Agent<ID>,
) -> Routed {
match handle_inbound(frame_bytes, codec, clock, probe, conn_id, manager) {
Inbound::Forward(frame_bytes) => {
manager.forward_incoming(agent.clone(), frame_bytes).await;
Routed::Nothing
}
Inbound::Reply(reply) => Routed::Reply(reply),
Inbound::Consumed | Inbound::Shed => Routed::Nothing,
Inbound::Eject => {
debug!(conn_id, "Closing a connection that exceeded its inbound rate.");
Routed::Eject
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::JsonCodec;
use crate::manager::DEFAULT_PROBE_SLOTS;
use plaza::agent::Agent;
use std::sync::Arc;
fn manager() -> Arc<ConnectionManager<u32>> {
Arc::new(ConnectionManager::new("test", 8))
}
fn ping_frame(origin: u64) -> Frame {
let mut buf = Vec::new();
frame::begin(frame::Kind::Ping, &mut buf);
JsonCodec.encode_into(&frame::Ping { origin }, &mut buf).unwrap();
Frame::from(buf)
}
fn pong_frame(origin: u64) -> Frame {
let mut buf = Vec::new();
frame::begin(frame::Kind::Pong, &mut buf);
JsonCodec
.encode_into(&frame::Pong { origin, responder: None }, &mut buf)
.unwrap();
Frame::from(buf)
}
fn decode_pong(frame_bytes: &Frame) -> frame::Pong {
let (kind, body) = frame::split(frame_bytes).unwrap();
assert_eq!(frame::Kind::from_byte(kind), Some(frame::Kind::Pong));
JsonCodec.decode(body).unwrap()
}
#[tokio::test]
async fn a_pong_that_straddles_a_profile_change_is_discarded_not_recorded() {
let manager = manager();
let agent = Agent::new_human(7u32);
let (tx, _rx) = plaza::session::session_channel(4);
let conn_id = manager.register(agent, tx).await;
let mut probe = ProbeState::default();
let sent = make_probe(&JsonCodec, &mut probe, Instant::now());
let (_, body) = frame::split(&sent).unwrap();
let origin = JsonCodec.decode::<frame::Ping>(body).unwrap().origin;
probe.forget_in_flight();
let out = handle_inbound(pong_frame(origin), &JsonCodec, None, &mut probe, conn_id, &manager);
assert!(matches!(out, Inbound::Consumed));
assert!(
manager.agent_link_rtt(&7).is_none(),
"a straddling sample records nothing"
);
let sent = make_probe(&JsonCodec, &mut probe, Instant::now());
let (_, body) = frame::split(&sent).unwrap();
let origin = JsonCodec.decode::<frame::Ping>(body).unwrap().origin;
handle_inbound(pong_frame(origin), &JsonCodec, None, &mut probe, conn_id, &manager);
assert!(manager.agent_link_rtt(&7).is_some(), "and measuring resumes at once");
}
#[tokio::test]
async fn a_ping_is_answered_without_the_application_seeing_it() {
let manager = manager();
let mut probe = ProbeState::default();
let clock: SessionClock = Arc::new(|| 4242);
let out = handle_inbound(ping_frame(9), &JsonCodec, Some(&clock), &mut probe, 1, &manager);
let Inbound::Reply(reply) = out else {
panic!("a ping is answered")
};
let pong = decode_pong(&reply);
assert_eq!(pong.origin, 9);
assert_eq!(pong.responder, Some(4242));
}
#[tokio::test]
async fn without_a_clock_the_answer_still_carries_the_echo() {
let manager = manager();
let mut probe = ProbeState::default();
let out = handle_inbound(ping_frame(9), &JsonCodec, None, &mut probe, 1, &manager);
let Inbound::Reply(reply) = out else {
panic!("a ping is answered")
};
let pong = decode_pong(&reply);
assert_eq!(pong.origin, 9, "a round trip is measurable without a clock");
assert_eq!(pong.responder, None, "only the offset needs one");
}
#[tokio::test]
async fn a_matching_pong_records_a_sample() {
let manager = manager();
let (tx, _rx) = plaza::session::session_channel(4);
let conn_id = manager.register(Agent::new_human(7u32), tx).await;
let mut probe = ProbeState::default();
let sent = make_probe(&JsonCodec, &mut probe, Instant::now());
let (_, body) = frame::split(&sent).unwrap();
let origin = JsonCodec.decode::<frame::Ping>(body).unwrap().origin;
let mut reply = Vec::new();
frame::begin(frame::Kind::Pong, &mut reply);
JsonCodec
.encode_into(
&frame::Pong {
origin,
responder: None,
},
&mut reply,
)
.unwrap();
let out = handle_inbound(
Frame::from(reply),
&JsonCodec,
None,
&mut probe,
conn_id,
&manager,
);
assert!(matches!(out, Inbound::Consumed));
assert_eq!(manager.link_rtt_samples(conn_id), 1);
assert!(manager.agent_link_rtt(&7).is_some());
}
#[tokio::test]
async fn a_pong_answering_nothing_records_nothing() {
let manager = manager();
let (tx, _rx) = plaza::session::session_channel(4);
let conn_id = manager.register(Agent::new_human(7u32), tx).await;
let mut probe = ProbeState::default();
let mut stray = Vec::new();
frame::begin(frame::Kind::Pong, &mut stray);
JsonCodec
.encode_into(
&frame::Pong {
origin: 12345,
responder: None,
},
&mut stray,
)
.unwrap();
handle_inbound(
Frame::from(stray),
&JsonCodec,
None,
&mut probe,
conn_id,
&manager,
);
assert_eq!(manager.link_rtt_samples(conn_id), 0);
}
#[tokio::test]
async fn ops_and_hello_are_not_this_module_s_business() {
let manager = manager();
let mut probe = ProbeState::default();
for kind in [frame::Kind::Ops, frame::Kind::Hello] {
let mut buf = Vec::new();
frame::begin(kind, &mut buf);
buf.extend_from_slice(b"[]");
let out = handle_inbound(Frame::from(buf), &JsonCodec, None, &mut probe, 1, &manager);
assert!(matches!(out, Inbound::Forward(_)), "{kind:?} belongs to the bridge");
}
}
#[tokio::test]
async fn a_pong_slower_than_the_probe_interval_is_still_matched() {
let manager = manager();
let (tx, _rx) = plaza::session::session_channel(4);
let conn_id = manager.register(Agent::new_human(7u32), tx).await;
let mut probe = ProbeState::default();
let mut origins = Vec::new();
for _ in 0..4 {
let frame_bytes = make_probe(&JsonCodec, &mut probe, Instant::now());
let (_, body) = frame::split(&frame_bytes).unwrap();
origins.push(JsonCodec.decode::<frame::Ping>(body).unwrap().origin);
}
handle_inbound(pong_frame(origins[0]), &JsonCodec, None, &mut probe, conn_id, &manager);
assert_eq!(manager.link_rtt_samples(conn_id), 1, "a late reply still counts");
handle_inbound(pong_frame(origins[3]), &JsonCodec, None, &mut probe, conn_id, &manager);
assert_eq!(manager.link_rtt_samples(conn_id), 2);
handle_inbound(pong_frame(origins[1]), &JsonCodec, None, &mut probe, conn_id, &manager);
assert_eq!(manager.link_rtt_samples(conn_id), 2, "answered out of order and already dropped");
}
#[tokio::test]
async fn an_unanswered_link_does_not_grow_without_bound() {
let mut probe = ProbeState::default();
for _ in 0..(DEFAULT_PROBE_SLOTS * 4) {
make_probe(&JsonCodec, &mut probe, Instant::now());
}
assert_eq!(probe.outstanding.len(), DEFAULT_PROBE_SLOTS, "the oldest are abandoned");
}
#[test]
fn a_session_that_does_not_probe_never_schedules_one() {
let probe = ProbeState::new(&Probes::off());
assert_eq!(probe.interval(), None);
assert_eq!(probe.first_due(Instant::now()), None, "nothing for the timer arm to wait on");
}
#[tokio::test]
async fn probes_start_fast_and_settle() {
let mut probe = ProbeState::default();
let schedule = Probes::default();
for _ in 0..schedule.fast_pings {
assert_eq!(probe.interval(), Some(schedule.fast_interval));
make_probe(&JsonCodec, &mut probe, Instant::now());
}
assert_eq!(probe.interval(), Some(schedule.idle_interval));
}
}