feat: ws reconnect (#5)

* feat: ws reconnect

* chore: update collab rev
This commit is contained in:
Nathan.fooo 2023-05-10 20:54:10 +08:00 committed by GitHub
parent 90ae1d5fb6
commit 8b9e6584d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 83 additions and 40 deletions

5
Cargo.lock generated
View File

@ -824,7 +824,6 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af4941#af4941ba5394157869eca56d4c937dbec1f0a0e3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -841,7 +840,6 @@ dependencies = [
[[package]] [[package]]
name = "collab-client-ws" name = "collab-client-ws"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af4941#af4941ba5394157869eca56d4c937dbec1f0a0e3"
dependencies = [ dependencies = [
"bytes", "bytes",
"collab-sync", "collab-sync",
@ -859,7 +857,6 @@ dependencies = [
[[package]] [[package]]
name = "collab-persistence" name = "collab-persistence"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af4941#af4941ba5394157869eca56d4c937dbec1f0a0e3"
dependencies = [ dependencies = [
"bincode", "bincode",
"chrono", "chrono",
@ -879,7 +876,6 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af4941#af4941ba5394157869eca56d4c937dbec1f0a0e3"
dependencies = [ dependencies = [
"collab", "collab",
"collab-client-ws", "collab-client-ws",
@ -895,7 +891,6 @@ dependencies = [
[[package]] [[package]]
name = "collab-sync" name = "collab-sync"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=af4941#af4941ba5394157869eca56d4c937dbec1f0a0e3"
dependencies = [ dependencies = [
"bytes", "bytes",
"collab", "collab",

View File

@ -88,11 +88,11 @@ members = [
] ]
[patch.crates-io] [patch.crates-io]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af4941" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ad656c" }
collab-client-ws = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af4941" } collab-client-ws = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ad656c" }
collab-sync= { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af4941" } collab-sync= { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ad656c" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af4941" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ad656c" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "af4941" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ad656c" }
#collab = { path = "./crates/AppFlowy-Collab/collab" } #collab = { path = "./crates/AppFlowy-Collab/collab" }
#collab-client-ws = { path = "./crates/AppFlowy-Collab/collab-client-ws" } #collab-client-ws = { path = "./crates/AppFlowy-Collab/collab-client-ws" }

View File

@ -10,4 +10,7 @@ database:
username: "postgres" username: "postgres"
password: "password" password: "password"
database_name: "appflowy" database_name: "appflowy"
websocket:
heartbeat_interval: 8
client_timeout: 10
redis_uri: "redis://127.0.0.1:6379" redis_uri: "redis://127.0.0.1:6379"

View File

@ -10,32 +10,36 @@ use bytes::Bytes;
use std::ops::Deref; use std::ops::Deref;
use collab_plugins::sync::msg::CollabMessage; use collab_plugins::sync::msg::CollabMessage;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct CollabSession { pub struct CollabSession {
user: Arc<WSUser>, user: Arc<WSUser>,
hb: Instant, hb: Instant,
pub server: Addr<CollabServer>, pub server: Addr<CollabServer>,
heartbeat_interval: Duration,
client_timeout: Duration,
} }
impl CollabSession { impl CollabSession {
pub fn new(user: WSUser, server: Addr<CollabServer>) -> Self { pub fn new(
user: WSUser,
server: Addr<CollabServer>,
heartbeat_interval: Duration,
client_timeout: Duration,
) -> Self {
Self { Self {
user: Arc::new(user), user: Arc::new(user),
hb: Instant::now(), hb: Instant::now(),
server, server,
heartbeat_interval,
client_timeout,
} }
} }
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) { fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { ctx.run_interval(self.heartbeat_interval, |act, ctx| {
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { if Instant::now().duration_since(act.hb) > act.client_timeout {
act.server.do_send(Disconnect { act.server.do_send(Disconnect {
user: act.user.clone(), user: act.user.clone(),
}); });

View File

@ -30,7 +30,9 @@ pub struct CollabServer {
collab_id_gen: Arc<Mutex<CollabIDGen>>, collab_id_gen: Arc<Mutex<CollabIDGen>>,
/// Memory cache for fast lookup of collab_id from object_id /// Memory cache for fast lookup of collab_id from object_id
collab_id_by_object_id: Arc<DashMap<String, CollabId>>, collab_id_by_object_id: Arc<DashMap<String, CollabId>>,
/// Keep track of all collab groups
collab_groups: Arc<RwLock<HashMap<CollabId, CollabGroup>>>, collab_groups: Arc<RwLock<HashMap<CollabId, CollabGroup>>>,
/// Keep track of all client streams
client_streams: Arc<RwLock<HashMap<Arc<WSUser>, WSClientStream>>>, client_streams: Arc<RwLock<HashMap<Arc<WSUser>, WSClientStream>>>,
} }

View File

@ -5,6 +5,7 @@ use actix_web::web::{Data, Path, Payload};
use actix_web::{get, web, HttpRequest, HttpResponse, Result, Scope}; use actix_web::{get, web, HttpRequest, HttpResponse, Result, Scope};
use actix_web_actors::ws; use actix_web_actors::ws;
use secrecy::Secret; use secrecy::Secret;
use std::time::Duration;
use websocket::entities::WSUser; use websocket::entities::WSUser;
use websocket::{CollabServer, CollabSession}; use websocket::{CollabServer, CollabSession};
@ -23,7 +24,12 @@ pub async fn establish_ws_connection(
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
tracing::trace!("{:?}", request); tracing::trace!("{:?}", request);
let user = LoggedUser::from_token(&state.config.application.server_key, token.as_str())?; let user = LoggedUser::from_token(&state.config.application.server_key, token.as_str())?;
let client = CollabSession::new(user.into(), server.get_ref().clone()); let client = CollabSession::new(
user.into(),
server.get_ref().clone(),
Duration::from_secs(state.config.websocket.heartbeat_interval as u64),
Duration::from_secs(state.config.websocket.client_timeout as u64),
);
match ws::start(client, &request, payload) { match ws::start(client, &request, payload) {
Ok(response) => Ok(response), Ok(response) => Ok(response),
Err(e) => { Err(e) => {

View File

@ -20,6 +20,7 @@ use snowflake::Snowflake;
use sqlx::{postgres::PgPoolOptions, PgPool}; use sqlx::{postgres::PgPoolOptions, PgPool};
use std::net::TcpListener; use std::net::TcpListener;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing_actix_web::TracingLogger; use tracing_actix_web::TracingLogger;
@ -69,7 +70,7 @@ pub async fn run(
.map(|(_, server_key)| Key::from(server_key.expose_secret().as_bytes())) .map(|(_, server_key)| Key::from(server_key.expose_secret().as_bytes()))
.unwrap_or_else(Key::generate); .unwrap_or_else(Key::generate);
let collab_server = CollabServer::new(state.rocksdb.clone()).unwrap().start(); let collab_server_addr = CollabServer::new(state.rocksdb.clone()).unwrap().start();
let mut server = HttpServer::new(move || { let mut server = HttpServer::new(move || {
App::new() App::new()
.wrap( .wrap(
@ -84,7 +85,7 @@ pub async fn run(
.app_data(web::JsonConfig::default().limit(4096)) .app_data(web::JsonConfig::default().limit(4096))
.service(user_scope()) .service(user_scope())
.service(ws_scope()) .service(ws_scope())
.app_data(Data::new(collab_server.clone())) .app_data(Data::new(collab_server_addr.clone()))
.app_data(Data::new(state.clone())) .app_data(Data::new(state.clone()))
}); });

View File

@ -8,7 +8,8 @@ use std::path::PathBuf;
#[derive(serde::Deserialize, Clone, Debug)] #[derive(serde::Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub database: DatabaseSetting, pub database: DatabaseSetting,
pub application: ApplicationSettings, pub application: ApplicationSetting,
pub websocket: WebsocketSetting,
pub redis_uri: Secret<String>, pub redis_uri: Secret<String>,
} }
@ -21,7 +22,7 @@ pub struct Config {
// it to 0.0.0.0 in our Docker images. // it to 0.0.0.0 in our Docker images.
// //
#[derive(serde::Deserialize, Clone, Debug)] #[derive(serde::Deserialize, Clone, Debug)]
pub struct ApplicationSettings { pub struct ApplicationSetting {
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16, pub port: u16,
pub host: String, pub host: String,
@ -30,7 +31,7 @@ pub struct ApplicationSettings {
pub tls_config: Option<TlsConfig>, pub tls_config: Option<TlsConfig>,
} }
impl ApplicationSettings { impl ApplicationSetting {
pub fn use_https(&self) -> bool { pub fn use_https(&self) -> bool {
match &self.tls_config { match &self.tls_config {
None => false, None => false,
@ -89,7 +90,7 @@ pub fn get_configuration() -> Result<Config, config::ConfigError> {
let configuration_dir = base_path.join("configuration"); let configuration_dir = base_path.join("configuration");
let environment: Environment = std::env::var("APP_ENVIRONMENT") let environment: Environment = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "local".into()) .unwrap_or_else(|_| "local".to_string())
.try_into() .try_into()
.expect("Failed to parse APP_ENVIRONMENT."); .expect("Failed to parse APP_ENVIRONMENT.");
@ -143,3 +144,8 @@ impl TryFrom<String> for Environment {
} }
} }
} }
#[derive(serde::Deserialize, Clone, Debug)]
pub struct WebsocketSetting {
pub heartbeat_interval: u8,
pub client_timeout: u8,
}

View File

@ -1,5 +1,5 @@
use crate::util::{spawn_server, TestUser}; use crate::util::{spawn_server, TestUser};
use collab_client_ws::WSClient; use collab_client_ws::{WSClient, WSClientConfig};
#[actix_rt::test] #[actix_rt::test]
async fn ws_conn_test() { async fn ws_conn_test() {
@ -8,6 +8,6 @@ async fn ws_conn_test() {
let token = test_user.register(&server).await; let token = test_user.register(&server).await;
let address = format!("{}/{}", server.ws_addr, token); let address = format!("{}/{}", server.ws_addr, token);
let client = WSClient::new(address, 100); let client = WSClient::new(address, WSClientConfig::default());
let _ = client.connect().await; let _ = client.connect().await;
} }

View File

@ -20,7 +20,7 @@ use sqlx::{Connection, Executor, PgConnection, PgPool};
// Ensure that the `tracing` stack is only initialised once using `once_cell` // Ensure that the `tracing` stack is only initialised once using `once_cell`
static TRACING: Lazy<()> = Lazy::new(|| { static TRACING: Lazy<()> = Lazy::new(|| {
let level = "debug".to_string(); let level = "trace".to_string();
let mut filters = vec![]; let mut filters = vec![];
filters.push(format!("appflowy_server={}", level)); filters.push(format!("appflowy_server={}", level));
filters.push(format!("collab_client_ws={}", level)); filters.push(format!("collab_client_ws={}", level));

View File

@ -1,7 +1,7 @@
use collab::core::collab::MutexCollab; use collab::core::collab::MutexCollab;
use collab::core::origin::{CollabClient, CollabOrigin}; use collab::core::origin::{CollabClient, CollabOrigin};
use collab_client_ws::{WSClient, WSMessageHandler}; use collab_client_ws::{WSBusinessHandler, WSClient, WSClientConfig};
use collab_plugins::disk::kv::rocks_kv::RocksCollabDB; use collab_plugins::disk::kv::rocks_kv::RocksCollabDB;
use collab_plugins::disk::rocksdb::RocksdbDiskPlugin; use collab_plugins::disk::rocksdb::RocksdbDiskPlugin;
use collab_plugins::sync::SyncPlugin; use collab_plugins::sync::SyncPlugin;
@ -9,6 +9,7 @@ use std::net::SocketAddr;
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir; use tempfile::TempDir;
pub async fn spawn_client( pub async fn spawn_client(
@ -16,10 +17,13 @@ pub async fn spawn_client(
object_id: &str, object_id: &str,
address: String, address: String,
) -> std::io::Result<TestClient> { ) -> std::io::Result<TestClient> {
let ws_client = WSClient::new(address, 100); let ws_client = WSClient::new(address, WSClientConfig::default());
let addr = ws_client.connect().await.unwrap().unwrap(); let addr = ws_client.connect().await.unwrap().unwrap();
let origin = origin_from_tcp_stream(&addr); let origin = origin_from_tcp_stream(&addr);
let handler = ws_client.subscribe("collab".to_string()).await.unwrap(); let handler = ws_client
.subscribe_business("collab".to_string())
.await
.unwrap();
// //
let (sink, stream) = (handler.sink(), handler.stream()); let (sink, stream) = (handler.sink(), handler.stream());
@ -60,7 +64,7 @@ pub struct TestClient {
cleaner: Cleaner, cleaner: Cleaner,
#[allow(dead_code)] #[allow(dead_code)]
handlers: Vec<Arc<WSMessageHandler>>, handlers: Vec<Arc<WSBusinessHandler>>,
} }
struct Cleaner(PathBuf); struct Cleaner(PathBuf);
@ -88,3 +92,7 @@ impl Deref for TestClient {
&self.collab &self.collab
} }
} }
pub async fn wait(secs: u64) {
tokio::time::sleep(Duration::from_secs(secs)).await;
}

View File

@ -1,2 +1,3 @@
mod client; mod client;
mod test; mod test;
mod ws_reconnect;

View File

@ -1,7 +1,6 @@
use crate::util::{spawn_server, TestUser}; use crate::util::{spawn_server, TestUser};
use crate::ws::client::spawn_client; use crate::ws::client::{spawn_client, wait};
use serde_json::json; use serde_json::json;
use std::time::Duration;
#[actix_rt::test] #[actix_rt::test]
async fn ws_conn_test() { async fn ws_conn_test() {
@ -11,12 +10,12 @@ async fn ws_conn_test() {
let address = format!("{}/{}", server.ws_addr, token); let address = format!("{}/{}", server.ws_addr, token);
let client = spawn_client(1, "1", address).await.unwrap(); let client = spawn_client(1, "1", address).await.unwrap();
wait_a_sec().await; wait(1).await;
{ {
let collab = client.lock(); let collab = client.lock();
collab.insert("1", "a"); collab.insert("1", "a");
} }
wait_a_sec().await; wait(1).await;
let value = server.get_doc("1"); let value = server.get_doc("1");
assert_json_diff::assert_json_eq!( assert_json_diff::assert_json_eq!(
@ -26,7 +25,3 @@ async fn ws_conn_test() {
}) })
); );
} }
async fn wait_a_sec() {
tokio::time::sleep(Duration::from_secs(2)).await;
}

22
tests/ws/ws_reconnect.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::util::{spawn_server, TestUser};
use collab_client_ws::{WSClient, WSClientConfig};
#[actix_rt::test]
async fn ws_retry_connect() {
let server = spawn_server().await;
let test_user = TestUser::generate();
let token = test_user.register(&server).await;
let address = format!("{}/{}", server.ws_addr, token);
let ws_client = WSClient::new(
address,
WSClientConfig {
buffer_capacity: 100,
ping_per_secs: 2,
retry_connect_per_pings: 5,
},
);
let _addr = ws_client.connect().await.unwrap().unwrap();
// wait(20).await;
}