Skip to content

Commit 32542a8

Browse files
committed
feat: rewrite presence system to use sets
1 parent 1df90ff commit 32542a8

File tree

8 files changed

+91
-156
lines changed

8 files changed

+91
-156
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bonfire/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "revolt-bonfire"
3-
version = "0.5.17"
3+
version = "0.5.18"
44
license = "AGPL-3.0-or-later"
55
edition = "2021"
66

crates/delta/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "revolt-delta"
3-
version = "0.5.17"
3+
version = "0.5.18"
44
license = "AGPL-3.0-or-later"
55
authors = ["Paul Makles <paulmakles@gmail.com>"]
66
edition = "2018"

crates/quark/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "revolt-quark"
3-
version = "0.5.17"
3+
version = "0.5.18"
44
license = "AGPL-3.0-or-later"
55
edition = "2021"
66

@@ -57,6 +57,7 @@ log = "0.4.14"
5757
pretty_env_logger = "0.4.0"
5858

5959
# Util
60+
rand = "0.8.5"
6061
ulid = "0.5.0"
6162
regex = "1.5.5"
6263
nanoid = "0.4.0"

crates/quark/src/presence/entry.rs

Lines changed: 6 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,12 @@
11
use std::env;
22

3-
use serde::{Deserialize, Serialize};
43
use once_cell::sync::Lazy;
54

6-
pub static REGION_ID: Lazy<u16> = Lazy::new(|| env::var("REGION_ID")
7-
.unwrap_or_else(|_| "0".to_string())
8-
.parse()
9-
.unwrap());
5+
pub static REGION_ID: Lazy<u16> = Lazy::new(|| {
6+
env::var("REGION_ID")
7+
.unwrap_or_else(|_| "0".to_string())
8+
.parse()
9+
.unwrap()
10+
});
1011

1112
pub static REGION_KEY: Lazy<String> = Lazy::new(|| format!("region{}", &*REGION_ID));
12-
13-
/// Compact presence information for a user
14-
#[derive(Serialize, Deserialize, Debug)]
15-
pub struct PresenceEntry {
16-
/// Region this session exists in
17-
///
18-
/// We can have up to 65535 regions
19-
pub region_id: u16,
20-
21-
/// Unique session ID
22-
pub session_id: u8,
23-
24-
/// Known flags about session
25-
pub flags: u8,
26-
}
27-
28-
impl PresenceEntry {
29-
/// Create a new presence entry from a given session ID and known flags
30-
pub fn from(session_id: u8, flags: u8) -> Self {
31-
Self {
32-
region_id: *REGION_ID,
33-
session_id,
34-
flags,
35-
}
36-
}
37-
}
38-
39-
pub trait PresenceOp {
40-
/// Find next available session ID
41-
fn find_next_id(&self) -> u8;
42-
}
43-
44-
impl PresenceOp for Vec<PresenceEntry> {
45-
fn find_next_id(&self) -> u8 {
46-
// O(n^2) scan algorithm
47-
// should be relatively fast at low numbers anyways
48-
for i in 0..255 {
49-
let mut found = false;
50-
for entry in self {
51-
if entry.session_id == i {
52-
found = true;
53-
break;
54-
}
55-
}
56-
57-
if !found {
58-
return i;
59-
}
60-
}
61-
62-
255
63-
}
64-
}

crates/quark/src/presence/mod.rs

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,77 @@ use std::collections::HashSet;
22

33
use redis_kiss::{get_connection, AsyncCommands};
44

5+
use rand::Rng;
56
mod entry;
67
mod operations;
78

8-
use entry::{PresenceEntry, PresenceOp};
99
use operations::{
10-
__add_to_set_sessions, __delete_key_presence_entry, __get_key_presence_entry,
11-
__get_set_sessions, __remove_from_set_sessions, __set_key_presence_entry,
10+
__add_to_set_string, __add_to_set_u32, __delete_key, __get_set_members_as_string,
11+
__get_set_size, __remove_from_set_string, __remove_from_set_u32,
1212
};
1313

14-
use crate::presence::operations::__delete_set_sessions;
15-
1614
use self::entry::REGION_KEY;
1715

