chore: add test for connect state (#414)
* chore: add test * chore: add test * chore: disable redis test
This commit is contained in:
parent
4878d51c1b
commit
d0c0d7832c
|
|
@ -1509,6 +1509,7 @@ dependencies = [
|
||||||
"md5",
|
"md5",
|
||||||
"parking_lot 0.12.1",
|
"parking_lot 0.12.1",
|
||||||
"prometheus-client",
|
"prometheus-client",
|
||||||
|
"rand 0.8.5",
|
||||||
"redis 0.25.2",
|
"redis 0.25.2",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ pub struct RealtimeUser {
|
||||||
pub uid: i64,
|
pub uid: i64,
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
pub connect_at: i64,
|
pub connect_at: i64,
|
||||||
|
/// Represents the websocket connection session id.
|
||||||
|
/// When each websocket connection is established, a unique session id is generated.
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,6 @@ indexmap = "2.2.5"
|
||||||
semver = "1.0.22"
|
semver = "1.0.22"
|
||||||
redis = "0.25.2"
|
redis = "0.25.2"
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
@ -3,6 +3,7 @@ use collab_rt_entity::message::{RealtimeMessage, SystemMessage};
|
||||||
use collab_rt_entity::user::{RealtimeUser, UserDevice};
|
use collab_rt_entity::user::{RealtimeUser, UserDevice};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
|
||||||
|
use dashmap::mapref::entry::Entry;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
|
|
||||||
|
|
@ -19,37 +20,58 @@ impl ConnectState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a new user connection, updating the connection state accordingly.
|
||||||
|
///
|
||||||
|
/// This function checks if there is already a connection from the same user device. If an existing
|
||||||
|
/// connection is found and the new connection is more recent (`connect_at` is greater), the old connection
|
||||||
|
/// is replaced with the new one. This process involves:
|
||||||
|
///
|
||||||
|
/// - Removing the old user's client stream, if present, and sending a `DuplicateConnection` system message.
|
||||||
|
/// - Inserting the new user connection into the `user_by_device` and `client_stream_by_user` maps.
|
||||||
|
///
|
||||||
pub fn handle_user_connect(
|
pub fn handle_user_connect(
|
||||||
&self,
|
&self,
|
||||||
new_user: RealtimeUser,
|
new_user: RealtimeUser,
|
||||||
client_stream: CollabClientStream,
|
client_stream: CollabClientStream,
|
||||||
) -> Option<RealtimeUser> {
|
) -> Option<RealtimeUser> {
|
||||||
let old_user = self
|
let user_device = UserDevice::from(&new_user);
|
||||||
.user_by_device
|
let entry = self.user_by_device.entry(user_device);
|
||||||
.insert(UserDevice::from(&new_user), new_user.clone());
|
match entry {
|
||||||
|
Entry::Occupied(mut e) => {
|
||||||
trace!(
|
if e.get().connect_at <= new_user.connect_at {
|
||||||
"[realtime]: new connection => {}, removing old: {:?}",
|
let old_user = e.insert(new_user.clone());
|
||||||
new_user,
|
trace!("[realtime]: new connection replaces old => {}", new_user);
|
||||||
old_user
|
if let Some((_, old_stream)) = self.client_stream_by_user.remove(&old_user) {
|
||||||
|
info!(
|
||||||
|
"Removing old stream for same user and device: {}",
|
||||||
|
old_user.uid
|
||||||
);
|
);
|
||||||
|
old_stream
|
||||||
if let Some(old_user) = &old_user {
|
|
||||||
// Remove and retrieve the old client stream if it exists.
|
|
||||||
if let Some((_, client_stream)) = self.client_stream_by_user.remove(old_user) {
|
|
||||||
info!("Removing old stream for same user and device: {}", old_user);
|
|
||||||
// Notify the old stream of the duplicate connection.
|
|
||||||
client_stream
|
|
||||||
.sink
|
.sink
|
||||||
.do_send(RealtimeMessage::System(SystemMessage::DuplicateConnection));
|
.do_send(RealtimeMessage::System(SystemMessage::DuplicateConnection));
|
||||||
}
|
}
|
||||||
// Remove the old user from all collaboration groups.
|
|
||||||
}
|
|
||||||
self.client_stream_by_user.insert(new_user, client_stream);
|
self.client_stream_by_user.insert(new_user, client_stream);
|
||||||
|
Some(old_user)
|
||||||
old_user
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
trace!("[realtime]: new connection => {}", new_user);
|
||||||
|
e.insert(new_user.clone());
|
||||||
|
self.client_stream_by_user.insert(new_user, client_stream);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles the disconnection of a user from the system.
|
||||||
|
///
|
||||||
|
/// remove a user based on their device and session ID. If the session ID of the disconnecting user matches
|
||||||
|
/// the session ID stored in the system for that device, the user is removed. Additionally, it also
|
||||||
|
/// attempts to remove the associated client stream for the disconnecting user.
|
||||||
|
///
|
||||||
pub fn handle_user_disconnect(
|
pub fn handle_user_disconnect(
|
||||||
&self,
|
&self,
|
||||||
disconnect_user: &RealtimeUser,
|
disconnect_user: &RealtimeUser,
|
||||||
|
|
@ -68,11 +90,6 @@ impl ConnectState {
|
||||||
was_removed
|
was_removed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn num_connected_users(&self) -> usize {
|
|
||||||
self.user_by_device.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn get_user_by_device(&self, user_device: &UserDevice) -> Option<RealtimeUser> {
|
fn get_user_by_device(&self, user_device: &UserDevice) -> Option<RealtimeUser> {
|
||||||
self.user_by_device.get(user_device).map(|v| v.clone())
|
self.user_by_device.get(user_device).map(|v| v.clone())
|
||||||
|
|
@ -85,6 +102,8 @@ mod tests {
|
||||||
use crate::{CollabClientStream, RealtimeClientWebsocketSink};
|
use crate::{CollabClientStream, RealtimeClientWebsocketSink};
|
||||||
use collab_rt_entity::message::RealtimeMessage;
|
use collab_rt_entity::message::RealtimeMessage;
|
||||||
use collab_rt_entity::user::{RealtimeUser, UserDevice};
|
use collab_rt_entity::user::{RealtimeUser, UserDevice};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
struct MockSink;
|
struct MockSink;
|
||||||
|
|
||||||
|
|
@ -92,12 +111,12 @@ mod tests {
|
||||||
fn do_send(&self, _message: RealtimeMessage) {}
|
fn do_send(&self, _message: RealtimeMessage) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mock_user(uid: i64, device_id: &str) -> RealtimeUser {
|
fn mock_user(uid: i64, device_id: &str, connect_at: i64) -> RealtimeUser {
|
||||||
RealtimeUser::new(
|
RealtimeUser::new(
|
||||||
uid,
|
uid,
|
||||||
device_id.to_string(),
|
device_id.to_string(),
|
||||||
uuid::Uuid::new_v4().to_string(),
|
uuid::Uuid::new_v4().to_string(),
|
||||||
chrono::Utc::now().timestamp(),
|
connect_at,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,43 +127,99 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn same_user_different_device_connect_test() {
|
async fn same_user_different_device_connect_test() {
|
||||||
let connect_state = ConnectState::new();
|
let connect_state = ConnectState::new();
|
||||||
let user_device_a = mock_user(1, "device_a");
|
let user_device_a = mock_user(1, "device_a", 1);
|
||||||
let user_device_b = mock_user(1, "device_b");
|
let user_device_b = mock_user(1, "device_b", 1);
|
||||||
connect_state.handle_user_connect(user_device_a, mock_stream());
|
connect_state.handle_user_connect(user_device_a, mock_stream());
|
||||||
connect_state.handle_user_connect(user_device_b, mock_stream());
|
connect_state.handle_user_connect(user_device_b, mock_stream());
|
||||||
|
|
||||||
assert_eq!(connect_state.num_connected_users(), 2);
|
assert_eq!(connect_state.user_by_device.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn same_user_same_device_connect_test() {
|
async fn same_user_same_device_connect_test() {
|
||||||
let connect_state = ConnectState::new();
|
let connect_state = ConnectState::new();
|
||||||
let user_device_a = mock_user(1, "device_a");
|
let user_device_a = mock_user(1, "device_a", 1);
|
||||||
let user_device_b = mock_user(1, "device_a");
|
let user_device_b = mock_user(1, "device_a", 1);
|
||||||
connect_state.handle_user_connect(user_device_a, mock_stream());
|
connect_state.handle_user_connect(user_device_a, mock_stream());
|
||||||
connect_state.handle_user_connect(user_device_b.clone(), mock_stream());
|
connect_state.handle_user_connect(user_device_b.clone(), mock_stream());
|
||||||
|
|
||||||
assert_eq!(connect_state.num_connected_users(), 1);
|
assert_eq!(connect_state.user_by_device.len(), 1);
|
||||||
let user = connect_state
|
let user = connect_state
|
||||||
.get_user_by_device(&UserDevice::from(&user_device_b))
|
.get_user_by_device(&UserDevice::from(&user_device_b))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(user, user_device_b);
|
assert_eq!(user, user_device_b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_same_devices_connect_test() {
|
||||||
|
let connect_state = ConnectState::new();
|
||||||
|
let mut handles = vec![];
|
||||||
|
for i in 0..1000 {
|
||||||
|
let cloned_connect_state = connect_state.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let random_seconds = rand::random::<u64>() % 500;
|
||||||
|
sleep(Duration::from_millis(random_seconds)).await;
|
||||||
|
let user = mock_user(1, "device_a", i);
|
||||||
|
cloned_connect_state.handle_user_connect(user, mock_stream());
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = futures::future::join_all(handles).await;
|
||||||
|
let user = connect_state
|
||||||
|
.get_user_by_device(&UserDevice::new("device_a", 1))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(connect_state.user_by_device.len(), 1);
|
||||||
|
assert_eq!(connect_state.client_stream_by_user.len(), 1);
|
||||||
|
assert_eq!(user.connect_at, 999);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_same_devices_connect_disconnect_test() {
|
||||||
|
let connect_state = ConnectState::new();
|
||||||
|
let mut handles = vec![];
|
||||||
|
for i in 0..2000 {
|
||||||
|
let should_disconnect = i % 2 == 0;
|
||||||
|
let cloned_connect_state = connect_state.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let random_seconds = rand::random::<u64>() % 500;
|
||||||
|
sleep(Duration::from_millis(random_seconds)).await;
|
||||||
|
let user = mock_user(1, "device_a", i);
|
||||||
|
|
||||||
|
if should_disconnect {
|
||||||
|
cloned_connect_state.handle_user_disconnect(&user);
|
||||||
|
} else {
|
||||||
|
cloned_connect_state.handle_user_connect(user, mock_stream());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = futures::future::join_all(handles).await;
|
||||||
|
let user = connect_state
|
||||||
|
.get_user_by_device(&UserDevice::new("device_a", 1))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(connect_state.user_by_device.len(), 1);
|
||||||
|
assert_eq!(connect_state.client_stream_by_user.len(), 1);
|
||||||
|
assert_eq!(user.connect_at, 1999);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn multiple_devices_connect_test() {
|
async fn multiple_devices_connect_test() {
|
||||||
let user_a = vec![
|
let user_a = vec![
|
||||||
mock_user(1, "device_a"),
|
mock_user(1, "device_a", 1),
|
||||||
mock_user(1, "device_b"),
|
mock_user(1, "device_b", 2),
|
||||||
mock_user(1, "device_c"),
|
mock_user(1, "device_c", 1),
|
||||||
mock_user(1, "device_d"),
|
mock_user(1, "device_d", 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let user_b = vec![
|
let user_b = vec![
|
||||||
mock_user(2, "device_a"),
|
mock_user(2, "device_a", 1),
|
||||||
mock_user(2, "device_b"),
|
// device_b starts two connections, last connection should be kept.
|
||||||
mock_user(2, "device_b"),
|
mock_user(2, "device_b", 1),
|
||||||
mock_user(2, "device_a"),
|
mock_user(2, "device_b", 2),
|
||||||
|
mock_user(2, "device_a", 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let connect_state = ConnectState::new();
|
let connect_state = ConnectState::new();
|
||||||
|
|
@ -160,15 +235,21 @@ mod tests {
|
||||||
|
|
||||||
let (tx, rx_2) = tokio::sync::oneshot::channel();
|
let (tx, rx_2) = tokio::sync::oneshot::channel();
|
||||||
let cloned_connect_state = connect_state.clone();
|
let cloned_connect_state = connect_state.clone();
|
||||||
|
let clone_user_b = user_b.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for user in user_b {
|
for user in clone_user_b {
|
||||||
cloned_connect_state.handle_user_connect(user, mock_stream());
|
cloned_connect_state.handle_user_connect(user, mock_stream());
|
||||||
}
|
}
|
||||||
tx.send(()).unwrap();
|
tx.send(()).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = futures::future::join(rx_1, rx_2).await;
|
let _ = futures::future::join(rx_1, rx_2).await;
|
||||||
|
assert_eq!(connect_state.user_by_device.len(), 6);
|
||||||
|
|
||||||
assert_eq!(connect_state.num_connected_users(), 6);
|
// device_b with connect_at 2 should be kept.
|
||||||
|
let user = connect_state
|
||||||
|
.get_user_by_device(&UserDevice::from(&user_b[2]))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(user.connect_at, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use crate::collab::util::test_encode_collab_v1;
|
use crate::collab::util::test_encode_collab_v1;
|
||||||
use app_error::ErrorCode;
|
use app_error::ErrorCode;
|
||||||
use client_api_test_util::*;
|
use client_api_test_util::*;
|
||||||
|
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use database_entity::dto::{
|
use database_entity::dto::{
|
||||||
CreateCollabParams, DeleteCollabParams, QueryCollab, QueryCollabParams, QueryCollabResult,
|
CreateCollabParams, DeleteCollabParams, QueryCollab, QueryCollabParams, QueryCollabResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sqlx::types::Uuid;
|
use sqlx::types::Uuid;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue