use super::event::WsMessage;
use crate::metrics::Metrics;
use crate::router::{RouterCommand, run_router};
use crate::session::WsSession;
use redis::{AsyncCommands, Client, aio::ConnectionManager};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use uuid::Uuid;
use tracing::{warn, info};
const ONLINE_USERS_KEY: &str = "arifa:online_users";
const USER_SESSIONS_PREFIX: &str = "arifa:user_sessions";
const SESSION_USER_PREFIX: &str = "arifa:session_user";
const USER_NODE_PREFIX: &str = "arifa:user_node";
const SESSION_CHANNEL_CAPACITY: usize = 256;
#[derive(Clone)]
pub struct Arifa {
manager: ConnectionManager,
presence_manager: ConnectionManager,
node_id: String,
command_tx: mpsc::UnboundedSender<RouterCommand>,
sessions: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
shutdown_tx: watch::Sender<bool>,
pub metrics: Metrics,
}
impl Arifa {
pub async fn new(
redis_url: impl AsRef<str>,
node_id: impl Into<String>,
) -> redis::RedisResult<Self> {
let redis_url = redis_url.as_ref().to_string();
let client = Client::open(redis_url.as_str())?;
let manager = client.get_connection_manager().await?;
let presence_manager = client.get_connection_manager().await?;
let node_id = node_id.into();
let metrics = Metrics::default();
let (command_tx, command_rx) = mpsc::unbounded_channel();
let (shutdown_tx, shutdown_rx) = watch::channel(false);
{
let router_node_id = node_id.clone();
let router_metrics = metrics.clone();
tokio::spawn(async move {
run_router(
redis_url,
router_node_id,
command_rx,
shutdown_rx,
router_metrics,
)
.await;
});
}
Ok(Self {
manager,
presence_manager,
node_id,
command_tx,
sessions: Arc::new(Mutex::new(HashMap::new())),
shutdown_tx,
metrics,
})
}
pub fn node_id(&self) -> &str {
&self.node_id
}
pub async fn subscribe<C, S>(
&self,
channels: impl IntoIterator<Item = S>,
session: C,
user_id: impl Into<String>,
) -> String
where
C: WsSession + 'static,
S: Into<String>,
{
let channels = channels.into_iter().map(Into::into).collect::<Vec<_>>();
let session = Arc::new(session);
let arifa = self.clone();
let session_id = Uuid::new_v4().to_string();
let user_id = user_id.into();
let (tx, mut rx) = mpsc::channel::<WsMessage>(SESSION_CHANNEL_CAPACITY);
for channel in &channels {
let (ack_tx, ack_rx) = tokio::sync::oneshot::channel();
let _ = self.command_tx.send(RouterCommand::Subscribe {
channel: channel.clone(),
session_id: session_id.clone(),
sender: tx.clone(),
ack: ack_tx,
});
let _ = ack_rx.await; }
self.metrics.session_started();
let task_session_id = session_id.clone();
let metrics = self.metrics.clone();
let handle = tokio::spawn(async move {
if let Err(err) = arifa.add_online_user(&task_session_id, &user_id).await {
warn!("Failed to mark session '{task_session_id}' online: {err}");
}
while let Some(event) = rx.recv().await {
if session.send(event).await.is_err() {
break;
}
}
metrics.session_ended();
info!("Forwarder for session '{task_session_id}' stopped.");
});
self.sessions
.lock()
.unwrap()
.insert(session_id.clone(), handle);
session_id
}
pub fn unsubscribe(&self, session_id: &str, channels: &[String]) {
for channel in channels {
let _ = self.command_tx.send(RouterCommand::Unsubscribe {
channel: channel.clone(),
session_id: session_id.to_string(),
});
}
if let Some(handle) = self.sessions.lock().unwrap().remove(session_id) {
self.metrics.session_ended();
handle.abort();
}
}
pub async fn publish(
&self,
channel: impl AsRef<str>,
event: &WsMessage,
) -> redis::RedisResult<()> {
let payload = serde_json::to_string(event).expect("WsMessage serialization failed");
let mut conn = self.manager.clone();
let _: usize = conn.publish(channel.as_ref(), payload).await?;
Ok(())
}
pub async fn remove_online_user(&self, session_id: &str) -> redis::RedisResult<()> {
let mut conn = self.presence_manager.clone();
let user_id: Option<String> = conn
.get(format!("{SESSION_USER_PREFIX}:{session_id}"))
.await?;
let _: usize = conn.srem(ONLINE_USERS_KEY, session_id).await?;
if let Some(user_id) = user_id {
let user_sessions_key = format!("{USER_SESSIONS_PREFIX}:{user_id}");
let _: usize = conn.srem(&user_sessions_key, session_id).await?;
let _: usize = conn
.del(format!("{SESSION_USER_PREFIX}:{session_id}"))
.await?;
let remaining: u64 = conn.scard(&user_sessions_key).await?;
if remaining == 0 {
let _: usize = conn.del(format!("{USER_NODE_PREFIX}:{user_id}")).await?;
let _: usize = conn.del(user_sessions_key).await?;
}
}
Ok(())
}
pub async fn get_user_node_id(&self, user_id: &str) -> redis::RedisResult<Option<String>> {
let mut conn = self.presence_manager.clone();
conn.get(format!("{USER_NODE_PREFIX}:{user_id}")).await
}
pub async fn add_online_user(&self, session_id: &str, user_id: &str) -> redis::RedisResult<()> {
let mut conn = self.presence_manager.clone();
let _: usize = conn.sadd(ONLINE_USERS_KEY, session_id).await?;
let _: () = conn
.set(format!("{SESSION_USER_PREFIX}:{session_id}"), user_id)
.await?;
let _: usize = conn
.sadd(format!("{USER_SESSIONS_PREFIX}:{user_id}"), session_id)
.await?;
let _: () = conn
.set(format!("{USER_NODE_PREFIX}:{user_id}"), &self.node_id)
.await?;
Ok(())
}
pub async fn is_user_online(&self, user_id: &str) -> redis::RedisResult<bool> {
let mut conn = self.presence_manager.clone();
let count: u64 = conn
.scard(format!("{USER_SESSIONS_PREFIX}:{user_id}"))
.await?;
Ok(count > 0)
}
pub async fn online_users(&self) -> redis::RedisResult<u64> {
let mut conn = self.presence_manager.clone();
conn.scard(ONLINE_USERS_KEY).await
}
pub async fn shutdown(&self) {
let sessions: Vec<(String, tokio::task::JoinHandle<()>)> = {
let mut sessions = self.sessions.lock().unwrap();
sessions.drain().collect()
};
for (session_id, handle) in sessions {
let _ = self.remove_online_user(&session_id).await;
handle.abort();
}
let _ = self.shutdown_tx.send(true);
}
}