use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use axum::{
extract::Query,
response::sse::{Event, Sse},
};
use futures::StreamExt;
use serde::Deserialize;
use std::convert::Infallible;
use uuid::Uuid;
pub type SessionId = String;
const SESSION_QUEUE_CAPACITY: usize = 256;
const MAX_SESSIONS: usize = 1_000;
type SessionSender = tokio::sync::mpsc::Sender<String>;
struct RxStream {
rx: tokio::sync::mpsc::Receiver<String>,
}
impl futures::Stream for RxStream {
type Item = String;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<String>> {
self.rx.poll_recv(cx)
}
}
#[derive(Clone, Default)]
pub struct SessionManager {
sessions: Arc<Mutex<HashMap<SessionId, SessionSender>>>,
insertion_order: Arc<Mutex<VecDeque<SessionId>>>,
}
impl SessionManager {
pub fn new() -> Self {
Self {
sessions: Arc::new(Mutex::new(HashMap::new())),
insertion_order: Arc::new(Mutex::new(VecDeque::new())),
}
}
pub fn create_id(&self) -> SessionId {
let id = Uuid::new_v4().to_string();
let mut guard = self.sessions.lock().unwrap();
if guard.len() >= MAX_SESSIONS {
let mut order = self.insertion_order.lock().unwrap();
while guard.len() >= MAX_SESSIONS {
match order.pop_front() {
Some(oldest) => {
guard.remove(&oldest);
}
None => break,
}
}
}
self.insertion_order.lock().unwrap().push_back(id.clone());
guard.insert(
id.clone(),
tokio::sync::mpsc::channel(SESSION_QUEUE_CAPACITY).0,
);
id
}
pub fn send(&self, session_id: &str, msg: &str) -> bool {
let mut guard = self.sessions.lock().unwrap();
if let Some(tx) = guard.get(session_id) {
let ok = tx.try_send(msg.to_string()).is_ok();
if !ok {
guard.remove(session_id);
self.forget_order(session_id);
}
ok
} else {
false
}
}
fn forget_order(&self, session_id: &str) {
self.insertion_order
.lock()
.unwrap()
.retain(|id| id != session_id);
}
pub fn close(&self, session_id: &str) {
self.sessions.lock().unwrap().remove(session_id);
self.forget_order(session_id);
}
pub fn exists(&self, session_id: &str) -> bool {
self.sessions.lock().unwrap().contains_key(session_id)
}
}
#[derive(Deserialize)]
pub struct EventsQuery {
pub session: String,
}
pub async fn sse_events_handler(
Query(q): Query<EventsQuery>,
sessions: axum::extract::State<SessionManager>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
sse_events_handler_impl(sessions.0, q.session).await
}
pub async fn sse_events_handler_impl(
sessions: SessionManager,
session_id: String,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
let (tx, rx) = tokio::sync::mpsc::channel(SESSION_QUEUE_CAPACITY);
{
let mut guard = sessions.sessions.lock().unwrap();
if guard.insert(session_id.clone(), tx.clone()).is_some() {
tracing::warn!(
target: "plana_jsonrpc::session",
session = %session_id,
"SSE mount replaced a live session sender (reconnect or takeover)"
);
}
{
let mut order = sessions.insertion_order.lock().unwrap();
order.retain(|id| id != &session_id);
order.push_back(session_id.clone());
}
}
let janitor = SessionJanitor {
sessions: std::sync::Arc::clone(&sessions.sessions),
insertion_order: std::sync::Arc::clone(&sessions.insertion_order),
session_id: session_id.clone(),
own_tx: tx.clone(),
};
let stream = RxStream { rx }
.map(move |msg| Ok(Event::default().data(msg)))
.chain(futures::stream::unfold(janitor, |_j| async {
None::<(Result<Event, Infallible>, SessionJanitor)>
}));
Sse::new(stream)
}
struct SessionJanitor {
sessions: std::sync::Arc<std::sync::Mutex<HashMap<SessionId, SessionSender>>>,
insertion_order: std::sync::Arc<std::sync::Mutex<VecDeque<SessionId>>>,
session_id: String,
own_tx: SessionSender,
}
impl Drop for SessionJanitor {
fn drop(&mut self) {
let mut guard = self.sessions.lock().unwrap();
if let Some(current) = guard.get(&self.session_id) {
if current.same_channel(&self.own_tx) {
guard.remove(&self.session_id);
self.insertion_order
.lock()
.unwrap()
.retain(|id| id != &self.session_id);
}
}
}
}
#[cfg(test)]
mod reclamation_tests {
use super::*;
use futures::FutureExt;
#[test]
fn a_dead_receiver_is_reclaimed_on_the_next_push() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
assert!(!mgr.send(&sid, "x"), "no receiver: undeliverable");
assert!(!mgr.exists(&sid), "the dead entry is reclaimed");
assert!(!mgr.send(&sid, "y"));
}
#[test]
fn a_live_receiver_stays_registered() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
let (tx, rx) = tokio::sync::mpsc::channel(16);
mgr.sessions.lock().unwrap().insert(sid.clone(), tx);
assert!(mgr.send(&sid, "hello"));
assert!(mgr.exists(&sid), "live sessions stay");
use futures::StreamExt;
let mut rx = RxStream { rx };
assert_eq!(rx.next().now_or_never().unwrap().unwrap(), "hello");
mgr.close(&sid);
mgr.close(&sid);
assert!(!mgr.exists(&sid));
}
}
#[cfg(test)]
mod janitor_tests {
use super::*;
#[tokio::test]
async fn a_completed_subscription_reclaims_its_entry() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
let sse = sse_events_handler_impl(SessionManager::clone(&mgr), sid.clone()).await;
assert!(mgr.exists(&sid));
drop(sse);
assert!(
!mgr.exists(&sid),
"a completed subscription must not outlive its stream"
);
}
}
#[cfg(test)]
mod reconnect_race_tests {
use super::*;
#[tokio::test]
async fn an_old_janitor_cannot_evict_a_reconnected_mount() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
let sse1 = sse_events_handler_impl(mgr.clone(), sid.clone()).await;
assert!(mgr.exists(&sid), "first mount registered");
let sse2 = sse_events_handler_impl(mgr.clone(), sid.clone()).await;
assert!(mgr.exists(&sid), "second mount replaced the sender");
assert_eq!(
mgr.insertion_order
.lock()
.unwrap()
.iter()
.filter(|id| *id == &sid)
.count(),
1,
"a remount must replace its stale order entry"
);
drop(sse1);
assert!(
mgr.exists(&sid),
"the old janitor must not evict the reconnected session"
);
assert!(mgr.send(&sid, "still-alive"));
drop(sse2);
assert!(!mgr.exists(&sid), "the last mount's janitor reclaims");
}
#[test]
fn a_full_queue_reclaims_the_session() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
let (tx, _rx) = tokio::sync::mpsc::channel::<String>(2);
mgr.sessions.lock().unwrap().insert(sid.clone(), tx);
assert!(mgr.send(&sid, "1"));
assert!(mgr.send(&sid, "2"));
assert!(
!mgr.send(&sid, "3"),
"a full queue must count as undeliverable"
);
assert!(!mgr.exists(&sid), "the wedged session must be reclaimed");
}
#[test]
fn the_session_map_stays_under_its_ceiling() {
let mgr = SessionManager::default();
let oldest = mgr.create_id();
for _ in 0..(MAX_SESSIONS + 10) {
let _ = mgr.create_id();
}
assert!(
mgr.sessions.lock().unwrap().len() <= MAX_SESSIONS,
"the map must stay at or under the ceiling"
);
assert!(
!mgr.exists(&oldest),
"FIFO: the oldest session must be the one evicted"
);
let newest = mgr.create_id();
assert!(mgr.exists(&newest), "the newest session must survive");
}
#[tokio::test]
async fn the_fifo_order_queue_never_holds_zombies() {
let mgr = SessionManager::default();
let sid = mgr.create_id();
let sse = sse_events_handler_impl(mgr.clone(), sid.clone()).await;
assert!(mgr.exists(&sid));
drop(sse);
assert!(!mgr.exists(&sid), "map entry reclaimed");
assert!(
mgr.insertion_order.lock().unwrap().is_empty(),
"the order queue must not hold zombie ids"
);
}
}