use bytes::Bytes;
use flume::{Receiver, Sender};
use std::collections::HashMap;
use std::collections::hash_map::RandomState;
use std::sync::Arc;
type PeerMap<V> = HashMap<Bytes, V, RandomState>;
#[derive(Debug)]
pub enum RouterCmd {
SendMessage(Vec<Bytes>),
Close,
}
#[derive(Debug)]
pub enum PeerCmd {
SendBody(Arc<Vec<Bytes>>),
Close,
}
#[derive(Debug)]
pub enum HubEvent {
PeerUp {
routing_id: Bytes, tx: Sender<PeerCmd>,
},
PeerDown {
routing_id: Bytes,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouterBehavior {
Standard,
LoadBalancer,
}
pub struct RouterHub {
peers: PeerMap<Sender<PeerCmd>>,
lb_list: Vec<Bytes>,
lb_cursor: usize,
behavior: RouterBehavior,
hub_rx: Receiver<HubEvent>,
user_tx_rx: Receiver<RouterCmd>,
}
impl RouterHub {
#[must_use]
pub fn new(
hub_rx: Receiver<HubEvent>,
user_tx_rx: Receiver<RouterCmd>,
behavior: RouterBehavior,
) -> Self {
Self {
peers: PeerMap::default(),
lb_list: Vec::new(),
lb_cursor: 0,
behavior,
hub_rx,
user_tx_rx,
}
}
pub async fn run(mut self) {
use futures::FutureExt;
use futures::select;
loop {
select! {
msg = self.hub_rx.recv_async().fuse() => {
match msg {
Ok(ev) => self.handle_peer_event(ev),
Err(_) => break, }
}
msg = self.user_tx_rx.recv_async().fuse() => {
match msg {
Ok(cmd) => self.handle_user_cmd(cmd),
Err(_) => break, }
}
}
}
for tx in self.peers.values() {
let _ = tx.send(PeerCmd::Close);
}
}
fn handle_peer_event(&mut self, event: HubEvent) {
match event {
HubEvent::PeerUp { routing_id, tx } => {
if self.peers.contains_key(&routing_id)
&& let Some(pos) = self.lb_list.iter().position(|x| x == &routing_id)
{
self.lb_list.remove(pos);
if self.lb_cursor >= self.lb_list.len() {
self.lb_cursor = 0;
}
}
self.lb_list.push(routing_id.clone());
self.peers.insert(routing_id, tx);
}
HubEvent::PeerDown { routing_id } => {
self.peers.remove(&routing_id);
if let Some(pos) = self.lb_list.iter().position(|x| x == &routing_id) {
self.lb_list.remove(pos);
if self.lb_cursor >= self.lb_list.len() {
self.lb_cursor = 0;
}
}
}
}
}
fn handle_user_cmd(&mut self, cmd: RouterCmd) {
match cmd {
RouterCmd::SendMessage(parts) => self.route_outbound(parts),
RouterCmd::Close => {
for tx in self.peers.values() {
let _ = tx.send(PeerCmd::Close);
}
}
}
}
fn pick_rr_peer(&mut self) -> Option<Bytes> {
let mut attempts = 0usize;
let max_attempts = self.lb_list.len();
while !self.lb_list.is_empty() && attempts <= max_attempts {
if self.lb_cursor >= self.lb_list.len() {
self.lb_cursor = 0;
}
let id = self.lb_list[self.lb_cursor].clone();
self.lb_cursor = (self.lb_cursor + 1) % self.lb_list.len();
if self.peers.contains_key(&id) {
return Some(id);
}
if let Some(pos) = self.lb_list.iter().position(|x| x == &id) {
self.lb_list.remove(pos);
}
attempts += 1;
}
None
}
fn route_outbound(&mut self, mut parts: Vec<Bytes>) {
if parts.is_empty() {
return;
}
match self.behavior {
RouterBehavior::Standard => {
let target_id = parts.remove(0);
if !parts.is_empty() && parts[0].is_empty() {
parts.remove(0);
}
if let Some(tx) = self.peers.get(&target_id) {
let _ = tx.send(PeerCmd::SendBody(Arc::new(parts)));
} else {
}
}
RouterBehavior::LoadBalancer => {
if let Some(id) = self.pick_rr_peer() {
if let Some(tx) = self.peers.get(&id) {
let _ = tx.send(PeerCmd::SendBody(Arc::new(parts)));
}
} else {
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn b(s: &str) -> Bytes {
Bytes::copy_from_slice(s.as_bytes())
}
async fn recv_body(rx: &Receiver<PeerCmd>) -> Option<Vec<Bytes>> {
match crate::rt::timeout(Duration::from_secs(1), rx.recv_async()).await {
Ok(Ok(PeerCmd::SendBody(parts))) => Some((*parts).clone()),
_ => None,
}
}
async fn expect_no_body(rx: &Receiver<PeerCmd>) -> bool {
!matches!(
crate::rt::timeout(Duration::from_millis(150), rx.recv_async()).await,
Ok(Ok(PeerCmd::SendBody(_)))
)
}
#[test]
fn standard_routes_by_id_strips_envelope_and_drops_unknown() {
crate::rt::LocalRuntime::new().unwrap().block_on(async {
let (hub_tx, hub_rx) = flume::unbounded::<HubEvent>();
let (user_tx, user_rx) = flume::unbounded::<RouterCmd>();
let hub = RouterHub::new(hub_rx, user_rx, RouterBehavior::Standard);
let handle = crate::rt::spawn(hub.run());
let (peer_a_tx, peer_a_rx) = flume::unbounded::<PeerCmd>();
hub_tx
.send(HubEvent::PeerUp {
routing_id: b("A"),
tx: peer_a_tx,
})
.unwrap();
crate::rt::sleep(Duration::from_millis(30)).await;
user_tx
.send(RouterCmd::SendMessage(vec![
b("A"),
Bytes::new(),
b("hello"),
]))
.unwrap();
assert_eq!(recv_body(&peer_a_rx).await, Some(vec![b("hello")]));
user_tx
.send(RouterCmd::SendMessage(vec![b("Z"), b("payload")]))
.unwrap();
assert!(
expect_no_body(&peer_a_rx).await,
"unknown id must be dropped"
);
drop(hub_tx);
drop(user_tx);
crate::rt::join(handle).await;
});
}
#[test]
fn load_balancer_round_robins_across_peers() {
crate::rt::LocalRuntime::new().unwrap().block_on(async {
let (hub_tx, hub_rx) = flume::unbounded::<HubEvent>();
let (user_tx, user_rx) = flume::unbounded::<RouterCmd>();
let hub = RouterHub::new(hub_rx, user_rx, RouterBehavior::LoadBalancer);
let handle = crate::rt::spawn(hub.run());
let (peer_a_tx, peer_a_rx) = flume::unbounded::<PeerCmd>();
let (peer_b_tx, peer_b_rx) = flume::unbounded::<PeerCmd>();
hub_tx
.send(HubEvent::PeerUp {
routing_id: b("A"),
tx: peer_a_tx,
})
.unwrap();
hub_tx
.send(HubEvent::PeerUp {
routing_id: b("B"),
tx: peer_b_tx,
})
.unwrap();
crate::rt::sleep(Duration::from_millis(30)).await;
user_tx.send(RouterCmd::SendMessage(vec![b("m1")])).unwrap();
user_tx.send(RouterCmd::SendMessage(vec![b("m2")])).unwrap();
let got_a = recv_body(&peer_a_rx).await;
let got_b = recv_body(&peer_b_rx).await;
assert!(
got_a.is_some() && got_b.is_some(),
"each peer should receive exactly one message under round robin"
);
let mut bodies = vec![got_a.unwrap(), got_b.unwrap()];
bodies.sort();
assert_eq!(bodies, vec![vec![b("m1")], vec![b("m2")]]);
drop(hub_tx);
drop(user_tx);
crate::rt::join(handle).await;
});
}
#[test]
fn peer_down_stops_delivery() {
crate::rt::LocalRuntime::new().unwrap().block_on(async {
let (hub_tx, hub_rx) = flume::unbounded::<HubEvent>();
let (user_tx, user_rx) = flume::unbounded::<RouterCmd>();
let hub = RouterHub::new(hub_rx, user_rx, RouterBehavior::Standard);
let handle = crate::rt::spawn(hub.run());
let (peer_a_tx, peer_a_rx) = flume::unbounded::<PeerCmd>();
hub_tx
.send(HubEvent::PeerUp {
routing_id: b("A"),
tx: peer_a_tx,
})
.unwrap();
crate::rt::sleep(Duration::from_millis(30)).await;
hub_tx
.send(HubEvent::PeerDown { routing_id: b("A") })
.unwrap();
crate::rt::sleep(Duration::from_millis(30)).await;
user_tx
.send(RouterCmd::SendMessage(vec![b("A"), b("late")]))
.unwrap();
assert!(
expect_no_body(&peer_a_rx).await,
"a downed peer must not receive messages"
);
drop(hub_tx);
drop(user_tx);
crate::rt::join(handle).await;
});
}
}