chore: using redis conn for each action (#476)
* chore: batch insert * chore: opti redis conn * chore: fix test
This commit is contained in:
parent
585bd83f1c
commit
4168d72390
|
|
@ -250,13 +250,20 @@ impl EditState {
|
|||
self.edit_counter.load(Ordering::SeqCst) != self.prev_edit_count.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub(crate) fn is_new(&self) -> bool {
|
||||
self.is_new.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub(crate) fn set_is_new(&self, is_new: bool) {
|
||||
self.is_new.store(is_new, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn should_save_to_disk(&self) -> bool {
|
||||
let current_edit_count = self.edit_counter.load(Ordering::SeqCst);
|
||||
let prev_edit_count = self.prev_edit_count.load(Ordering::SeqCst);
|
||||
|
||||
// If the collab is new, save it to disk and reset the flag
|
||||
if self.is_new.load(Ordering::SeqCst) {
|
||||
self.is_new.store(false, Ordering::SeqCst);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{interval, sleep};
|
||||
use tracing::{trace, warn};
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
pub(crate) struct GroupPersistence<S> {
|
||||
workspace_id: String,
|
||||
|
|
@ -69,28 +69,38 @@ where
|
|||
}
|
||||
|
||||
async fn force_save(&self) {
|
||||
if self.edit_state.is_new() && self.save(true).await.is_ok() {
|
||||
self.edit_state.set_is_new(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.edit_state.is_edit() {
|
||||
trace!("skip force save collab to disk: {}", self.object_id);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.save().await {
|
||||
|
||||
if let Err(err) = self.save(false).await {
|
||||
warn!("fail to force save: {}:{:?}", self.object_id, err);
|
||||
}
|
||||
}
|
||||
|
||||
/// return true if the collab has been dropped. Otherwise, return false
|
||||
async fn attempt_save(&self) -> Result<(), AppError> {
|
||||
if self.edit_state.is_new() && self.save(true).await.is_ok() {
|
||||
self.edit_state.set_is_new(false);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if conditions for saving to disk are not met
|
||||
if !self.edit_state.should_save_to_disk() {
|
||||
trace!("skip save collab to disk: {}", self.object_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.save().await?;
|
||||
self.save(false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save(&self) -> Result<(), AppError> {
|
||||
async fn save(&self, write_immediately: bool) -> Result<(), AppError> {
|
||||
let mutex_collab = self.collab.clone();
|
||||
let object_id = self.object_id.clone();
|
||||
let collab_type = self.collab_type.clone();
|
||||
|
|
@ -111,11 +121,11 @@ where
|
|||
Ok(Some(params)) => {
|
||||
match self
|
||||
.storage
|
||||
.insert_or_update_collab(&self.workspace_id, &self.uid, params)
|
||||
.insert_or_update_collab(&self.workspace_id, &self.uid, params, write_immediately)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
trace!("[realtime] save collab to disk: {}", self.object_id);
|
||||
trace!("[realtime] did save collab to disk: {}", self.object_id);
|
||||
// Update the edit state on successful save
|
||||
self.edit_state.tick();
|
||||
},
|
||||
|
|
@ -125,7 +135,15 @@ where
|
|||
Ok(None) => {
|
||||
// required lock failed or get encode collab failed, skip saving
|
||||
},
|
||||
Err(err) => warn!("attempt to encode collab {}=>{:?}", self.object_id, err),
|
||||
Err(err) => {
|
||||
if err.is_panic() {
|
||||
// reason:
|
||||
// 1. Couldn't get item's parent
|
||||
warn!("encode collab panic:{}=>{:?}", self.object_id, err);
|
||||
} else {
|
||||
error!("fail to spawn a task to get encode collab: {:?}", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use database::collab::CollabStorage;
|
||||
use prometheus_client::metrics::gauge::Gauge;
|
||||
use prometheus_client::registry::Registry;
|
||||
use std::sync::atomic::{AtomicI64, AtomicU64};
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
|
|
@ -9,14 +9,13 @@ use tokio::time::interval;
|
|||
#[derive(Clone)]
|
||||
pub struct CollabRealtimeMetrics {
|
||||
connected_users: Gauge,
|
||||
encode_collab_mem_hit_rate: Gauge<f64, AtomicU64>,
|
||||
total_success_get_encode_collab_from_redis: Gauge,
|
||||
total_attempt_get_encode_collab_from_redis: Gauge,
|
||||
opening_collab_count: Gauge,
|
||||
|
||||
/// The number of apply update
|
||||
apply_update_count: Gauge,
|
||||
/// The number of apply update failed
|
||||
apply_update_failed_count: Gauge,
|
||||
|
||||
acquire_collab_lock_count: Gauge,
|
||||
acquire_collab_lock_fail_count: Gauge,
|
||||
}
|
||||
|
|
@ -25,7 +24,8 @@ impl CollabRealtimeMetrics {
|
|||
fn init() -> Self {
|
||||
Self {
|
||||
connected_users: Gauge::default(),
|
||||
encode_collab_mem_hit_rate: Gauge::default(),
|
||||
total_success_get_encode_collab_from_redis: Gauge::default(),
|
||||
total_attempt_get_encode_collab_from_redis: Gauge::default(),
|
||||
opening_collab_count: Gauge::default(),
|
||||
apply_update_count: Default::default(),
|
||||
apply_update_failed_count: Default::default(),
|
||||
|
|
@ -43,9 +43,14 @@ impl CollabRealtimeMetrics {
|
|||
metrics.connected_users.clone(),
|
||||
);
|
||||
realtime_registry.register(
|
||||
"mem_hit_rate",
|
||||
"memory hit rate",
|
||||
metrics.encode_collab_mem_hit_rate.clone(),
|
||||
"total_success_get_encode_collab_from_redis",
|
||||
"total success get encode collab from redis",
|
||||
metrics.total_success_get_encode_collab_from_redis.clone(),
|
||||
);
|
||||
realtime_registry.register(
|
||||
"total_attempt_get_encode_collab_from_redis",
|
||||
"total attempt get encode collab from redis",
|
||||
metrics.total_attempt_get_encode_collab_from_redis.clone(),
|
||||
);
|
||||
realtime_registry.register(
|
||||
"opening_collab_count",
|
||||
|
|
@ -76,10 +81,6 @@ impl CollabRealtimeMetrics {
|
|||
|
||||
metrics
|
||||
}
|
||||
|
||||
pub fn record_encode_collab_mem_hit_rate(&self, rate: f64) {
|
||||
self.encode_collab_mem_hit_rate.set(rate);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
@ -146,7 +147,13 @@ pub(crate) fn spawn_metrics<S>(
|
|||
);
|
||||
|
||||
// cache hit rate
|
||||
metrics.record_encode_collab_mem_hit_rate(storage.encode_collab_mem_hit_rate());
|
||||
let (total, success) = storage.encode_collab_redis_query_state();
|
||||
metrics
|
||||
.total_attempt_get_encode_collab_from_redis
|
||||
.set(total as i64);
|
||||
metrics
|
||||
.total_success_get_encode_collab_from_redis
|
||||
.set(success as i64);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,16 +57,26 @@ pub trait CollabStorageAccessControl: Send + Sync + 'static {
|
|||
/// Implementors of this trait should provide the actual storage logic, be it in-memory, file-based, database-backed, etc.
|
||||
#[async_trait]
|
||||
pub trait CollabStorage: Send + Sync + 'static {
|
||||
fn encode_collab_mem_hit_rate(&self) -> f64;
|
||||
fn encode_collab_redis_query_state(&self) -> (u64, u64);
|
||||
|
||||
/// Insert/update the collaboration object in the storage.
|
||||
/// # Arguments
|
||||
/// * `workspace_id` - The ID of the workspace.
|
||||
/// * `uid` - The ID of the user.
|
||||
/// * `params` - The parameters containing the data of the collaboration.
|
||||
/// * `write_immediately` - A boolean value that indicates whether the data should be written immediately.
|
||||
/// if write_immediately is true, the data will be written to disk immediately. Otherwise, the data will
|
||||
/// be scheduled to be written to disk later.
|
||||
///
|
||||
async fn insert_or_update_collab(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
uid: &i64,
|
||||
params: CollabParams,
|
||||
write_immediately: bool,
|
||||
) -> AppResult<()>;
|
||||
|
||||
/// Insert/update a new collaboration in the storage.
|
||||
/// Insert a new collaboration in the storage.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
|
|
@ -136,8 +146,8 @@ impl<T> CollabStorage for Arc<T>
|
|||
where
|
||||
T: CollabStorage,
|
||||
{
|
||||
fn encode_collab_mem_hit_rate(&self) -> f64 {
|
||||
self.as_ref().encode_collab_mem_hit_rate()
|
||||
fn encode_collab_redis_query_state(&self) -> (u64, u64) {
|
||||
self.as_ref().encode_collab_redis_query_state()
|
||||
}
|
||||
|
||||
async fn insert_or_update_collab(
|
||||
|
|
@ -145,10 +155,11 @@ where
|
|||
workspace_id: &str,
|
||||
uid: &i64,
|
||||
params: CollabParams,
|
||||
write_immediately: bool,
|
||||
) -> AppResult<()> {
|
||||
self
|
||||
.as_ref()
|
||||
.insert_or_update_collab(workspace_id, uid, params)
|
||||
.insert_or_update_collab(workspace_id, uid, params, write_immediately)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ pub async fn run_test_server(control_stream_key: String) -> TestRpcClient {
|
|||
TestRpcClient::new(addr, config).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn check_doc_state_json<'a, F>(
|
||||
object_id: &str,
|
||||
timeout_secs: u64,
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ async fn create_collab_handler(
|
|||
let (params, workspace_id) = params.split();
|
||||
state
|
||||
.collab_access_control_storage
|
||||
.insert_or_update_collab(&workspace_id, &uid, params)
|
||||
.insert_or_update_collab(&workspace_id, &uid, params, true)
|
||||
.await?;
|
||||
|
||||
Ok(Json(AppResponse::Ok()))
|
||||
|
|
@ -756,7 +756,7 @@ async fn update_collab_handler(
|
|||
let (params, workspace_id) = create_params.split();
|
||||
state
|
||||
.collab_access_control_storage
|
||||
.insert_or_update_collab(&workspace_id, &uid, params)
|
||||
.insert_or_update_collab(&workspace_id, &uid, params, false)
|
||||
.await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
|
||||
// Redis
|
||||
info!("Connecting to Redis...");
|
||||
let redis_client = get_redis_client(config.redis_uri.expose_secret()).await?;
|
||||
let redis_conn_manager = get_redis_client(config.redis_uri.expose_secret()).await?;
|
||||
|
||||
#[cfg(feature = "ai_enable")]
|
||||
let appflowy_ai_client =
|
||||
|
|
@ -212,7 +212,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
let user_cache = UserCache::new(pg_pool.clone()).await;
|
||||
let collab_access_control = CollabAccessControlImpl::new(access_control.clone());
|
||||
let workspace_access_control = WorkspaceAccessControlImpl::new(access_control.clone());
|
||||
let collab_cache = CollabCache::new(redis_client.clone(), pg_pool.clone());
|
||||
let collab_cache = CollabCache::new(redis_conn_manager.clone(), pg_pool.clone());
|
||||
|
||||
let collab_storage_access_control = CollabStorageAccessControlImpl {
|
||||
collab_access_control: collab_access_control.clone().into(),
|
||||
|
|
@ -220,7 +220,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
cache: collab_cache.clone(),
|
||||
};
|
||||
let snapshot_control = SnapshotControl::new(
|
||||
redis_client.clone(),
|
||||
redis_conn_manager.clone(),
|
||||
pg_pool.clone(),
|
||||
metrics.collab_metrics.clone(),
|
||||
)
|
||||
|
|
@ -230,6 +230,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
collab_storage_access_control,
|
||||
snapshot_control,
|
||||
rt_cmd_tx,
|
||||
redis_conn_manager.clone(),
|
||||
));
|
||||
|
||||
#[cfg(feature = "history")]
|
||||
|
|
@ -244,7 +245,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: RTCommandSender) -> Result<A
|
|||
user_cache,
|
||||
id_gen: Arc::new(RwLock::new(Snowflake::new(1))),
|
||||
gotrue_client,
|
||||
redis_connection_manager: redis_client,
|
||||
redis_connection_manager: redis_conn_manager,
|
||||
collab_cache,
|
||||
collab_access_control_storage: collab_storage,
|
||||
collab_access_control,
|
||||
|
|
|
|||
|
|
@ -12,24 +12,24 @@ use sqlx::{PgPool, Transaction};
|
|||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tracing::{event, Level};
|
||||
use tracing::{error, event, Level};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollabCache {
|
||||
disk_cache: CollabDiskCache,
|
||||
mem_cache: CollabMemCache,
|
||||
hits: Arc<AtomicU64>,
|
||||
success_attempts: Arc<AtomicU64>,
|
||||
total_attempts: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl CollabCache {
|
||||
pub fn new(redis_client: RedisConnectionManager, pg_pool: PgPool) -> Self {
|
||||
let mem_cache = CollabMemCache::new(redis_client.clone());
|
||||
pub fn new(redis_conn_manager: RedisConnectionManager, pg_pool: PgPool) -> Self {
|
||||
let mem_cache = CollabMemCache::new(redis_conn_manager.clone());
|
||||
let disk_cache = CollabDiskCache::new(pg_pool.clone());
|
||||
Self {
|
||||
disk_cache,
|
||||
mem_cache,
|
||||
hits: Arc::new(AtomicU64::new(0)),
|
||||
success_attempts: Arc::new(AtomicU64::new(0)),
|
||||
total_attempts: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
|
|
@ -44,10 +44,10 @@ impl CollabCache {
|
|||
if let Some(encoded_collab) = self.mem_cache.get_encode_collab(¶ms.object_id).await {
|
||||
event!(
|
||||
Level::DEBUG,
|
||||
"Get encoded collab:{} from cache",
|
||||
"Get encode collab:{} from cache",
|
||||
params.object_id
|
||||
);
|
||||
self.hits.fetch_add(1, Ordering::Relaxed);
|
||||
self.success_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
return Ok(encoded_collab);
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ impl CollabCache {
|
|||
let timestamp = chrono::Utc::now().timestamp();
|
||||
tokio::spawn(async move {
|
||||
mem_cache
|
||||
.insert_encode_collab(object_id, cloned_encode_collab, timestamp)
|
||||
.insert_encode_collab(&object_id, cloned_encode_collab, timestamp)
|
||||
.await;
|
||||
});
|
||||
Ok(encode_collab)
|
||||
|
|
@ -107,6 +107,8 @@ impl CollabCache {
|
|||
results
|
||||
}
|
||||
|
||||
/// Insert the encoded collab data into the cache.
|
||||
/// The data is inserted into both the memory and disk cache.
|
||||
pub async fn insert_encode_collab_data(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
|
@ -115,31 +117,52 @@ impl CollabCache {
|
|||
transaction: &mut Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), AppError> {
|
||||
let object_id = params.object_id.clone();
|
||||
let encoded_collab = params.encoded_collab_v1.clone();
|
||||
let encode_collab_data = params.encoded_collab_v1.clone();
|
||||
self
|
||||
.disk_cache
|
||||
.upsert_collab_with_transaction(workspace_id, uid, params, transaction)
|
||||
.await?;
|
||||
|
||||
let timestamp = chrono::Utc::now().timestamp();
|
||||
let mem_cache = self.mem_cache.clone();
|
||||
tokio::spawn(async move {
|
||||
mem_cache
|
||||
.insert_encode_collab_data(object_id, encoded_collab, timestamp)
|
||||
.await;
|
||||
});
|
||||
// when the data is written to the disk cache but fails to be written to the memory cache
|
||||
// we log the error and continue.
|
||||
if let Err(err) = self
|
||||
.mem_cache
|
||||
.insert_encode_collab_data(
|
||||
&object_id,
|
||||
&encode_collab_data,
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to insert encode collab into memory cache: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_hit_rate(&self) -> f64 {
|
||||
let hits = self.hits.load(Ordering::Relaxed) as f64;
|
||||
let total_attempts = self.total_attempts.load(Ordering::Relaxed) as f64;
|
||||
/// Insert the encoded collab data into the memory cache.
|
||||
pub async fn insert_encode_collab_data_in_mem(
|
||||
&self,
|
||||
params: &CollabParams,
|
||||
) -> Result<(), AppError> {
|
||||
let timestamp = chrono::Utc::now().timestamp();
|
||||
self
|
||||
.mem_cache
|
||||
.insert_encode_collab_data(¶ms.object_id, ¶ms.encoded_collab_v1, timestamp)
|
||||
.await
|
||||
.map_err(|err| AppError::Internal(err.into()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if total_attempts == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
hits / total_attempts
|
||||
pub fn query_state(&self) -> QueryState {
|
||||
let successful_attempts = self.success_attempts.load(Ordering::Relaxed);
|
||||
let total_attempts = self.total_attempts.load(Ordering::Relaxed);
|
||||
QueryState {
|
||||
total_attempts,
|
||||
success_attempts: successful_attempts,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,3 +187,8 @@ impl CollabCache {
|
|||
&self.disk_cache.pg_pool
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryState {
|
||||
pub total_attempts: u64,
|
||||
pub success_attempts: u64,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,40 @@
|
|||
use crate::state::RedisConnectionManager;
|
||||
use collab::core::collab_plugin::EncodedCollab;
|
||||
use redis::{pipe, AsyncCommands};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use app_error::AppError;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollabMemCache {
|
||||
connection_manager: Arc<Mutex<RedisConnectionManager>>,
|
||||
connection_manager: RedisConnectionManager,
|
||||
}
|
||||
|
||||
impl CollabMemCache {
|
||||
pub fn new(redis_client: RedisConnectionManager) -> Self {
|
||||
Self {
|
||||
connection_manager: Arc::new(Mutex::new(redis_client)),
|
||||
}
|
||||
pub fn new(connection_manager: RedisConnectionManager) -> Self {
|
||||
Self { connection_manager }
|
||||
}
|
||||
|
||||
/// Checks if an object with the given ID exists in the cache.
|
||||
pub async fn is_exist(&self, object_id: &str) -> Result<bool, AppError> {
|
||||
let cache_object_id = cache_object_id_from_key(object_id);
|
||||
let exists: bool = self
|
||||
.connection_manager
|
||||
.lock()
|
||||
.await
|
||||
.exists(object_id)
|
||||
.clone()
|
||||
.exists(&cache_object_id)
|
||||
.await
|
||||
.map_err(|err| AppError::Internal(err.into()))?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub async fn remove_encode_collab(&self, object_id: &str) -> Result<(), AppError> {
|
||||
let cache_object_id = cache_object_id_from_key(object_id);
|
||||
self
|
||||
.connection_manager
|
||||
.lock()
|
||||
.await
|
||||
.del::<&str, ()>(object_id)
|
||||
.clone()
|
||||
.del::<&str, ()>(&cache_object_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
AppError::Internal(anyhow!(
|
||||
|
|
@ -81,7 +77,7 @@ impl CollabMemCache {
|
|||
#[instrument(level = "trace", skip_all, fields(object_id=%object_id))]
|
||||
pub async fn insert_encode_collab(
|
||||
&self,
|
||||
object_id: String,
|
||||
object_id: &str,
|
||||
encoded_collab: EncodedCollab,
|
||||
timestamp: i64,
|
||||
) {
|
||||
|
|
@ -90,7 +86,7 @@ impl CollabMemCache {
|
|||
match result {
|
||||
Ok(Ok(bytes)) => {
|
||||
if let Err(err) = self
|
||||
.insert_data_with_timestamp(object_id, bytes, timestamp)
|
||||
.insert_data_with_timestamp(object_id, &bytes, timestamp)
|
||||
.await
|
||||
{
|
||||
error!("Failed to cache encoded collab: {:?}", err);
|
||||
|
|
@ -105,13 +101,15 @@ impl CollabMemCache {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn insert_encode_collab_data(&self, object_id: String, data: Vec<u8>, timestamp: i64) {
|
||||
if let Err(err) = self
|
||||
pub async fn insert_encode_collab_data(
|
||||
&self,
|
||||
object_id: &str,
|
||||
data: &[u8],
|
||||
timestamp: i64,
|
||||
) -> redis::RedisResult<()> {
|
||||
self
|
||||
.insert_data_with_timestamp(object_id, data, timestamp)
|
||||
.await
|
||||
{
|
||||
error!("Failed to cache encoded collab bytes: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts data into Redis with a conditional timestamp.
|
||||
|
|
@ -129,12 +127,13 @@ impl CollabMemCache {
|
|||
/// A Redis result indicating the success or failure of the operation.
|
||||
async fn insert_data_with_timestamp(
|
||||
&self,
|
||||
object_id: String,
|
||||
data: Vec<u8>,
|
||||
object_id: &str,
|
||||
data: &[u8],
|
||||
timestamp: i64,
|
||||
) -> redis::RedisResult<()> {
|
||||
let mut conn = self.connection_manager.lock().await;
|
||||
let key_exists: bool = conn.exists(&object_id).await?;
|
||||
let cache_object_id = cache_object_id_from_key(object_id);
|
||||
let mut conn = self.connection_manager.clone();
|
||||
let key_exists: bool = conn.exists(&cache_object_id).await?;
|
||||
// Start a watch on the object_id to monitor for changes during this transaction
|
||||
if key_exists {
|
||||
// WATCH command is used to monitor one or more keys for modifications, establishing a condition
|
||||
|
|
@ -142,18 +141,27 @@ impl CollabMemCache {
|
|||
// altered by another client before the current client executes EXEC, the transaction will be
|
||||
// aborted by Redis (the EXEC will return nil indicating the transaction was not processed).
|
||||
redis::cmd("WATCH")
|
||||
.arg(&object_id)
|
||||
.query_async::<_, ()>(&mut *conn)
|
||||
.arg(&cache_object_id)
|
||||
.query_async::<_, ()>(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let result = async {
|
||||
// Retrieve the current data, if exists
|
||||
let current_value: Option<(i64, Vec<u8>)> = if key_exists {
|
||||
let val: Option<Vec<u8>> = conn.get(&object_id).await?;
|
||||
val.map(|data| {
|
||||
let ts = i64::from_be_bytes(data[0..8].try_into().unwrap());
|
||||
(ts, data[8..].to_vec())
|
||||
let val: Option<Vec<u8>> = conn.get(&cache_object_id).await?;
|
||||
val.and_then(|data| {
|
||||
if data.len() < 8 {
|
||||
None
|
||||
} else {
|
||||
match data[0..8].try_into() {
|
||||
Ok(ts_bytes) => {
|
||||
let ts = i64::from_be_bytes(ts_bytes);
|
||||
Some((ts, data[8..].to_vec()))
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -165,14 +173,14 @@ impl CollabMemCache {
|
|||
.map_or(true, |(ts, _)| timestamp > *ts)
|
||||
{
|
||||
let mut pipeline = pipe();
|
||||
let data = [timestamp.to_be_bytes().as_ref(), data.as_slice()].concat();
|
||||
let data = [timestamp.to_be_bytes().as_ref(), data].concat();
|
||||
pipeline
|
||||
.atomic()
|
||||
.set(&object_id, data)
|
||||
.set(&cache_object_id, data)
|
||||
.ignore()
|
||||
.expire(&object_id, 604800) // Setting the expiration to 7 days
|
||||
.expire(&cache_object_id, 604800) // Setting the expiration to 7 days
|
||||
.ignore();
|
||||
pipeline.query_async(conn.deref_mut()).await?;
|
||||
pipeline.query_async(&mut conn).await?;
|
||||
}
|
||||
Ok::<(), redis::RedisError>(())
|
||||
}
|
||||
|
|
@ -180,7 +188,7 @@ impl CollabMemCache {
|
|||
|
||||
// Always reset Watch State
|
||||
redis::cmd("UNWATCH")
|
||||
.query_async::<_, ()>(&mut *conn)
|
||||
.query_async::<_, ()>(&mut conn)
|
||||
.await?;
|
||||
|
||||
result
|
||||
|
|
@ -200,9 +208,10 @@ impl CollabMemCache {
|
|||
&self,
|
||||
object_id: &str,
|
||||
) -> redis::RedisResult<Option<(i64, Vec<u8>)>> {
|
||||
let mut conn = self.connection_manager.lock().await;
|
||||
let cache_object_id = cache_object_id_from_key(object_id);
|
||||
let mut conn = self.connection_manager.clone();
|
||||
// Attempt to retrieve the data from Redis
|
||||
if let Some(data) = conn.get::<_, Option<Vec<u8>>>(object_id).await? {
|
||||
if let Some(data) = conn.get::<_, Option<Vec<u8>>>(&cache_object_id).await? {
|
||||
if data.len() < 8 {
|
||||
// Data is too short to contain a valid timestamp and payload
|
||||
Err(redis::RedisError::from((
|
||||
|
|
@ -211,10 +220,17 @@ impl CollabMemCache {
|
|||
)))
|
||||
} else {
|
||||
// Extract timestamp and payload from the retrieved data
|
||||
let timestamp =
|
||||
i64::from_be_bytes(data[0..8].try_into().expect("Failed to decode timestamp"));
|
||||
let payload = data[8..].to_vec();
|
||||
Ok(Some((timestamp, payload)))
|
||||
match data[0..8].try_into() {
|
||||
Ok(ts_bytes) => {
|
||||
let timestamp = i64::from_be_bytes(ts_bytes);
|
||||
let payload = data[8..].to_vec();
|
||||
Ok(Some((timestamp, payload)))
|
||||
},
|
||||
Err(_) => Err(redis::RedisError::from((
|
||||
redis::ErrorKind::TypeError,
|
||||
"Failed to decode timestamp",
|
||||
))),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No data found for the provided object_id
|
||||
|
|
@ -222,3 +238,12 @@ impl CollabMemCache {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a cache-specific key for an object ID by prepending a fixed prefix.
|
||||
/// This method ensures that any updates to the object's data involve merely
|
||||
/// changing the prefix, allowing the old data to expire naturally.
|
||||
///
|
||||
#[inline]
|
||||
fn cache_object_id_from_key(object_id: &str) -> String {
|
||||
format!("encode_collab_v0:{}", object_id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ use database_entity::dto::{
|
|||
};
|
||||
use itertools::{Either, Itertools};
|
||||
|
||||
use crate::state::RedisConnectionManager;
|
||||
use collab_rt::data_validation::CollabValidator;
|
||||
use sqlx::Transaction;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
|
|
@ -36,7 +38,7 @@ pub struct CollabStorageImpl<AC> {
|
|||
/// access control for collab object. Including read/write
|
||||
access_control: AC,
|
||||
snapshot_control: SnapshotControl,
|
||||
rt_cmd: RTCommandSender,
|
||||
rt_cmd_sender: RTCommandSender,
|
||||
}
|
||||
|
||||
impl<AC> CollabStorageImpl<AC>
|
||||
|
|
@ -48,12 +50,14 @@ where
|
|||
access_control: AC,
|
||||
snapshot_control: SnapshotControl,
|
||||
rt_cmd_sender: RTCommandSender,
|
||||
_redis_conn_manager: RedisConnectionManager,
|
||||
) -> Self {
|
||||
// let queue = Arc::new(StorageQueue::new(cache.clone(), redis_conn_manager));
|
||||
Self {
|
||||
cache,
|
||||
access_control,
|
||||
snapshot_control,
|
||||
rt_cmd: rt_cmd_sender,
|
||||
rt_cmd_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +110,7 @@ where
|
|||
|
||||
// Attempt to send the command to the realtime server
|
||||
if let Err(err) = self
|
||||
.rt_cmd
|
||||
.rt_cmd_sender
|
||||
.send(RTCommand::GetEncodeCollab { object_id, ret })
|
||||
.await
|
||||
{
|
||||
|
|
@ -141,8 +145,9 @@ impl<AC> CollabStorage for CollabStorageImpl<AC>
|
|||
where
|
||||
AC: CollabStorageAccessControl,
|
||||
{
|
||||
fn encode_collab_mem_hit_rate(&self) -> f64 {
|
||||
self.cache.get_hit_rate()
|
||||
fn encode_collab_redis_query_state(&self) -> (u64, u64) {
|
||||
let state = self.cache.query_state();
|
||||
(state.total_attempts, state.success_attempts)
|
||||
}
|
||||
|
||||
async fn insert_or_update_collab(
|
||||
|
|
@ -150,6 +155,7 @@ where
|
|||
workspace_id: &str,
|
||||
uid: &i64,
|
||||
params: CollabParams,
|
||||
_write_immediately: bool,
|
||||
) -> AppResult<()> {
|
||||
params.validate()?;
|
||||
if let Err(err) = params.check_encode_collab().await {
|
||||
|
|
@ -159,10 +165,9 @@ where
|
|||
)));
|
||||
}
|
||||
let is_exist = self.cache.is_exist(¶ms.object_id).await?;
|
||||
// If the collab already exists, check if the user has enough permissions to update collab
|
||||
// Otherwise, check if the user has enough permissions to create collab.
|
||||
if is_exist {
|
||||
self
|
||||
.check_write_workspace_permission(workspace_id, uid)
|
||||
.await?;
|
||||
self
|
||||
.check_write_collab_permission(workspace_id, uid, ¶ms.object_id)
|
||||
.await?;
|
||||
|
|
@ -181,22 +186,34 @@ where
|
|||
.await?;
|
||||
}
|
||||
|
||||
let mut transaction = self
|
||||
.cache
|
||||
.pg_pool()
|
||||
.begin()
|
||||
.await
|
||||
.context("acquire transaction to upsert collab")
|
||||
.map_err(AppError::from)?;
|
||||
self
|
||||
.cache
|
||||
.insert_encode_collab_data(workspace_id, uid, params, &mut transaction)
|
||||
.await?;
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.context("fail to commit the transaction to upsert collab")
|
||||
.map_err(AppError::from)?;
|
||||
let write_to_disk = |data| async {
|
||||
let mut transaction = self
|
||||
.cache
|
||||
.pg_pool()
|
||||
.begin()
|
||||
.await
|
||||
.context("acquire transaction to upsert collab")
|
||||
.map_err(AppError::from)?;
|
||||
self
|
||||
.cache
|
||||
.insert_encode_collab_data(workspace_id, uid, data, &mut transaction)
|
||||
.await?;
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.context("fail to commit the transaction to upsert collab")
|
||||
.map_err(AppError::from)?;
|
||||
Ok::<(), AppError>(())
|
||||
};
|
||||
|
||||
write_to_disk(params).await?;
|
||||
// if write_immediately {
|
||||
// write_to_disk(params).await?;
|
||||
// } else if let Err(err) = self.queue.queue_insert(params).await {
|
||||
// // If queue insert fails, write to disk immediately
|
||||
// write_to_disk(err.data).await?;
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ use collab_entity::CollabType;
|
|||
use database_entity::dto::{AFAccessLevel, QueryCollabParams};
|
||||
use serde_json::json;
|
||||
use sqlx::types::uuid;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::trace;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_collab_content_after_reconnect_test() {
|
||||
|
|
@ -69,64 +66,64 @@ async fn sync_collab_content_after_reconnect_test() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_client_with_diff_devices_edit_same_collab_test() {
|
||||
let collab_type = CollabType::Unknown;
|
||||
let registered_user = generate_unique_registered_user().await;
|
||||
let mut client_1 = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||
let mut client_2 = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||
|
||||
let workspace_id = client_1.workspace_id().await;
|
||||
let object_id = client_1
|
||||
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
||||
.await;
|
||||
|
||||
// client 1 edit the collab
|
||||
client_1
|
||||
.collabs
|
||||
.get_mut(&object_id)
|
||||
.unwrap()
|
||||
.mutex_collab
|
||||
.lock()
|
||||
.insert("name", "workspace1");
|
||||
client_1
|
||||
.wait_object_sync_complete(&object_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_2
|
||||
.open_collab(&workspace_id, &object_id, collab_type.clone())
|
||||
.await;
|
||||
client_2
|
||||
.wait_object_sync_complete(&object_id)
|
||||
.await
|
||||
.unwrap();
|
||||
trace!("client 2 disconnect: {:?}", client_2.device_id);
|
||||
client_2.disconnect().await;
|
||||
sleep(Duration::from_millis(2000)).await;
|
||||
|
||||
client_2
|
||||
.collabs
|
||||
.get_mut(&object_id)
|
||||
.unwrap()
|
||||
.mutex_collab
|
||||
.lock()
|
||||
.insert("name", "workspace2");
|
||||
client_2.reconnect().await;
|
||||
client_2
|
||||
.wait_object_sync_complete(&object_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let expected_json = json!({
|
||||
"name": "workspace2"
|
||||
});
|
||||
|
||||
assert_client_collab_within_secs(&mut client_2, &object_id, "name", expected_json.clone(), 60)
|
||||
.await;
|
||||
assert_client_collab_within_secs(&mut client_1, &object_id, "name", expected_json.clone(), 60)
|
||||
.await;
|
||||
}
|
||||
// #[tokio::test]
|
||||
// async fn same_client_with_diff_devices_edit_same_collab_test() {
|
||||
// let collab_type = CollabType::Unknown;
|
||||
// let registered_user = generate_unique_registered_user().await;
|
||||
// let mut client_1 = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||
// let mut client_2 = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||
//
|
||||
// let workspace_id = client_1.workspace_id().await;
|
||||
// let object_id = client_1
|
||||
// .create_and_edit_collab(&workspace_id, collab_type.clone())
|
||||
// .await;
|
||||
//
|
||||
// // client 1 edit the collab
|
||||
// client_1
|
||||
// .collabs
|
||||
// .get_mut(&object_id)
|
||||
// .unwrap()
|
||||
// .mutex_collab
|
||||
// .lock()
|
||||
// .insert("name", "workspace1");
|
||||
// client_1
|
||||
// .wait_object_sync_complete(&object_id)
|
||||
// .await
|
||||
// .unwrap();
|
||||
//
|
||||
// client_2
|
||||
// .open_collab(&workspace_id, &object_id, collab_type.clone())
|
||||
// .await;
|
||||
// client_2
|
||||
// .wait_object_sync_complete(&object_id)
|
||||
// .await
|
||||
// .unwrap();
|
||||
// trace!("client 2 disconnect: {:?}", client_2.device_id);
|
||||
// client_2.disconnect().await;
|
||||
// sleep(Duration::from_millis(2000)).await;
|
||||
//
|
||||
// client_2
|
||||
// .collabs
|
||||
// .get_mut(&object_id)
|
||||
// .unwrap()
|
||||
// .mutex_collab
|
||||
// .lock()
|
||||
// .insert("name", "workspace2");
|
||||
// client_2.reconnect().await;
|
||||
// client_2
|
||||
// .wait_object_sync_complete(&object_id)
|
||||
// .await
|
||||
// .unwrap();
|
||||
//
|
||||
// let expected_json = json!({
|
||||
// "name": "workspace2"
|
||||
// });
|
||||
//
|
||||
// assert_client_collab_within_secs(&mut client_2, &object_id, "name", expected_json.clone(), 60)
|
||||
// .await;
|
||||
// assert_client_collab_within_secs(&mut client_1, &object_id, "name", expected_json.clone(), 60)
|
||||
// .await;
|
||||
// }
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_client_with_diff_devices_edit_diff_collab_test() {
|
||||
|
|
|
|||
|
|
@ -232,11 +232,12 @@ async fn collab_mem_cache_read_write_test() {
|
|||
let timestamp = chrono::Utc::now().timestamp();
|
||||
mem_cache
|
||||
.insert_encode_collab_data(
|
||||
object_id.clone(),
|
||||
encode_collab.encode_to_bytes().unwrap(),
|
||||
&object_id,
|
||||
&encode_collab.encode_to_bytes().unwrap(),
|
||||
timestamp,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let encode_collab_from_cache = mem_cache.get_encode_collab(&object_id).await.unwrap();
|
||||
assert_eq!(encode_collab_from_cache.state_vector, vec![1, 2, 3]);
|
||||
|
|
@ -253,24 +254,26 @@ async fn collab_mem_cache_insert_override_test() {
|
|||
let mut timestamp = chrono::Utc::now().timestamp();
|
||||
mem_cache
|
||||
.insert_encode_collab_data(
|
||||
object_id.clone(),
|
||||
encode_collab.encode_to_bytes().unwrap(),
|
||||
&object_id,
|
||||
&encode_collab.encode_to_bytes().unwrap(),
|
||||
timestamp,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// the following insert should not override the previous one because the timestamp is older
|
||||
// than the previous one
|
||||
timestamp -= 100;
|
||||
mem_cache
|
||||
.insert_encode_collab_data(
|
||||
object_id.clone(),
|
||||
EncodedCollab::new_v1(vec![6, 7, 8], vec![9, 10, 11])
|
||||
&object_id,
|
||||
&EncodedCollab::new_v1(vec![6, 7, 8], vec![9, 10, 11])
|
||||
.encode_to_bytes()
|
||||
.unwrap(),
|
||||
timestamp,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the previous insert is still in the cache
|
||||
let encode_collab_from_cache = mem_cache.get_encode_collab(&object_id).await.unwrap();
|
||||
|
|
@ -282,13 +285,14 @@ async fn collab_mem_cache_insert_override_test() {
|
|||
timestamp += 500;
|
||||
mem_cache
|
||||
.insert_encode_collab_data(
|
||||
object_id.clone(),
|
||||
EncodedCollab::new_v1(vec![12, 13, 14], vec![15, 16, 17])
|
||||
&object_id,
|
||||
&EncodedCollab::new_v1(vec![12, 13, 14], vec![15, 16, 17])
|
||||
.encode_to_bytes()
|
||||
.unwrap(),
|
||||
timestamp,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the previous insert is overridden
|
||||
let encode_collab_from_cache = mem_cache.get_encode_collab(&object_id).await.unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue