use std::sync::Arc;
use std::time::Duration;
use rainier_broadcast::kafka::{KafkaRelay, RelayedBroadcast};
use rainier_support::Result;
use rainier_websocket::{socket_from_identity, Rooms};
const RECONNECT_AFTER: Duration = Duration::from_secs(5);
type RoomNamer = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
#[derive(Clone)]
pub struct SocketFanOut {
rooms: Arc<Rooms>,
room_for: RoomNamer,
}
impl SocketFanOut {
pub fn new(rooms: Arc<Rooms>) -> Self {
Self { rooms, room_for: Arc::new(|channel: &str| Some(channel.to_string())) }
}
pub fn naming_rooms<F>(mut self, room_for: F) -> Self
where
F: Fn(&str) -> Option<String> + Send + Sync + 'static,
{
self.room_for = Arc::new(room_for);
self
}
pub fn deliver(&self, broadcast: &RelayedBroadcast) -> usize {
let Some(room) = (self.room_for)(&broadcast.channel) else {
return 0;
};
let body = broadcast.wire_payload().to_string();
match broadcast.except.as_deref().and_then(socket_from_identity) {
Some(socket) => self.rooms.send_except(&room, socket, body),
None => self.rooms.send(&room, body),
}
}
pub fn rooms(&self) -> &Arc<Rooms> {
&self.rooms
}
}
pub async fn run(relay: &KafkaRelay, fan_out: &SocketFanOut) -> Result<()> {
let mut cursor = relay.subscribe().await?;
tracing::info!(
topic = relay.topic(),
partitions = cursor.len(),
"relaying broadcasts to this process's sockets"
);
loop {
for broadcast in relay.poll(&mut cursor).await? {
let reached = fan_out.deliver(&broadcast);
tracing::debug!(
channel = broadcast.channel,
event = broadcast.event,
sockets = reached,
"relayed"
);
}
}
}
pub fn spawn(relay: KafkaRelay, fan_out: SocketFanOut) -> tokio::task::JoinHandle<()> {
rainier_container::spawn_with_facades(async move {
loop {
if let Err(e) = run(&relay, &fan_out).await {
tracing::error!(
topic = relay.topic(),
error = %e,
"the broadcast relay stopped; reconnecting"
);
}
tokio::time::sleep(RECONNECT_AFTER).await;
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use rainier_drivers::kafka::KafkaPosition;
use rainier_websocket::{Socket, SocketId};
use serde_json::json;
use tokio::sync::mpsc;
fn broadcast(channel: &str, except: Option<&str>) -> RelayedBroadcast {
RelayedBroadcast {
channel: channel.to_string(),
event: "OrderShipped".into(),
payload: json!({ "id": 7 }),
except: except.map(str::to_string),
position: KafkaPosition { topic: "broadcasts".into(), partition: 0, offset: 1 },
}
}
fn socket() -> (Socket, mpsc::UnboundedReceiver<rainier_websocket::Outbound>) {
let (tx, rx) = mpsc::unbounded_channel();
(Socket::new(SocketId::next(), "/ws", vec![], tx), rx)
}
#[test]
fn a_broadcast_reaches_the_room_named_after_its_channel() {
let rooms = Arc::new(Rooms::new());
let (socket, mut rx) = socket();
rooms.join("private-orders.7", socket);
let reached = SocketFanOut::new(rooms).deliver(&broadcast("private-orders.7", None));
assert_eq!(reached, 1);
assert!(rx.try_recv().is_ok(), "the socket should have been sent something");
}
#[test]
fn the_room_name_can_be_derived_from_the_channel() {
let rooms = Arc::new(Rooms::new());
let (socket, mut rx) = socket();
rooms.join("orders.7", socket);
let fan_out = SocketFanOut::new(rooms)
.naming_rooms(|channel| Some(channel.trim_start_matches("private-").to_string()));
assert_eq!(fan_out.deliver(&broadcast("private-orders.7", None)), 1);
assert!(rx.try_recv().is_ok());
}
#[test]
fn a_channel_this_process_does_not_serve_is_dropped() {
let rooms = Arc::new(Rooms::new());
let (socket, mut rx) = socket();
rooms.join("private-orders.7", socket);
let fan_out = SocketFanOut::new(rooms).naming_rooms(|_| None);
assert_eq!(fan_out.deliver(&broadcast("private-orders.7", None)), 0);
assert!(rx.try_recv().is_err(), "nothing should have been sent");
}
#[test]
fn to_others_skips_the_socket_that_caused_it() {
let rooms = Arc::new(Rooms::new());
let (mine, mut mine_rx) = socket();
let (theirs, mut theirs_rx) = socket();
let identity = mine.identity();
rooms.join("news", mine);
rooms.join("news", theirs);
let reached = SocketFanOut::new(rooms).deliver(&broadcast("news", Some(&identity)));
assert_eq!(reached, 1);
assert!(mine_rx.try_recv().is_err(), "the socket that caused it hears nothing");
assert!(theirs_rx.try_recv().is_ok(), "everyone else does");
}
#[test]
fn an_identity_from_another_replica_skips_nobody_here() {
let rooms = Arc::new(Rooms::new());
let (socket, mut rx) = socket();
let number = socket.id().get();
rooms.join("news", socket);
let reached = SocketFanOut::new(rooms)
.deliver(&broadcast("news", Some(&format!("another-replica.{number}"))));
assert_eq!(reached, 1, "that socket is not in this process");
assert!(rx.try_recv().is_ok());
}
#[test]
fn the_body_is_the_event_and_its_data() {
let rooms = Arc::new(Rooms::new());
let (socket, mut rx) = socket();
rooms.join("news", socket);
SocketFanOut::new(rooms).deliver(&broadcast("news", None));
match rx.try_recv().unwrap() {
rainier_websocket::Outbound::Send(message) => {
let body: serde_json::Value =
serde_json::from_str(message.as_text().unwrap()).unwrap();
assert_eq!(body["event"], "OrderShipped");
assert_eq!(body["data"]["id"], 7);
}
other => panic!("{other:?}"),
}
}
}