1use std::sync::Arc;
13
14use axum::extract::{Query, State};
15use axum::response::{IntoResponse, Response};
16use axum::routing::{get, post};
17use axum::{Json, Router, http::StatusCode};
18use dashmap::DashMap;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use tokio::sync::Notify;
22use tokio::time::{Duration, timeout};
23
24use crate::store::Store;
25
26pub const DEFAULT_RECV_TIMEOUT_MS: u64 = 25_000;
27
28pub const ROSTER_TTL_MS: u64 = 90_000;
31
32const ROSTER_CAP: usize = 4096;
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct Envelope {
39 pub payload: Value,
40 pub ts: u64,
42}
43
44#[derive(Clone)]
45pub struct Broker {
46 store: Store,
47 notifies: Arc<DashMap<String, Arc<Notify>>>,
50 roster: Arc<DashMap<String, (Value, u64)>>,
54 cap: usize,
55}
56
57impl Broker {
58 pub fn new(store: Store, cap: usize) -> Self {
59 Self {
60 store,
61 notifies: Arc::new(DashMap::new()),
62 roster: Arc::new(DashMap::new()),
63 cap: cap.max(1),
64 }
65 }
66
67 pub fn announce(&self, pubkey: String, announcement: Value, now: u64) {
71 self.roster
72 .retain(|_, (_, at)| now.saturating_sub(*at) < ROSTER_TTL_MS);
73 if self.roster.len() >= ROSTER_CAP && !self.roster.contains_key(&pubkey) {
74 return; }
76 self.roster.insert(pubkey, (announcement, now));
77 }
78
79 pub fn roster(&self, now: u64) -> Vec<Value> {
81 self.roster
82 .iter()
83 .filter(|e| now.saturating_sub(e.value().1) < ROSTER_TTL_MS)
84 .map(|e| e.value().0.clone())
85 .collect()
86 }
87
88 fn notify_handle(&self, id: &str) -> Arc<Notify> {
89 self.notifies
90 .entry(id.to_string())
91 .or_insert_with(|| Arc::new(Notify::new()))
92 .clone()
93 }
94
95 pub async fn enqueue(&self, to: &str, payload: Value, ts: u64) -> anyhow::Result<()> {
97 let bytes = serde_json::to_vec(&Envelope { payload, ts })?;
98 self.store.enqueue(to.to_string(), bytes).await?;
99 while self.store.depth(to.to_string()).await? > self.cap {
102 match self.store.peek_oldest(to.to_string()).await? {
103 Some((old_key, _)) => {
104 self.store.ack(old_key).await?;
105 tracing::warn!(to, cap = self.cap, "queue full; dropped oldest");
106 }
107 None => break,
108 }
109 }
110 self.notify_handle(to).notify_one();
111 Ok(())
112 }
113
114 pub async fn recv(
117 &self,
118 id: &str,
119 wait: Duration,
120 ) -> anyhow::Result<Option<(Envelope, String)>> {
121 let deadline = tokio::time::Instant::now() + wait;
122 let notify = self.notify_handle(id);
123 loop {
124 if let Some((key, bytes)) = self.store.peek_oldest(id.to_string()).await? {
125 return Ok(Some((serde_json::from_slice(&bytes)?, key)));
126 }
127 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
128 if remaining.is_zero() {
129 return Ok(None);
130 }
131 let notified = notify.notified();
134 if timeout(remaining, notified).await.is_err() {
135 return Ok(None);
136 }
137 }
138 }
139
140 pub async fn ack(&self, key: &str) -> anyhow::Result<()> {
141 self.store.ack(key.to_string()).await
142 }
143
144 pub async fn depth(&self, id: &str) -> anyhow::Result<usize> {
145 self.store.depth(id.to_string()).await
146 }
147
148 pub fn router(self) -> Router {
149 Router::new()
150 .route("/send", post(send))
151 .route("/recv", get(recv_handler))
152 .route("/ack", post(ack_handler))
153 .route("/announce", post(announce_handler))
154 .route("/roster", get(roster_handler))
155 .with_state(self)
156 }
157}
158
159#[derive(Deserialize)]
160struct SendBody {
161 to: String,
162 payload: Value,
163}
164
165async fn send(State(broker): State<Broker>, Json(body): Json<SendBody>) -> StatusCode {
166 match broker
167 .enqueue(&body.to, body.payload, crate::now_ms())
168 .await
169 {
170 Ok(()) => StatusCode::ACCEPTED,
171 Err(e) => {
172 tracing::error!("enqueue failed: {e}");
173 StatusCode::INTERNAL_SERVER_ERROR
174 }
175 }
176}
177
178#[derive(Deserialize)]
179struct RecvQuery {
180 me: String,
181 #[serde(default = "default_timeout")]
182 timeout_ms: u64,
183}
184
185fn default_timeout() -> u64 {
186 DEFAULT_RECV_TIMEOUT_MS
187}
188
189async fn recv_handler(State(broker): State<Broker>, Query(q): Query<RecvQuery>) -> Response {
190 match broker
191 .recv(&q.me, Duration::from_millis(q.timeout_ms))
192 .await
193 {
194 Ok(Some((env, ack))) => {
195 Json(json!({ "status": "message", "envelope": env, "ack": ack })).into_response()
196 }
197 Ok(None) => Json(json!({ "status": "timeout" })).into_response(),
198 Err(e) => {
199 tracing::error!("recv failed: {e}");
200 (
203 StatusCode::INTERNAL_SERVER_ERROR,
204 Json(json!({ "status": "error" })),
205 )
206 .into_response()
207 }
208 }
209}
210
211#[derive(Deserialize)]
212struct AckBody {
213 me: String,
214 ack: String,
215}
216
217async fn ack_handler(State(broker): State<Broker>, Json(body): Json<AckBody>) -> StatusCode {
218 if !body.ack.starts_with(&format!("{}\u{0}", body.me)) {
222 return StatusCode::FORBIDDEN;
223 }
224 match broker.ack(&body.ack).await {
225 Ok(()) => StatusCode::OK,
226 Err(e) => {
227 tracing::error!("ack failed: {e}");
228 StatusCode::INTERNAL_SERVER_ERROR
229 }
230 }
231}
232
233async fn announce_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
236 let Some(pubkey) = body
237 .get("pubkey")
238 .and_then(|v| v.as_str())
239 .map(str::to_string)
240 else {
241 return StatusCode::BAD_REQUEST;
242 };
243 broker.announce(pubkey, body, crate::now_ms());
244 StatusCode::ACCEPTED
245}
246
247async fn roster_handler(State(broker): State<Broker>) -> Response {
248 Json(json!({ "roster": broker.roster(crate::now_ms()) })).into_response()
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 fn broker(cap: usize) -> Broker {
256 Broker::new(Store::in_memory().unwrap(), cap)
257 }
258
259 #[tokio::test]
260 async fn enqueue_recv_ack_roundtrip() {
261 let b = broker(8);
262 b.enqueue("alice", json!({ "hi": 1 }), 5).await.unwrap();
263 let (env, ack) = b
264 .recv("alice", Duration::from_millis(50))
265 .await
266 .unwrap()
267 .unwrap();
268 assert_eq!(env.payload, json!({ "hi": 1 }));
269 assert_eq!(env.ts, 5);
270 assert_eq!(b.depth("alice").await.unwrap(), 1);
272 b.ack(&ack).await.unwrap();
273 assert_eq!(b.depth("alice").await.unwrap(), 0);
274 }
275
276 #[tokio::test]
277 async fn redelivers_until_acked() {
278 let b = broker(8);
279 b.enqueue("alice", json!("x"), 1).await.unwrap();
280 let (_e1, ack) = b
281 .recv("alice", Duration::from_millis(50))
282 .await
283 .unwrap()
284 .unwrap();
285 let (_e2, ack2) = b
287 .recv("alice", Duration::from_millis(50))
288 .await
289 .unwrap()
290 .unwrap();
291 assert_eq!(ack, ack2);
292 b.ack(&ack).await.unwrap();
293 assert!(
294 b.recv("alice", Duration::from_millis(20))
295 .await
296 .unwrap()
297 .is_none()
298 );
299 }
300
301 #[tokio::test]
302 async fn recv_times_out_when_empty() {
303 let b = broker(8);
304 assert!(
305 b.recv("nobody", Duration::from_millis(10))
306 .await
307 .unwrap()
308 .is_none()
309 );
310 }
311
312 #[tokio::test]
313 async fn fifo_order_across_acks() {
314 let b = broker(8);
315 for i in 0..3 {
316 b.enqueue("bob", json!(i), i).await.unwrap();
317 }
318 for i in 0..3 {
319 let (env, ack) = b
320 .recv("bob", Duration::from_millis(50))
321 .await
322 .unwrap()
323 .unwrap();
324 assert_eq!(env.payload, json!(i));
325 b.ack(&ack).await.unwrap();
326 }
327 }
328
329 #[tokio::test]
330 async fn bounded_queue_drops_oldest() {
331 let b = broker(2);
332 for i in 0..4 {
333 b.enqueue("bob", json!(i), i).await.unwrap();
334 }
335 assert_eq!(b.depth("bob").await.unwrap(), 2);
336 let (env, _) = b
337 .recv("bob", Duration::from_millis(50))
338 .await
339 .unwrap()
340 .unwrap();
341 assert_eq!(env.payload, json!(2), "0 and 1 were evicted");
342 }
343
344 #[tokio::test]
345 async fn a_waiting_recv_is_woken_by_a_later_send() {
346 let b = broker(8);
347 let b2 = b.clone();
348 let waiter = tokio::spawn(async move { b2.recv("alice", Duration::from_secs(2)).await });
349 tokio::time::sleep(Duration::from_millis(20)).await;
350 b.enqueue("alice", json!("wake"), 1).await.unwrap();
351 let (env, _) = waiter.await.unwrap().unwrap().unwrap();
352 assert_eq!(env.payload, json!("wake"));
353 }
354
355 #[test]
356 fn roster_stores_and_expires() {
357 let b = broker(8);
358 b.announce(
359 "keyA".into(),
360 json!({"pubkey":"keyA","name":"alice"}),
361 1_000,
362 );
363 b.announce("keyB".into(), json!({"pubkey":"keyB","name":"bob"}), 1_000);
364 assert_eq!(b.roster(1_000).len(), 2);
365 assert_eq!(b.roster(1_000 + ROSTER_TTL_MS - 1).len(), 2, "within TTL");
366 assert_eq!(
367 b.roster(1_000 + ROSTER_TTL_MS + 1).len(),
368 0,
369 "expired past TTL"
370 );
371 }
372
373 #[test]
374 fn announce_upserts_by_pubkey() {
375 let b = broker(8);
376 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"old"}), 1_000);
377 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"new"}), 2_000);
378 let r = b.roster(2_000);
379 assert_eq!(r.len(), 1, "same pubkey replaces, not appends");
380 assert_eq!(r[0]["name"], "new");
381 }
382}