1816
/// Create a new presence session, returns the ID of this session
19-
pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u8) {
17+
pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u32) {
2018
info!("Creating a presence session for {user_id} with flags {flags}");
2119

22-
// Try to find the presence entry for this user.
23-
let mut conn = get_connection().await.unwrap();
24-
let mut entry: Vec<PresenceEntry> = __get_key_presence_entry(&mut conn, user_id)
25-
.await
26-
.unwrap_or_default();
20+
if let Ok(mut conn) = get_connection().await {
21+
// Check whether this is the first session
22+
let was_empty = __get_set_size(&mut conn, user_id).await == 0;
2723

28-
// Return whether this was the first session.
29-
let was_empty = entry.is_empty();
30-
info!("User ID {} just came online.", &user_id);
24+
// A session ID is comprised of random data and any flags ORed to the end
25+
let session_id = {
26+
let mut rng = rand::thread_rng();
27+
(rng.gen::<u32>() ^ 1) | (flags as u32 & 1)
28+
};
3129

32-
// Generate session ID and push new entry.
33-
let session_id = entry.find_next_id();
34-
entry.push(PresenceEntry::from(session_id, flags));
35-
__set_key_presence_entry(&mut conn, user_id, entry).await;
30+
// Add session to user's sessions and to the region
31+
__add_to_set_u32(&mut conn, user_id, session_id).await;
32+
__add_to_set_string(&mut conn, &REGION_KEY, &format!("{user_id}:{session_id}")).await;
33+
info!("Created session for {user_id}, assigned them a session ID of {session_id}.");
3634

37-
// Add to region set in case of failure.
38-
__add_to_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
39-
(was_empty, session_id)
35+
(was_empty, session_id)
36+
} else {
37+
// Fail through
38+
(false, 0)
39+
}
4040
}
4141

