197 lines
5.9 KiB
Rust
197 lines
5.9 KiB
Rust
use anyhow::anyhow;
|
|
use collab::core::collab_plugin::CollabPluginType;
|
|
use collab::lock::RwLock;
|
|
use collab::preclude::updates::encoder::{Encoder, EncoderV2};
|
|
use collab::preclude::{Collab, CollabPlugin, ReadTxn, Snapshot, StateVector, TransactionMut};
|
|
use collab_entity::CollabType;
|
|
use database::history::ops::get_snapshot_meta_list;
|
|
use serde_json::Value;
|
|
use sqlx::PgPool;
|
|
use std::sync::Arc;
|
|
use tonic_proto::history::{RepeatedSnapshotMetaPb, SnapshotMetaPb};
|
|
|
|
use crate::biz::snapshot::{
|
|
calculate_edit_count, CollabSnapshot, CollabSnapshotState, SnapshotGenerator,
|
|
};
|
|
use crate::error::HistoryError;
|
|
|
|
pub struct CollabHistory {
|
|
pub(crate) object_id: String,
|
|
collab: Arc<RwLock<Collab>>,
|
|
collab_type: CollabType,
|
|
snapshot_generator: SnapshotGenerator,
|
|
}
|
|
|
|
impl CollabHistory {
|
|
pub async fn new(object_id: &str, collab: Arc<RwLock<Collab>>, collab_type: CollabType) -> Self {
|
|
let current_edit_count = {
|
|
let read_guard = collab.read().await;
|
|
let txn = read_guard.transact();
|
|
calculate_edit_count(&txn)
|
|
};
|
|
|
|
#[cfg(feature = "verbose_log")]
|
|
tracing::trace!(
|
|
"[History] object:{} init edit count: {}",
|
|
object_id,
|
|
current_edit_count
|
|
);
|
|
|
|
let snapshot_generator = SnapshotGenerator::new(
|
|
object_id,
|
|
Arc::downgrade(&collab),
|
|
collab_type.clone(),
|
|
current_edit_count as u32,
|
|
);
|
|
collab.read().await.add_plugin(Box::new(CountUpdatePlugin {
|
|
snapshot_generator: snapshot_generator.clone(),
|
|
}));
|
|
collab.write().await.initialize();
|
|
|
|
Self {
|
|
object_id: object_id.to_string(),
|
|
snapshot_generator,
|
|
collab,
|
|
collab_type,
|
|
}
|
|
}
|
|
|
|
pub async fn generate_snapshot_if_empty(&self) {
|
|
if !self.snapshot_generator.has_snapshot().await {
|
|
self.snapshot_generator.generate().await;
|
|
}
|
|
}
|
|
|
|
pub async fn gen_history(
|
|
&self,
|
|
min_snapshot_required: Option<usize>,
|
|
) -> Result<Option<HistoryContext>, HistoryError> {
|
|
if let Some(min_snapshot_required) = min_snapshot_required {
|
|
let num_snapshot = self.snapshot_generator.num_pending_snapshots().await;
|
|
if num_snapshot < min_snapshot_required {
|
|
#[cfg(feature = "verbose_log")]
|
|
tracing::trace!(
|
|
"[History]: {} current snapshot:{}, minimum required:{}",
|
|
self.object_id,
|
|
num_snapshot,
|
|
min_snapshot_required
|
|
);
|
|
return Ok(None);
|
|
}
|
|
}
|
|
|
|
let collab = self.collab.clone();
|
|
let timestamp = chrono::Utc::now().timestamp();
|
|
let snapshots: Vec<CollabSnapshot> = self.snapshot_generator.consume_pending_snapshots().await
|
|
.into_iter()
|
|
// Remove the snapshots which created_at is bigger than the current timestamp
|
|
.filter(|snapshot| snapshot.created_at <= timestamp)
|
|
.collect();
|
|
|
|
// If there are no snapshots, we don't need to generate a new snapshot
|
|
if snapshots.is_empty() {
|
|
#[cfg(feature = "verbose_log")]
|
|
tracing::trace!("[History]: {} has no snapshots", self.object_id,);
|
|
return Ok(None);
|
|
}
|
|
let collab_type = self.collab_type.clone();
|
|
let object_id = self.object_id.clone();
|
|
let (doc_state, state_vector) = tokio::task::spawn_blocking(move || {
|
|
let lock = collab.blocking_read();
|
|
let result = collab_type.validate_require_data(&lock);
|
|
match result {
|
|
Ok(_) => {
|
|
let txn = lock.transact();
|
|
let doc_state_v2 = txn.encode_state_as_update_v2(&StateVector::default());
|
|
let state_vector = txn.state_vector();
|
|
Ok::<_, HistoryError>((doc_state_v2, state_vector))
|
|
},
|
|
Err(err) => Err::<_, HistoryError>(HistoryError::Internal(anyhow!(
|
|
"Failed to validate {}:{} required data: {}",
|
|
object_id,
|
|
collab_type,
|
|
err
|
|
))),
|
|
}
|
|
})
|
|
.await
|
|
.map_err(|err| HistoryError::Internal(err.into()))??;
|
|
|
|
let collab_type = self.collab_type.clone();
|
|
let object_id = self.object_id.clone();
|
|
let state = CollabSnapshotState::new(
|
|
object_id,
|
|
doc_state,
|
|
2,
|
|
state_vector,
|
|
chrono::Utc::now().timestamp(),
|
|
);
|
|
Ok(Some(HistoryContext {
|
|
collab_type,
|
|
state,
|
|
snapshots,
|
|
}))
|
|
}
|
|
|
|
/// Encode the state of the collab as Update.
|
|
/// We encode the collaboration state as an update using the v2 format, chosen over the v1 format
|
|
/// due to its reduced data size. This optimization helps in minimizing the storage and
|
|
/// transmission overhead, making the process more efficient.
|
|
pub async fn encode_update_v2(&self, snapshot: &Snapshot) -> Result<Vec<u8>, HistoryError> {
|
|
let lock = self.collab.read().await;
|
|
let txn = lock.transact();
|
|
let mut encoder = EncoderV2::new();
|
|
txn
|
|
.encode_state_from_snapshot(snapshot, &mut encoder)
|
|
.map_err(|err| HistoryError::Internal(err.into()))?;
|
|
Ok(encoder.to_vec())
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
pub async fn json(&self) -> Value {
|
|
let lock_guard = self.collab.read().await;
|
|
lock_guard.to_json_value()
|
|
}
|
|
}
|
|
|
|
pub struct HistoryContext {
|
|
pub collab_type: CollabType,
|
|
pub state: CollabSnapshotState,
|
|
pub snapshots: Vec<CollabSnapshot>,
|
|
}
|
|
|
|
struct CountUpdatePlugin {
|
|
snapshot_generator: SnapshotGenerator,
|
|
}
|
|
impl CollabPlugin for CountUpdatePlugin {
|
|
fn receive_update(&self, _object_id: &str, txn: &TransactionMut, _update: &[u8]) {
|
|
self.snapshot_generator.did_apply_update(txn);
|
|
}
|
|
|
|
fn plugin_type(&self) -> CollabPluginType {
|
|
CollabPluginType::Other("CountUpdatePlugin".to_string())
|
|
}
|
|
}
|
|
|
|
pub async fn get_snapshots(
|
|
object_id: &str,
|
|
collab_type: &CollabType,
|
|
pg_pool: &PgPool,
|
|
) -> Result<RepeatedSnapshotMetaPb, HistoryError> {
|
|
let metas = get_snapshot_meta_list(object_id, collab_type, pg_pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
let metas = metas
|
|
.into_iter()
|
|
.map(|meta| SnapshotMetaPb {
|
|
oid: meta.oid,
|
|
snapshot: meta.snapshot,
|
|
snapshot_version: meta.snapshot_version,
|
|
created_at: meta.created_at,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(RepeatedSnapshotMetaPb { items: metas })
|
|
}
|