Allow multiple labrinth instances (#3360)

* Move a lot of scheduled tasks to be runnable from the command-line

* Use pubsub to handle sockets connected to multiple Labrinths

* Clippy fix

* Fix build and merge some stuff

* Fix build fmt
:

---------

Signed-off-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Josiah Glosson
2025-03-15 09:28:20 -05:00
committed by GitHub
parent 84a9438a70
commit c998d2566e
21 changed files with 1056 additions and 692 deletions

View File

@@ -0,0 +1,87 @@
use crate::queue::socket::ActiveSockets;
use crate::routes::internal::statuses::{
broadcast_to_local_friends, send_message_to_user,
};
use actix_web::web::Data;
use ariadne::ids::UserId;
use ariadne::networking::message::ServerToClientMessage;
use ariadne::users::UserStatus;
use redis::aio::PubSub;
use redis::{RedisWrite, ToRedisArgs};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tokio_stream::StreamExt;
pub const FRIENDS_CHANNEL_NAME: &str = "friends";
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RedisFriendsMessage {
StatusUpdate { status: UserStatus },
UserOffline { user: UserId },
DirectStatusUpdate { to_user: UserId, status: UserStatus },
}
impl ToRedisArgs for RedisFriendsMessage {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
out.write_arg(&serde_json::to_vec(&self).unwrap())
}
}
pub async fn handle_pubsub(
mut pubsub: PubSub,
pool: PgPool,
sockets: Data<ActiveSockets>,
) {
pubsub.subscribe(FRIENDS_CHANNEL_NAME).await.unwrap();
let mut stream = pubsub.into_on_message();
while let Some(message) = stream.next().await {
if message.get_channel_name() != FRIENDS_CHANNEL_NAME {
continue;
}
let payload = serde_json::from_slice(message.get_payload_bytes());
let pool = pool.clone();
let sockets = sockets.clone();
actix_rt::spawn(async move {
match payload {
Ok(RedisFriendsMessage::StatusUpdate { status }) => {
let _ = broadcast_to_local_friends(
status.user_id,
ServerToClientMessage::StatusUpdate { status },
&pool,
&sockets,
)
.await;
}
Ok(RedisFriendsMessage::UserOffline { user }) => {
let _ = broadcast_to_local_friends(
user,
ServerToClientMessage::UserOffline { id: user },
&pool,
&sockets,
)
.await;
}
Ok(RedisFriendsMessage::DirectStatusUpdate {
to_user,
status,
}) => {
let _ = send_message_to_user(
&sockets,
to_user,
&ServerToClientMessage::StatusUpdate { status },
)
.await;
}
Err(_) => {}
}
});
}
}

View File

@@ -0,0 +1,2 @@
pub mod friends;
pub mod status;

View File

@@ -0,0 +1,71 @@
use crate::database::redis::RedisPool;
use crate::queue::socket::ActiveSockets;
use ariadne::ids::UserId;
use ariadne::users::UserStatus;
use redis::AsyncCommands;
const EXPIRY_TIME_SECONDS: i64 = 60;
pub async fn get_user_status(
user: UserId,
local_sockets: &ActiveSockets,
redis: &RedisPool,
) -> Option<UserStatus> {
if let Some(friend_status) = local_sockets.get_status(user) {
return Some(friend_status);
}
if let Ok(mut conn) = redis.pool.get().await {
if let Ok(mut statuses) =
conn.sscan::<_, String>(get_field_name(user)).await
{
if let Some(status_json) = statuses.next_item().await {
return serde_json::from_str::<UserStatus>(&status_json).ok();
}
}
}
None
}
pub async fn replace_user_status(
old_status: Option<&UserStatus>,
new_status: Option<&UserStatus>,
redis: &RedisPool,
) -> Result<(), redis::RedisError> {
let Some(user) = new_status.or(old_status).map(|x| x.user_id) else {
return Ok(());
};
if let Ok(mut conn) = redis.pool.get().await {
let field_name = get_field_name(user);
let mut pipe = redis::pipe();
pipe.atomic();
if let Some(status) = old_status {
pipe.srem(&field_name, serde_json::to_string(&status).unwrap())
.ignore();
}
if let Some(status) = new_status {
pipe.sadd(&field_name, serde_json::to_string(&status).unwrap())
.ignore();
pipe.expire(&field_name, EXPIRY_TIME_SECONDS).ignore();
}
return pipe.query_async(&mut conn).await;
}
Ok(())
}
pub async fn push_back_user_expiry(
user: UserId,
redis: &RedisPool,
) -> Result<(), redis::RedisError> {
if let Ok(mut conn) = redis.pool.get().await {
return conn.expire(get_field_name(user), EXPIRY_TIME_SECONDS).await;
}
Ok(())
}
fn get_field_name(user: UserId) -> String {
format!("user_status:{}", user)
}