4242
/// Delete existing presence session
43-
pub async fn presence_delete_session(user_id: &str, session_id: u8) -> bool {
43+
pub async fn presence_delete_session(user_id: &str, session_id: u32) -> bool {
4444
presence_delete_session_internal(user_id, session_id, false).await
4545
}
4646

4747
/// Delete existing presence session (but also choose whether to skip region)
4848
async fn presence_delete_session_internal(
4949
user_id: &str,
50-
session_id: u8,
50+
session_id: u32,
5151
skip_region: bool,
5252
) -> bool {
5353
info!("Deleting presence session for {user_id} with id {session_id}");
5454

55-
// Return whether this was the last session.
56-
let mut is_empty = false;
57-
58-
// Only continue if we can actually find one.
59-
let mut conn = get_connection().await.unwrap();
60-
let entry: Option<Vec<PresenceEntry>> = __get_key_presence_entry(&mut conn, user_id).await;
61-
if let Some(entry) = entry {
62-
let entries = entry
63-
.into_iter()
64-
.filter(|x| x.session_id != session_id)
65-
.collect::<Vec<PresenceEntry>>();
66-
67-
// If entry is empty, then just delete it.
68-
if entries.is_empty() {
69-
__delete_key_presence_entry(&mut conn, user_id).await;
70-
is_empty = true;
71-
} else {
72-
__set_key_presence_entry(&mut conn, user_id, entries).await;
73-
}
55+
if let Ok(mut conn) = get_connection().await {
56+
// Remove the session
57+
__remove_from_set_u32(&mut conn, user_id, session_id).await;
7458

75-
// Remove from region set.
59+
// Remove from the region
7660
if !skip_region {
77-
__remove_from_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
61+
__remove_from_set_string(&mut conn, &REGION_KEY, &format!("{user_id}:{session_id}"))
62+
.await;
7863
}
79-
}
8064

81-
if is_empty {
82-
info!("User ID {} just went offline.", &user_id);
83-
}
65+
// Return whether this was the last session
66+
let is_empty = __get_set_size(&mut conn, user_id).await == 0;
67+
if is_empty {
68+
info!("User ID {} just went offline.", &user_id);
69+
}
8470

85-
is_empty
71+
is_empty
72+
} else {
73+
// Fail through
74+
false
75+
}
8676
}
8777

8878
/// Check whether a given user ID is online
@@ -102,6 +92,10 @@ pub async fn presence_filter_online(user_ids: &'_ [String]) -> HashSet<String> {
10292
return set;
10393
}
10494

95+
// NOTE: at the point that we need mobile indicators
96+
// you can interpret the data here and return a new data
97+
// structure like HashMap<String /* id */, u8 /* flags */>
98+
10599
// We need to handle a special case where only one is present
106100
// as for some reason or another, Redis does not like us sending
107101
// a list of just one ID to the server.
@@ -136,7 +130,7 @@ pub async fn presence_clear_region(region_id: Option<&str>) {
136130
let region_id = region_id.unwrap_or(&*REGION_KEY);
137131
let mut conn = get_connection().await.expect("Redis connection");
138132

139-
let sessions = __get_set_sessions(&mut conn, region_id).await;
133+
let sessions = __get_set_members_as_string(&mut conn, region_id).await;
140134
if !sessions.is_empty() {
141135
info!(
142136
"Cleaning up {} sessions, this may take a while...",
@@ -155,7 +149,7 @@ pub async fn presence_clear_region(region_id: Option<&str>) {
155149
}
156150

157151
// Then clear the set in Redis.
158-
__delete_set_sessions(&mut conn, region_id).await;
152+
__delete_key(&mut conn, region_id).await;
159153

160154
info!("Clean up complete.");
161155
}
Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,42 @@
11
use redis_kiss::{AsyncCommands, Conn};
22

3-
use super::entry::PresenceEntry;
4-
5-
/// Set presence entry by given ID
6-
pub async fn __set_key_presence_entry(conn: &mut Conn, id: &str, data: Vec<PresenceEntry>) {
7-
let _: Option<()> = conn.set(id, bincode::serialize(&data).unwrap()).await.ok();
3+
/// Add to set (string)
4+
pub async fn __add_to_set_string(conn: &mut Conn, key: &str, value: &str) {
5+
let _: Option<()> = conn.sadd(key, value).await.ok();
86
}
97

10-
/// Delete presence entry by given ID
11-
pub async fn __delete_key_presence_entry(conn: &mut Conn, id: &str) {
12-
let _: Option<()> = conn.del(id).await.ok();
8+
/// Add to set (u32)
9+
pub async fn __add_to_set_u32(conn: &mut Conn, key: &str, value: u32) {
10+
let _: Option<()> = conn.sadd(key, value).await.ok();
1311
}
1412

15-
/// Get presence entry by given ID
16-
pub async fn __get_key_presence_entry(conn: &mut Conn, id: &str) -> Option<Vec<PresenceEntry>> {
17-
conn.get::<_, Option<Vec<u8>>>(id)
18-
.await
19-
.unwrap()
20-
.map(|entry| bincode::deserialize(&entry[..]).unwrap())
13+
/// Remove from set (string)
14+
pub async fn __remove_from_set_string(conn: &mut Conn, key: &str, value: &str) {
15+
let _: Option<()> = conn.srem(key, value).await.ok();
2116
}
2217

23-
/// Add to region session set
24-
pub async fn __add_to_set_sessions(
25-
conn: &mut Conn,
26-
region_id: &str,
27-
user_id: &str,
28-
session_id: u8,
29-
) {
30-
let _: Option<()> = conn
31-
.sadd(region_id, format!("{user_id}:{session_id}"))
32-
.await
33-
.ok();
18+
/// Remove from set (u32)
19+
pub async fn __remove_from_set_u32(conn: &mut Conn, key: &str, value: u32) {
20+
let _: Option<()> = conn.srem(key, value).await.ok();
3421
}
3522

36-
/// Remove from region session set
37-
pub async fn __remove_from_set_sessions(
38-
conn: &mut Conn,
39-
region_id: &str,
40-
user_id: &str,
41-
session_id: u8,
42-
) {
43-
let _: Option<()> = conn
44-
.srem(region_id, format!("{user_id}:{session_id}"))
23+
/// Get set members as string
24+
pub async fn __get_set_members_as_string(conn: &mut Conn, key: &str) -> Vec<String> {
25+
conn.smembers::<_, Vec<String>>(key)
4526
.await
46-
.ok();
27+
.expect("could not get set members as string")
4728
}
4829

49-
/// Get region session set as list
50-
pub async fn __get_set_sessions(conn: &mut Conn, region_id: &str) -> Vec<String> {
51-
conn.smembers::<_, Vec<String>>(region_id).await.unwrap()
30+
/// Get set size
31+
pub async fn __get_set_size(conn: &mut Conn, id: &str) -> u32 {
32+
conn.scard::<_, u32>(id)
33+
.await
34+
.expect("could not get set size")
5235
}
5336

54-
/// Delete region session set
55-
pub async fn __delete_set_sessions(conn: &mut Conn, region_id: &str) {
56-
conn.del::<_, ()>(region_id).await.unwrap();
37+
/// Delete key by id
38+
pub async fn __delete_key(conn: &mut Conn, id: &str) {
39+
conn.del::<_, ()>(id)
40+
.await
41+
.expect("could not delete key by id");
5742
}

docker-compose.db.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
version: "3.3"
22
services:
3+
# Redis
4+
redis:
5+
image: eqalpha/keydb
6+
ports:
7+
- "6379:6379"
8+
39
# MongoDB
410
database:
511
image: mongo

0 commit comments

Comments
 (0)