rainier_framework/
relay.rs1use std::sync::Arc;
32use std::time::Duration;
33
34use rainier_broadcast::kafka::{KafkaRelay, RelayedBroadcast};
35use rainier_support::Result;
36use rainier_websocket::{socket_from_identity, Rooms};
37
38const RECONNECT_AFTER: Duration = Duration::from_secs(5);
44
45type RoomNamer = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
48
49#[derive(Clone)]
65pub struct SocketFanOut {
66 rooms: Arc<Rooms>,
67 room_for: RoomNamer,
68}
69
70impl SocketFanOut {
71 pub fn new(rooms: Arc<Rooms>) -> Self {
73 Self { rooms, room_for: Arc::new(|channel: &str| Some(channel.to_string())) }
74 }
75
76 pub fn naming_rooms<F>(mut self, room_for: F) -> Self
82 where
83 F: Fn(&str) -> Option<String> + Send + Sync + 'static,
84 {
85 self.room_for = Arc::new(room_for);
86 self
87 }
88
89 pub fn deliver(&self, broadcast: &RelayedBroadcast) -> usize {
97 let Some(room) = (self.room_for)(&broadcast.channel) else {
98 return 0;
99 };
100
101 let body = broadcast.wire_payload().to_string();
102
103 match broadcast.except.as_deref().and_then(socket_from_identity) {
104 Some(socket) => self.rooms.send_except(&room, socket, body),
105 None => self.rooms.send(&room, body),
106 }
107 }
108
109 pub fn rooms(&self) -> &Arc<Rooms> {
111 &self.rooms
112 }
113}
114
115pub async fn run(relay: &KafkaRelay, fan_out: &SocketFanOut) -> Result<()> {
120 let mut cursor = relay.subscribe().await?;
121
122 tracing::info!(
123 topic = relay.topic(),
124 partitions = cursor.len(),
125 "relaying broadcasts to this process's sockets"
126 );
127
128 loop {
129 for broadcast in relay.poll(&mut cursor).await? {
130 let reached = fan_out.deliver(&broadcast);
131
132 tracing::debug!(
133 channel = broadcast.channel,
134 event = broadcast.event,
135 sockets = reached,
136 "relayed"
137 );
138 }
139 }
140}
141
142pub fn spawn(relay: KafkaRelay, fan_out: SocketFanOut) -> tokio::task::JoinHandle<()> {
155 rainier_container::spawn_with_facades(async move {
156 loop {
157 if let Err(e) = run(&relay, &fan_out).await {
158 tracing::error!(
159 topic = relay.topic(),
160 error = %e,
161 "the broadcast relay stopped; reconnecting"
162 );
163 }
164
165 tokio::time::sleep(RECONNECT_AFTER).await;
166 }
167 })
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use rainier_drivers::kafka::KafkaPosition;
174 use rainier_websocket::{Socket, SocketId};
175 use serde_json::json;
176 use tokio::sync::mpsc;
177
178 fn broadcast(channel: &str, except: Option<&str>) -> RelayedBroadcast {
179 RelayedBroadcast {
180 channel: channel.to_string(),
181 event: "OrderShipped".into(),
182 payload: json!({ "id": 7 }),
183 except: except.map(str::to_string),
184 position: KafkaPosition { topic: "broadcasts".into(), partition: 0, offset: 1 },
185 }
186 }
187
188 fn socket() -> (Socket, mpsc::UnboundedReceiver<rainier_websocket::Outbound>) {
189 let (tx, rx) = mpsc::unbounded_channel();
190 (Socket::new(SocketId::next(), "/ws", vec![], tx), rx)
191 }
192
193 #[test]
194 fn a_broadcast_reaches_the_room_named_after_its_channel() {
195 let rooms = Arc::new(Rooms::new());
196 let (socket, mut rx) = socket();
197 rooms.join("private-orders.7", socket);
198
199 let reached = SocketFanOut::new(rooms).deliver(&broadcast("private-orders.7", None));
200
201 assert_eq!(reached, 1);
202 assert!(rx.try_recv().is_ok(), "the socket should have been sent something");
203 }
204
205 #[test]
206 fn the_room_name_can_be_derived_from_the_channel() {
207 let rooms = Arc::new(Rooms::new());
208 let (socket, mut rx) = socket();
209 rooms.join("orders.7", socket);
210
211 let fan_out = SocketFanOut::new(rooms)
212 .naming_rooms(|channel| Some(channel.trim_start_matches("private-").to_string()));
213
214 assert_eq!(fan_out.deliver(&broadcast("private-orders.7", None)), 1);
215 assert!(rx.try_recv().is_ok());
216 }
217
218 #[test]
219 fn a_channel_this_process_does_not_serve_is_dropped() {
220 let rooms = Arc::new(Rooms::new());
221 let (socket, mut rx) = socket();
222 rooms.join("private-orders.7", socket);
223
224 let fan_out = SocketFanOut::new(rooms).naming_rooms(|_| None);
225
226 assert_eq!(fan_out.deliver(&broadcast("private-orders.7", None)), 0);
227 assert!(rx.try_recv().is_err(), "nothing should have been sent");
228 }
229
230 #[test]
231 fn to_others_skips_the_socket_that_caused_it() {
232 let rooms = Arc::new(Rooms::new());
233 let (mine, mut mine_rx) = socket();
234 let (theirs, mut theirs_rx) = socket();
235
236 let identity = mine.identity();
237 rooms.join("news", mine);
238 rooms.join("news", theirs);
239
240 let reached = SocketFanOut::new(rooms).deliver(&broadcast("news", Some(&identity)));
241
242 assert_eq!(reached, 1);
243 assert!(mine_rx.try_recv().is_err(), "the socket that caused it hears nothing");
244 assert!(theirs_rx.try_recv().is_ok(), "everyone else does");
245 }
246
247 #[test]
248 fn an_identity_from_another_replica_skips_nobody_here() {
249 let rooms = Arc::new(Rooms::new());
253 let (socket, mut rx) = socket();
254 let number = socket.id().get();
255 rooms.join("news", socket);
256
257 let reached = SocketFanOut::new(rooms)
258 .deliver(&broadcast("news", Some(&format!("another-replica.{number}"))));
259
260 assert_eq!(reached, 1, "that socket is not in this process");
261 assert!(rx.try_recv().is_ok());
262 }
263
264 #[test]
265 fn the_body_is_the_event_and_its_data() {
266 let rooms = Arc::new(Rooms::new());
267 let (socket, mut rx) = socket();
268 rooms.join("news", socket);
269
270 SocketFanOut::new(rooms).deliver(&broadcast("news", None));
271
272 match rx.try_recv().unwrap() {
273 rainier_websocket::Outbound::Send(message) => {
274 let body: serde_json::Value =
275 serde_json::from_str(message.as_text().unwrap()).unwrap();
276
277 assert_eq!(body["event"], "OrderShipped");
278 assert_eq!(body["data"]["id"], 7);
279 }
280 other => panic!("{other:?}"),
281 }
282 }
283}