use std::sync::Arc;
use axum::extract::{Query, State};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router, http::StatusCode};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tokio::sync::Notify;
use tokio::time::{Duration, timeout};
use crate::store::Store;
pub const DEFAULT_RECV_TIMEOUT_MS: u64 = 25_000;
pub const ROSTER_TTL_MS: u64 = 90_000;
const ROSTER_CAP: usize = 4096;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Envelope {
pub payload: Value,
pub ts: u64,
}
#[derive(Clone)]
pub struct Broker {
store: Store,
notifies: Arc<DashMap<String, Arc<Notify>>>,
roster: Arc<DashMap<String, (Value, u64)>>,
cap: usize,
}
impl Broker {
pub fn new(store: Store, cap: usize) -> Self {
Self {
store,
notifies: Arc::new(DashMap::new()),
roster: Arc::new(DashMap::new()),
cap: cap.max(1),
}
}
pub fn announce(&self, pubkey: String, announcement: Value, now: u64) {
self.roster
.retain(|_, (_, at)| now.saturating_sub(*at) < ROSTER_TTL_MS);
if self.roster.len() >= ROSTER_CAP && !self.roster.contains_key(&pubkey) {
return; }
self.roster.insert(pubkey, (announcement, now));
}
pub fn roster(&self, now: u64) -> Vec<Value> {
self.roster
.iter()
.filter(|e| now.saturating_sub(e.value().1) < ROSTER_TTL_MS)
.map(|e| e.value().0.clone())
.collect()
}
fn notify_handle(&self, id: &str) -> Arc<Notify> {
self.notifies
.entry(id.to_string())
.or_insert_with(|| Arc::new(Notify::new()))
.clone()
}
pub async fn enqueue(&self, to: &str, payload: Value, ts: u64) -> anyhow::Result<()> {
let bytes = serde_json::to_vec(&Envelope { payload, ts })?;
self.store.enqueue(to.to_string(), bytes).await?;
while self.store.depth(to.to_string()).await? > self.cap {
match self.store.peek_oldest(to.to_string()).await? {
Some((old_key, _)) => {
self.store.ack(old_key).await?;
tracing::warn!(to, cap = self.cap, "queue full; dropped oldest");
}
None => break,
}
}
self.notify_handle(to).notify_one();
Ok(())
}
pub async fn recv(
&self,
id: &str,
wait: Duration,
) -> anyhow::Result<Option<(Envelope, String)>> {
let deadline = tokio::time::Instant::now() + wait;
let notify = self.notify_handle(id);
loop {
if let Some((key, bytes)) = self.store.peek_oldest(id.to_string()).await? {
return Ok(Some((serde_json::from_slice(&bytes)?, key)));
}
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let notified = notify.notified();
if timeout(remaining, notified).await.is_err() {
return Ok(None);
}
}
}
pub async fn ack(&self, key: &str) -> anyhow::Result<()> {
self.store.ack(key.to_string()).await
}
pub async fn depth(&self, id: &str) -> anyhow::Result<usize> {
self.store.depth(id.to_string()).await
}
pub fn router(self) -> Router {
Router::new()
.route("/send", post(send))
.route("/recv", get(recv_handler))
.route("/ack", post(ack_handler))
.route("/announce", post(announce_handler))
.route("/roster", get(roster_handler))
.with_state(self)
}
}
#[derive(Deserialize)]
struct SendBody {
to: String,
payload: Value,
}
async fn send(State(broker): State<Broker>, Json(body): Json<SendBody>) -> StatusCode {
match broker
.enqueue(&body.to, body.payload, crate::now_ms())
.await
{
Ok(()) => StatusCode::ACCEPTED,
Err(e) => {
tracing::error!("enqueue failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
#[derive(Deserialize)]
struct RecvQuery {
me: String,
#[serde(default = "default_timeout")]
timeout_ms: u64,
}
fn default_timeout() -> u64 {
DEFAULT_RECV_TIMEOUT_MS
}
async fn recv_handler(State(broker): State<Broker>, Query(q): Query<RecvQuery>) -> Response {
match broker
.recv(&q.me, Duration::from_millis(q.timeout_ms))
.await
{
Ok(Some((env, ack))) => {
Json(json!({ "status": "message", "envelope": env, "ack": ack })).into_response()
}
Ok(None) => Json(json!({ "status": "timeout" })).into_response(),
Err(e) => {
tracing::error!("recv failed: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "status": "error" })),
)
.into_response()
}
}
}
#[derive(Deserialize)]
struct AckBody {
me: String,
ack: String,
}
async fn ack_handler(State(broker): State<Broker>, Json(body): Json<AckBody>) -> StatusCode {
if !body.ack.starts_with(&format!("{}\u{0}", body.me)) {
return StatusCode::FORBIDDEN;
}
match broker.ack(&body.ack).await {
Ok(()) => StatusCode::OK,
Err(e) => {
tracing::error!("ack failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
async fn announce_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
let Some(pubkey) = body
.get("pubkey")
.and_then(|v| v.as_str())
.map(str::to_string)
else {
return StatusCode::BAD_REQUEST;
};
broker.announce(pubkey, body, crate::now_ms());
StatusCode::ACCEPTED
}
async fn roster_handler(State(broker): State<Broker>) -> Response {
Json(json!({ "roster": broker.roster(crate::now_ms()) })).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
fn broker(cap: usize) -> Broker {
Broker::new(Store::in_memory().unwrap(), cap)
}
#[tokio::test]
async fn enqueue_recv_ack_roundtrip() {
let b = broker(8);
b.enqueue("alice", json!({ "hi": 1 }), 5).await.unwrap();
let (env, ack) = b
.recv("alice", Duration::from_millis(50))
.await
.unwrap()
.unwrap();
assert_eq!(env.payload, json!({ "hi": 1 }));
assert_eq!(env.ts, 5);
assert_eq!(b.depth("alice").await.unwrap(), 1);
b.ack(&ack).await.unwrap();
assert_eq!(b.depth("alice").await.unwrap(), 0);
}
#[tokio::test]
async fn redelivers_until_acked() {
let b = broker(8);
b.enqueue("alice", json!("x"), 1).await.unwrap();
let (_e1, ack) = b
.recv("alice", Duration::from_millis(50))
.await
.unwrap()
.unwrap();
let (_e2, ack2) = b
.recv("alice", Duration::from_millis(50))
.await
.unwrap()
.unwrap();
assert_eq!(ack, ack2);
b.ack(&ack).await.unwrap();
assert!(
b.recv("alice", Duration::from_millis(20))
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn recv_times_out_when_empty() {
let b = broker(8);
assert!(
b.recv("nobody", Duration::from_millis(10))
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn fifo_order_across_acks() {
let b = broker(8);
for i in 0..3 {
b.enqueue("bob", json!(i), i).await.unwrap();
}
for i in 0..3 {
let (env, ack) = b
.recv("bob", Duration::from_millis(50))
.await
.unwrap()
.unwrap();
assert_eq!(env.payload, json!(i));
b.ack(&ack).await.unwrap();
}
}
#[tokio::test]
async fn bounded_queue_drops_oldest() {
let b = broker(2);
for i in 0..4 {
b.enqueue("bob", json!(i), i).await.unwrap();
}
assert_eq!(b.depth("bob").await.unwrap(), 2);
let (env, _) = b
.recv("bob", Duration::from_millis(50))
.await
.unwrap()
.unwrap();
assert_eq!(env.payload, json!(2), "0 and 1 were evicted");
}
#[tokio::test]
async fn a_waiting_recv_is_woken_by_a_later_send() {
let b = broker(8);
let b2 = b.clone();
let waiter = tokio::spawn(async move { b2.recv("alice", Duration::from_secs(2)).await });
tokio::time::sleep(Duration::from_millis(20)).await;
b.enqueue("alice", json!("wake"), 1).await.unwrap();
let (env, _) = waiter.await.unwrap().unwrap().unwrap();
assert_eq!(env.payload, json!("wake"));
}
#[test]
fn roster_stores_and_expires() {
let b = broker(8);
b.announce(
"keyA".into(),
json!({"pubkey":"keyA","name":"alice"}),
1_000,
);
b.announce("keyB".into(), json!({"pubkey":"keyB","name":"bob"}), 1_000);
assert_eq!(b.roster(1_000).len(), 2);
assert_eq!(b.roster(1_000 + ROSTER_TTL_MS - 1).len(), 2, "within TTL");
assert_eq!(
b.roster(1_000 + ROSTER_TTL_MS + 1).len(),
0,
"expired past TTL"
);
}
#[test]
fn announce_upserts_by_pubkey() {
let b = broker(8);
b.announce("keyA".into(), json!({"pubkey":"keyA","name":"old"}), 1_000);
b.announce("keyA".into(), json!({"pubkey":"keyA","name":"new"}), 2_000);
let r = b.roster(2_000);
assert_eq!(r.len(), 1, "same pubkey replaces, not appends");
assert_eq!(r[0]["name"], "new");
}
}