generic kv

This commit is contained in:
appflowy 2021-12-27 11:15:15 +08:00
parent a0e6c61f50
commit 049b8828fe
21 changed files with 344 additions and 350 deletions

12
backend/Cargo.lock generated
View File

@ -405,6 +405,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "async-trait"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "0.4.0" version = "0.4.0"
@ -446,6 +457,7 @@ dependencies = [
"actix-web-actors", "actix-web-actors",
"anyhow", "anyhow",
"async-stream", "async-stream",
"async-trait",
"backend", "backend",
"backend-service", "backend-service",
"bcrypt", "bcrypt",

View File

@ -24,6 +24,7 @@ bytes = "1"
toml = "0.5.8" toml = "0.5.8"
dashmap = "4.0" dashmap = "4.0"
log = "0.4.14" log = "0.4.14"
async-trait = "0.1.52"
# tracing # tracing
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
@ -34,6 +35,7 @@ tracing-appender = "0.1"
tracing-core = "0.1" tracing-core = "0.1"
tracing-log = { version = "0.1.1"} tracing-log = { version = "0.1.1"}
# serde # serde
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@ -5,7 +5,7 @@ use crate::services::{
use actix::Addr; use actix::Addr;
use actix_web::web::Data; use actix_web::web::Data;
use crate::services::document::{controller::make_document_ws_receiver, persistence::DocumentKVPersistence}; use crate::services::document::{persistence::DocumentKVPersistence, ws_receiver::make_document_ws_receiver};
use lib_ws::WSModule; use lib_ws::WSModule;
use sqlx::PgPool; use sqlx::PgPool;
use std::sync::Arc; use std::sync::Arc;

View File

@ -2,7 +2,7 @@ use crate::{
entities::logged_user::LoggedUser, entities::logged_user::LoggedUser,
services::{ services::{
core::{trash::read_trash_ids, view::persistence::*}, core::{trash::read_trash_ids, view::persistence::*},
document::persistence::{create_doc, delete_doc, DocumentKVPersistence}, document::persistence::{create_document, delete_document, DocumentKVPersistence},
}, },
util::sqlx_ext::{map_sqlx_error, DBTransaction, SqlBuilder}, util::sqlx_ext::{map_sqlx_error, DBTransaction, SqlBuilder},
}; };
@ -59,7 +59,7 @@ pub(crate) async fn delete_view(
.await .await
.map_err(map_sqlx_error)?; .map_err(map_sqlx_error)?;
let _ = delete_doc(kv_store, view_id).await?; let _ = delete_document(kv_store, view_id).await?;
} }
Ok(()) Ok(())
} }
@ -100,7 +100,7 @@ pub(crate) async fn create_view(
create_doc_params.set_revisions(repeated_revision); create_doc_params.set_revisions(repeated_revision);
create_doc_params.set_id(view.id.clone()); create_doc_params.set_id(view.id.clone());
let _ = create_doc(&kv_store, create_doc_params).await?; let _ = create_document(&kv_store, create_doc_params).await?;
Ok(view) Ok(view)
} }

View File

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
pub(crate) mod controller;
pub(crate) mod persistence; pub(crate) mod persistence;
pub(crate) mod router; pub(crate) mod router;
pub(crate) mod ws_actor; pub(crate) mod ws_actor;
pub(crate) mod ws_receiver;

View File

@ -1,12 +1,62 @@
use crate::{ use crate::{
context::FlowyPersistence,
services::kv::{KVStore, KeyValue}, services::kv::{KVStore, KeyValue},
util::serde_ext::parse_from_bytes, util::serde_ext::parse_from_bytes,
}; };
use backend_service::errors::ServerError; use anyhow::Context;
use backend_service::errors::{internal_error, ServerError};
use bytes::Bytes; use bytes::Bytes;
use flowy_collaboration::protobuf::{RepeatedRevision, Revision}; use flowy_collaboration::protobuf::{
CreateDocParams,
DocIdentifier,
DocumentInfo,
RepeatedRevision,
ResetDocumentParams,
Revision,
};
use lib_ot::{core::OperationTransformable, rich_text::RichTextDelta};
use protobuf::Message; use protobuf::Message;
use sqlx::PgPool;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid;
#[tracing::instrument(level = "debug", skip(kv_store), err)]
pub(crate) async fn create_document(
kv_store: &Arc<DocumentKVPersistence>,
mut params: CreateDocParams,
) -> Result<(), ServerError> {
let revisions = params.take_revisions().take_items();
let _ = kv_store.batch_set_revision(revisions.into()).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(persistence), err)]
pub(crate) async fn read_document(
persistence: &Arc<FlowyPersistence>,
params: DocIdentifier,
) -> Result<DocumentInfo, ServerError> {
let _ = Uuid::parse_str(&params.doc_id).context("Parse document id to uuid failed")?;
let kv_store = persistence.kv_store();
let revisions = kv_store.batch_get_revisions(&params.doc_id, None).await?;
make_doc_from_revisions(&params.doc_id, revisions)
}
#[tracing::instrument(level = "debug", skip(kv_store, params), fields(delta), err)]
pub async fn reset_document(
kv_store: &Arc<DocumentKVPersistence>,
params: ResetDocumentParams,
) -> Result<(), ServerError> {
// TODO: Reset document requires atomic operation
// let _ = kv_store.batch_delete_revisions(&doc_id.to_string(), None).await?;
todo!()
}
#[tracing::instrument(level = "debug", skip(kv_store), err)]
pub(crate) async fn delete_document(kv_store: &Arc<DocumentKVPersistence>, doc_id: Uuid) -> Result<(), ServerError> {
let _ = kv_store.batch_delete_revisions(&doc_id.to_string(), None).await?;
Ok(())
}
pub struct DocumentKVPersistence { pub struct DocumentKVPersistence {
inner: Arc<dyn KVStore>, inner: Arc<dyn KVStore>,
@ -111,3 +161,29 @@ fn key_value_items_to_revisions(items: Vec<KeyValue>) -> RepeatedRevision {
#[inline] #[inline]
fn make_revision_key(doc_id: &str, rev_id: i64) -> String { format!("{}:{}", doc_id, rev_id) } fn make_revision_key(doc_id: &str, rev_id: i64) -> String { format!("{}:{}", doc_id, rev_id) }
#[inline]
fn make_doc_from_revisions(doc_id: &str, mut revisions: RepeatedRevision) -> Result<DocumentInfo, ServerError> {
let revisions = revisions.take_items();
if revisions.is_empty() {
return Err(ServerError::record_not_found().context(format!("{} not exist", doc_id)));
}
let mut document_delta = RichTextDelta::new();
let mut base_rev_id = 0;
let mut rev_id = 0;
// TODO: generate delta from revision should be wrapped into function.
for revision in revisions {
base_rev_id = revision.base_rev_id;
rev_id = revision.rev_id;
let delta = RichTextDelta::from_bytes(revision.delta_data).map_err(internal_error)?;
document_delta = document_delta.compose(&delta).map_err(internal_error)?;
}
let text = document_delta.to_json();
let mut document_info = DocumentInfo::new();
document_info.set_doc_id(doc_id.to_owned());
document_info.set_text(text);
document_info.set_base_rev_id(base_rev_id);
document_info.set_rev_id(rev_id);
Ok(document_info)
}

View File

@ -1,5 +0,0 @@
mod kv_store;
mod postgres;
pub use kv_store::*;
pub use postgres::*;

View File

@ -1,78 +0,0 @@
use crate::{context::FlowyPersistence, services::document::persistence::DocumentKVPersistence};
use anyhow::Context;
use backend_service::errors::{internal_error, ServerError};
use flowy_collaboration::protobuf::{
CreateDocParams,
DocIdentifier,
DocumentInfo,
RepeatedRevision,
ResetDocumentParams,
};
use lib_ot::{core::OperationTransformable, rich_text::RichTextDelta};
use sqlx::PgPool;
use std::sync::Arc;
use uuid::Uuid;
#[tracing::instrument(level = "debug", skip(kv_store), err)]
pub(crate) async fn create_doc(
kv_store: &Arc<DocumentKVPersistence>,
mut params: CreateDocParams,
) -> Result<(), ServerError> {
let revisions = params.take_revisions().take_items();
let _ = kv_store.batch_set_revision(revisions.into()).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(persistence), err)]
pub(crate) async fn read_doc(
persistence: &Arc<FlowyPersistence>,
params: DocIdentifier,
) -> Result<DocumentInfo, ServerError> {
let _ = Uuid::parse_str(&params.doc_id).context("Parse document id to uuid failed")?;
let kv_store = persistence.kv_store();
let revisions = kv_store.batch_get_revisions(&params.doc_id, None).await?;
make_doc_from_revisions(&params.doc_id, revisions)
}
#[tracing::instrument(level = "debug", skip(_pool, _params), fields(delta), err)]
pub async fn reset_document(_pool: &PgPool, _params: ResetDocumentParams) -> Result<(), ServerError> {
unimplemented!()
}
#[tracing::instrument(level = "debug", skip(kv_store), err)]
pub(crate) async fn delete_doc(kv_store: &Arc<DocumentKVPersistence>, doc_id: Uuid) -> Result<(), ServerError> {
let _ = kv_store.batch_delete_revisions(&doc_id.to_string(), None).await?;
Ok(())
}
#[derive(Debug, Clone, sqlx::FromRow)]
struct DocTable {
id: uuid::Uuid,
rev_id: i64,
}
fn make_doc_from_revisions(doc_id: &str, mut revisions: RepeatedRevision) -> Result<DocumentInfo, ServerError> {
let revisions = revisions.take_items();
if revisions.is_empty() {
return Err(ServerError::record_not_found().context(format!("{} not exist", doc_id)));
}
let mut document_delta = RichTextDelta::new();
let mut base_rev_id = 0;
let mut rev_id = 0;
// TODO: generate delta from revision should be wrapped into function.
for revision in revisions {
base_rev_id = revision.base_rev_id;
rev_id = revision.rev_id;
let delta = RichTextDelta::from_bytes(revision.delta_data).map_err(internal_error)?;
document_delta = document_delta.compose(&delta).map_err(internal_error)?;
}
let text = document_delta.to_json();
let mut document_info = DocumentInfo::new();
document_info.set_doc_id(doc_id.to_owned());
document_info.set_text(text);
document_info.set_base_rev_id(base_rev_id);
document_info.set_rev_id(rev_id);
Ok(document_info)
}

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
context::FlowyPersistence, context::FlowyPersistence,
services::document::persistence::{create_doc, read_doc}, services::document::persistence::{create_document, read_document, reset_document},
util::serde_ext::parse_from_payload, util::serde_ext::parse_from_payload,
}; };
use actix_web::{ use actix_web::{
@ -18,7 +18,7 @@ pub async fn create_document_handler(
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
let params: CreateDocParams = parse_from_payload(payload).await?; let params: CreateDocParams = parse_from_payload(payload).await?;
let kv_store = persistence.kv_store(); let kv_store = persistence.kv_store();
let _ = create_doc(&kv_store, params).await?; let _ = create_document(&kv_store, params).await?;
Ok(FlowyResponse::success().into()) Ok(FlowyResponse::success().into())
} }
@ -28,13 +28,17 @@ pub async fn read_document_handler(
persistence: Data<Arc<FlowyPersistence>>, persistence: Data<Arc<FlowyPersistence>>,
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
let params: DocIdentifier = parse_from_payload(payload).await?; let params: DocIdentifier = parse_from_payload(payload).await?;
let doc = read_doc(persistence.get_ref(), params).await?; let doc = read_document(persistence.get_ref(), params).await?;
let response = FlowyResponse::success().pb(doc)?; let response = FlowyResponse::success().pb(doc)?;
Ok(response.into()) Ok(response.into())
} }
pub async fn reset_document_handler(payload: Payload, _pool: Data<PgPool>) -> Result<HttpResponse, ServerError> { pub async fn reset_document_handler(
let _params: ResetDocumentParams = parse_from_payload(payload).await?; payload: Payload,
// Ok(FlowyResponse::success().into()) persistence: Data<Arc<FlowyPersistence>>,
unimplemented!() ) -> Result<HttpResponse, ServerError> {
let params: ResetDocumentParams = parse_from_payload(payload).await?;
let kv_store = persistence.kv_store();
let _ = reset_document(&kv_store, params).await?;
Ok(FlowyResponse::success().into())
} }

View File

@ -1,6 +1,6 @@
use crate::services::{ use crate::services::{
document::{ document::{
persistence::{create_doc, read_doc}, persistence::{create_document, read_document},
ws_actor::{DocumentWebSocketActor, WSActorMessage}, ws_actor::{DocumentWebSocketActor, WSActorMessage},
}, },
web_socket::{WSClientData, WebSocketReceiver}, web_socket::{WSClientData, WebSocketReceiver},
@ -16,7 +16,7 @@ use flowy_collaboration::{
errors::CollaborateError, errors::CollaborateError,
protobuf::DocIdentifier, protobuf::DocIdentifier,
}; };
use lib_infra::future::FutureResultSend; use lib_infra::future::{BoxResultFuture, FutureResultSend};
use flowy_collaboration::sync::{DocumentPersistence, ServerDocumentManager}; use flowy_collaboration::sync::{DocumentPersistence, ServerDocumentManager};
use std::{ use std::{
@ -78,14 +78,14 @@ impl Debug for DocumentPersistenceImpl {
} }
impl DocumentPersistence for DocumentPersistenceImpl { impl DocumentPersistence for DocumentPersistenceImpl {
fn read_doc(&self, doc_id: &str) -> FutureResultSend<DocumentInfo, CollaborateError> { fn read_doc(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError> {
let params = DocIdentifier { let params = DocIdentifier {
doc_id: doc_id.to_string(), doc_id: doc_id.to_string(),
..Default::default() ..Default::default()
}; };
let persistence = self.0.clone(); let persistence = self.0.clone();
FutureResultSend::new(async move { Box::pin(async move {
let mut pb_doc = read_doc(&persistence, params) let mut pb_doc = read_document(&persistence, params)
.await .await
.map_err(server_error_to_collaborate_error)?; .map_err(server_error_to_collaborate_error)?;
let doc = (&mut pb_doc) let doc = (&mut pb_doc)
@ -95,23 +95,23 @@ impl DocumentPersistence for DocumentPersistenceImpl {
}) })
} }
fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> FutureResultSend<DocumentInfo, CollaborateError> { fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> BoxResultFuture<DocumentInfo, CollaborateError> {
let kv_store = self.0.kv_store(); let kv_store = self.0.kv_store();
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
FutureResultSend::new(async move { Box::pin(async move {
let doc = DocumentInfo::from_revisions(&doc_id, revisions.clone())?; let doc = DocumentInfo::from_revisions(&doc_id, revisions.clone())?;
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
let revisions = RepeatedRevision::new(revisions); let revisions = RepeatedRevision::new(revisions);
let params = CreateDocParams { id: doc_id, revisions }; let params = CreateDocParams { id: doc_id, revisions };
let pb_params: flowy_collaboration::protobuf::CreateDocParams = params.try_into().unwrap(); let pb_params: flowy_collaboration::protobuf::CreateDocParams = params.try_into().unwrap();
let _ = create_doc(&kv_store, pb_params) let _ = create_document(&kv_store, pb_params)
.await .await
.map_err(server_error_to_collaborate_error)?; .map_err(server_error_to_collaborate_error)?;
Ok(doc) Ok(doc)
}) })
} }
fn get_revisions(&self, doc_id: &str, rev_ids: Vec<i64>) -> FutureResultSend<Vec<Revision>, CollaborateError> { fn get_revisions(&self, doc_id: &str, rev_ids: Vec<i64>) -> BoxResultFuture<Vec<Revision>, CollaborateError> {
let kv_store = self.0.kv_store(); let kv_store = self.0.kv_store();
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
let f = || async move { let f = || async move {
@ -123,10 +123,10 @@ impl DocumentPersistence for DocumentPersistenceImpl {
Ok(revisions) Ok(revisions)
}; };
FutureResultSend::new(async move { f().await.map_err(server_error_to_collaborate_error) }) Box::pin(async move { f().await.map_err(server_error_to_collaborate_error) })
} }
fn get_doc_revisions(&self, doc_id: &str) -> FutureResultSend<Vec<Revision>, CollaborateError> { fn get_doc_revisions(&self, doc_id: &str) -> BoxResultFuture<Vec<Revision>, CollaborateError> {
let kv_store = self.0.kv_store(); let kv_store = self.0.kv_store();
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
let f = || async move { let f = || async move {
@ -136,7 +136,7 @@ impl DocumentPersistence for DocumentPersistenceImpl {
Ok(revisions) Ok(revisions)
}; };
FutureResultSend::new(async move { f().await.map_err(server_error_to_collaborate_error) }) Box::pin(async move { f().await.map_err(server_error_to_collaborate_error) })
} }
} }

View File

@ -1,12 +1,13 @@
use crate::{ use crate::{
services::kv::{KVStore, KeyValue}, services::kv::{KVAction, KVStore, KeyValue},
util::sqlx_ext::{map_sqlx_error, SqlBuilder}, util::sqlx_ext::{map_sqlx_error, DBTransaction, SqlBuilder},
}; };
use anyhow::Context; use anyhow::Context;
use async_trait::async_trait;
use backend_service::errors::ServerError; use backend_service::errors::ServerError;
use bytes::Bytes; use bytes::Bytes;
use lib_infra::future::FutureResultSend; use futures_core::future::BoxFuture;
use lib_infra::future::{BoxResultFuture, FutureResultSend};
use sql_builder::SqlBuilder as RawSqlBuilder; use sql_builder::SqlBuilder as RawSqlBuilder;
use sqlx::{ use sqlx::{
postgres::{PgArguments, PgRow}, postgres::{PgArguments, PgRow},
@ -16,6 +17,7 @@ use sqlx::{
Postgres, Postgres,
Row, Row,
}; };
use std::{future::Future, pin::Pin};
const KV_TABLE: &str = "kv_table"; const KV_TABLE: &str = "kv_table";
@ -23,23 +25,49 @@ pub(crate) struct PostgresKV {
pub(crate) pg_pool: PgPool, pub(crate) pg_pool: PgPool,
} }
impl KVStore for PostgresKV { impl PostgresKV {
fn get(&self, key: &str) -> FutureResultSend<Option<Bytes>, ServerError> { async fn transaction<F, O>(&self, f: F) -> Result<O, ServerError>
let pg_pool = self.pg_pool.clone(); where
let id = key.to_string(); F: for<'a> FnOnce(&'a mut DBTransaction<'_>) -> BoxFuture<'a, Result<O, ServerError>>,
FutureResultSend::new(async move { {
let mut transaction = pg_pool let mut transaction = self
.pg_pool
.begin() .begin()
.await .await
.context("[KV]:Failed to acquire a Postgres connection")?; .context("[KV]:Failed to acquire a Postgres connection")?;
let result = f(&mut transaction).await;
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
result
}
}
impl KVStore for PostgresKV {}
pub(crate) struct PostgresTransaction<'a> {
pub(crate) transaction: DBTransaction<'a>,
}
impl<'a> PostgresTransaction<'a> {}
#[async_trait]
impl KVAction for PostgresKV {
async fn get(&self, key: &str) -> Result<Option<Bytes>, ServerError> {
let id = key.to_string();
self.transaction(|transaction| {
Box::pin(async move {
let (sql, args) = SqlBuilder::select(KV_TABLE) let (sql, args) = SqlBuilder::select(KV_TABLE)
.add_field("*") .add_field("*")
.and_where_eq("id", &id) .and_where_eq("id", &id)
.build()?; .build()?;
let result = sqlx::query_as_with::<Postgres, KVTable, PgArguments>(&sql, args) let result = sqlx::query_as_with::<Postgres, KVTable, PgArguments>(&sql, args)
.fetch_one(&mut transaction) .fetch_one(transaction)
.await; .await;
let result = match result { let result = match result {
@ -49,57 +77,38 @@ impl KVStore for PostgresKV {
_ => Err(map_sqlx_error(error)), _ => Err(map_sqlx_error(error)),
}, },
}; };
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
result result
}) })
})
.await
} }
fn set(&self, key: &str, bytes: Bytes) -> FutureResultSend<(), ServerError> { async fn set(&self, key: &str, bytes: Bytes) -> Result<(), ServerError> {
self.batch_set(vec![KeyValue { self.batch_set(vec![KeyValue {
key: key.to_string(), key: key.to_string(),
value: bytes, value: bytes,
}]) }])
.await
} }
fn delete(&self, key: &str) -> FutureResultSend<(), ServerError> { async fn remove(&self, key: &str) -> Result<(), ServerError> {
let pg_pool = self.pg_pool.clone();
let id = key.to_string(); let id = key.to_string();
self.transaction(|transaction| {
FutureResultSend::new(async move { Box::pin(async move {
let mut transaction = pg_pool
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
let (sql, args) = SqlBuilder::delete(KV_TABLE).and_where_eq("id", &id).build()?; let (sql, args) = SqlBuilder::delete(KV_TABLE).and_where_eq("id", &id).build()?;
let _ = sqlx::query_with(&sql, args) let _ = sqlx::query_with(&sql, args)
.execute(&mut transaction) .execute(transaction)
.await .await
.map_err(map_sqlx_error)?; .map_err(map_sqlx_error)?;
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok(()) Ok(())
}) })
})
.await
} }
fn batch_set(&self, kvs: Vec<KeyValue>) -> FutureResultSend<(), ServerError> { async fn batch_set(&self, kvs: Vec<KeyValue>) -> Result<(), ServerError> {
let pg_pool = self.pg_pool.clone(); self.transaction(|transaction| {
FutureResultSend::new(async move { Box::pin(async move {
let mut transaction = pg_pool
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
SqlBuilder::create(KV_TABLE).add_field("id").add_field("blob");
let mut builder = RawSqlBuilder::insert_into(KV_TABLE); let mut builder = RawSqlBuilder::insert_into(KV_TABLE);
let m_builder = builder.field("id").field("blob"); let m_builder = builder.field("id").field("blob");
@ -116,128 +125,78 @@ impl KVStore for PostgresKV {
let sql = m_builder.sql()?; let sql = m_builder.sql()?;
let _ = sqlx::query_with(&sql, args) let _ = sqlx::query_with(&sql, args)
.execute(&mut transaction) .execute(transaction)
.await .await
.map_err(map_sqlx_error)?; .map_err(map_sqlx_error)?;
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok::<(), ServerError>(()) Ok::<(), ServerError>(())
}) })
})
.await
} }
fn batch_get(&self, keys: Vec<String>) -> FutureResultSend<Vec<KeyValue>, ServerError> { async fn batch_get(&self, keys: Vec<String>) -> Result<Vec<KeyValue>, ServerError> {
let pg_pool = self.pg_pool.clone(); self.transaction(|transaction| {
FutureResultSend::new(async move { Box::pin(async move {
let mut transaction = pg_pool
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
let sql = RawSqlBuilder::select_from(KV_TABLE) let sql = RawSqlBuilder::select_from(KV_TABLE)
.field("id") .field("id")
.field("blob") .field("blob")
.and_where_in_quoted("id", &keys) .and_where_in_quoted("id", &keys)
.sql()?; .sql()?;
let rows = sqlx::query(&sql) let rows = sqlx::query(&sql).fetch_all(transaction).await.map_err(map_sqlx_error)?;
.fetch_all(&mut transaction)
.await
.map_err(map_sqlx_error)?;
let kvs = rows_to_key_values(rows); let kvs = rows_to_key_values(rows);
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok::<Vec<KeyValue>, ServerError>(kvs) Ok::<Vec<KeyValue>, ServerError>(kvs)
}) })
})
.await
} }
fn batch_get_start_with(&self, key: &str) -> FutureResultSend<Vec<KeyValue>, ServerError> { async fn batch_delete(&self, keys: Vec<String>) -> Result<(), ServerError> {
let pg_pool = self.pg_pool.clone(); self.transaction(|transaction| {
let prefix = key.to_owned(); Box::pin(async move {
FutureResultSend::new(async move { let sql = RawSqlBuilder::delete_from(KV_TABLE).and_where_in("id", &keys).sql()?;
let mut transaction = pg_pool let _ = sqlx::query(&sql).execute(transaction).await.map_err(map_sqlx_error)?;
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
Ok::<(), ServerError>(())
})
})
.await
}
async fn batch_get_start_with(&self, key: &str) -> Result<Vec<KeyValue>, ServerError> {
let prefix = key.to_owned();
self.transaction(|transaction| {
Box::pin(async move {
let sql = RawSqlBuilder::select_from(KV_TABLE) let sql = RawSqlBuilder::select_from(KV_TABLE)
.field("id") .field("id")
.field("blob") .field("blob")
.and_where_like_left("id", &prefix) .and_where_like_left("id", &prefix)
.sql()?; .sql()?;
let rows = sqlx::query(&sql) let rows = sqlx::query(&sql).fetch_all(transaction).await.map_err(map_sqlx_error)?;
.fetch_all(&mut transaction)
.await
.map_err(map_sqlx_error)?;
let kvs = rows_to_key_values(rows); let kvs = rows_to_key_values(rows);
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok::<Vec<KeyValue>, ServerError>(kvs) Ok::<Vec<KeyValue>, ServerError>(kvs)
}) })
}
fn batch_delete(&self, keys: Vec<String>) -> FutureResultSend<(), ServerError> {
let pg_pool = self.pg_pool.clone();
FutureResultSend::new(async move {
let mut transaction = pg_pool
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
let sql = RawSqlBuilder::delete_from(KV_TABLE).and_where_in("id", &keys).sql()?;
let _ = sqlx::query(&sql)
.execute(&mut transaction)
.await
.map_err(map_sqlx_error)?;
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok::<(), ServerError>(())
}) })
.await
} }
fn batch_delete_key_start_with(&self, keyword: &str) -> FutureResultSend<(), ServerError> { async fn batch_delete_key_start_with(&self, keyword: &str) -> Result<(), ServerError> {
let pg_pool = self.pg_pool.clone();
let keyword = keyword.to_owned(); let keyword = keyword.to_owned();
FutureResultSend::new(async move { self.transaction(|transaction| {
let mut transaction = pg_pool Box::pin(async move {
.begin()
.await
.context("[KV]:Failed to acquire a Postgres connection")?;
let sql = RawSqlBuilder::delete_from(KV_TABLE) let sql = RawSqlBuilder::delete_from(KV_TABLE)
.and_where_like_left("id", &keyword) .and_where_like_left("id", &keyword)
.sql()?; .sql()?;
let _ = sqlx::query(&sql) let _ = sqlx::query(&sql).execute(transaction).await.map_err(map_sqlx_error)?;
.execute(&mut transaction)
.await
.map_err(map_sqlx_error)?;
transaction
.commit()
.await
.context("[KV]:Failed to commit SQL transaction.")?;
Ok::<(), ServerError>(()) Ok::<(), ServerError>(())
}) })
})
.await
} }
} }

View File

@ -1,11 +1,13 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
mod kv; mod kv;
use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use futures_core::future::BoxFuture;
pub(crate) use kv::*; pub(crate) use kv::*;
use backend_service::errors::ServerError; use backend_service::errors::ServerError;
use lib_infra::future::FutureResultSend; use lib_infra::future::{BoxResultFuture, FutureResultSend};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyValue { pub struct KeyValue {
@ -13,14 +15,27 @@ pub struct KeyValue {
pub value: Bytes, pub value: Bytes,
} }
pub trait KVStore: Send + Sync { // https://rust-lang.github.io/async-book/07_workarounds/05_async_in_traits.html
fn get(&self, key: &str) -> FutureResultSend<Option<Bytes>, ServerError>; // Note that using these trait methods will result in a heap allocation
fn set(&self, key: &str, value: Bytes) -> FutureResultSend<(), ServerError>; // per-function-call. This is not a significant cost for the vast majority of
fn delete(&self, key: &str) -> FutureResultSend<(), ServerError>; // applications, but should be considered when deciding whether to use this
fn batch_set(&self, kvs: Vec<KeyValue>) -> FutureResultSend<(), ServerError>; // functionality in the public API of a low-level function that is expected to
fn batch_get(&self, keys: Vec<String>) -> FutureResultSend<Vec<KeyValue>, ServerError>; // be called millions of times a second.
fn batch_get_start_with(&self, key: &str) -> FutureResultSend<Vec<KeyValue>, ServerError>; #[async_trait]
pub trait KVStore: KVAction + Send + Sync {}
fn batch_delete(&self, keys: Vec<String>) -> FutureResultSend<(), ServerError>; // pub trait KVTransaction
fn batch_delete_key_start_with(&self, keyword: &str) -> FutureResultSend<(), ServerError>;
#[async_trait]
pub trait KVAction: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Bytes>, ServerError>;
async fn set(&self, key: &str, value: Bytes) -> Result<(), ServerError>;
async fn remove(&self, key: &str) -> Result<(), ServerError>;
async fn batch_set(&self, kvs: Vec<KeyValue>) -> Result<(), ServerError>;
async fn batch_get(&self, keys: Vec<String>) -> Result<Vec<KeyValue>, ServerError>;
async fn batch_delete(&self, keys: Vec<String>) -> Result<(), ServerError>;
async fn batch_get_start_with(&self, key: &str) -> Result<Vec<KeyValue>, ServerError>;
async fn batch_delete_key_start_with(&self, keyword: &str) -> Result<(), ServerError>;
} }

View File

@ -23,7 +23,7 @@ async fn kv_delete_test() {
let key = "1"; let key = "1";
let _ = kv.set(key, s1.clone().into()).await.unwrap(); let _ = kv.set(key, s1.clone().into()).await.unwrap();
let _ = kv.delete(key).await.unwrap(); let _ = kv.remove(key).await.unwrap();
assert_eq!(kv.get(key).await.unwrap(), None); assert_eq!(kv.get(key).await.unwrap(), None);
} }

View File

@ -1,7 +1,7 @@
#![allow(clippy::all)] #![allow(clippy::all)]
#![cfg_attr(rustfmt, rustfmt::skip)] #![cfg_attr(rustfmt, rustfmt::skip)]
use std::convert::TryInto;
use actix_web::web::Data; use actix_web::web::Data;
use backend::services::doc::{crud::update_doc};
use flowy_document::services::doc::edit::ClientDocEditor as ClientEditDocContext; use flowy_document::services::doc::edit::ClientDocEditor as ClientEditDocContext;
use flowy_test::{helper::ViewTest, FlowySDKTest}; use flowy_test::{helper::ViewTest, FlowySDKTest};
use flowy_user::services::user::UserSession; use flowy_user::services::user::UserSession;
@ -14,9 +14,11 @@ use crate::util::helper::{spawn_server, TestServer};
use flowy_collaboration::{entities::doc::DocIdentifier, protobuf::ResetDocumentParams}; use flowy_collaboration::{entities::doc::DocIdentifier, protobuf::ResetDocumentParams};
use lib_ot::rich_text::{RichTextAttribute, RichTextDelta}; use lib_ot::rich_text::{RichTextAttribute, RichTextDelta};
use parking_lot::RwLock; use parking_lot::RwLock;
use flowy_collaboration::entities::revision::RepeatedRevision;
use lib_ot::core::Interval; use lib_ot::core::Interval;
use flowy_collaboration::core::sync::ServerDocManager;
use flowy_net::services::ws::WsManager; use flowy_net::services::ws::FlowyWSConnect;
use crate::util::helper::*;
pub struct DocumentTest { pub struct DocumentTest {
server: TestServer, server: TestServer,
@ -30,7 +32,7 @@ pub enum DocScript {
ClientOpenDoc, ClientOpenDoc,
AssertClient(&'static str), AssertClient(&'static str),
AssertServer(&'static str, i64), AssertServer(&'static str, i64),
ServerSaveDocument(String, i64), // delta_json, rev_id ServerSaveDocument(RepeatedRevision), // delta_json, rev_id
} }
impl DocumentTest { impl DocumentTest {
@ -54,9 +56,8 @@ struct ScriptContext {
client_edit_context: Option<Arc<ClientEditDocContext>>, client_edit_context: Option<Arc<ClientEditDocContext>>,
client_sdk: FlowySDKTest, client_sdk: FlowySDKTest,
client_user_session: Arc<UserSession>, client_user_session: Arc<UserSession>,
ws_manager: Arc<WsManager>, ws_conn: Arc<FlowyWSConnect>,
server_doc_manager: Arc<ServerDocManager>, server: TestServer,
server_pg_pool: Data<PgPool>,
doc_id: String, doc_id: String,
} }
@ -70,9 +71,8 @@ impl ScriptContext {
client_edit_context: None, client_edit_context: None,
client_sdk, client_sdk,
client_user_session: user_session, client_user_session: user_session,
ws_manager, ws_conn: ws_manager,
server_doc_manager: server.app_ctx.document_core.manager.clone(), server,
server_pg_pool: Data::new(server.pg_pool.clone()),
doc_id, doc_id,
} }
} }
@ -97,7 +97,7 @@ async fn run_scripts(context: Arc<RwLock<ScriptContext>>, scripts: Vec<DocScript
match script { match script {
DocScript::ClientConnectWS => { DocScript::ClientConnectWS => {
// sleep(Duration::from_millis(300)).await; // sleep(Duration::from_millis(300)).await;
let ws_manager = context.read().ws_manager.clone(); let ws_manager = context.read().ws_conn.clone();
let user_session = context.read().client_user_session.clone(); let user_session = context.read().client_user_session.clone();
let token = user_session.token().unwrap(); let token = user_session.token().unwrap();
let _ = ws_manager.start(token).await.unwrap(); let _ = ws_manager.start(token).await.unwrap();
@ -123,16 +123,24 @@ async fn run_scripts(context: Arc<RwLock<ScriptContext>>, scripts: Vec<DocScript
}, },
DocScript::AssertServer(s, rev_id) => { DocScript::AssertServer(s, rev_id) => {
sleep(Duration::from_millis(100)).await; sleep(Duration::from_millis(100)).await;
// let doc_identifier = DocIdentifier {
// doc_id
// };
//
// let doc = context.read().server.read_doc()
// let pg_pool = context.read().server_pg_pool.clone(); // let pg_pool = context.read().server_pg_pool.clone();
let doc_manager = context.read().server_doc_manager.clone(); // let doc_manager = context.read().server_doc_manager.clone();
let edit_doc = doc_manager.get(&doc_id).await.unwrap(); // let edit_doc = doc_manager.get(&doc_id).await.unwrap();
let json = edit_doc.document_json().await.unwrap(); // let json = edit_doc.document_json().await.unwrap();
assert_eq(s, &json); // assert_eq(s, &json);
assert_eq!(edit_doc.rev_id().await.unwrap(), rev_id); // assert_eq!(edit_doc.rev_id().await.unwrap(), rev_id);
}, },
DocScript::ServerSaveDocument(json, rev_id) => { DocScript::ServerSaveDocument(repeated_revision) => {
let pg_pool = context.read().server_pg_pool.clone(); let pg_pool = Data::new(context.read().server.pg_pool.clone());
save_doc(&doc_id, json, rev_id, pg_pool).await; reset_doc(&doc_id, repeated_revision, pg_pool).await;
}, },
// DocScript::Sleep(sec) => { // DocScript::Sleep(sec) => {
// sleep(Duration::from_secs(sec)).await; // sleep(Duration::from_secs(sec)).await;
@ -166,10 +174,10 @@ async fn create_doc(flowy_test: &FlowySDKTest) -> String {
view_test.view.id view_test.view.id
} }
async fn save_doc(doc_id: &str, json: String, rev_id: i64, pool: Data<PgPool>) { async fn reset_doc(doc_id: &str, repeated_revision: RepeatedRevision, pool: Data<PgPool>) {
let pb: flowy_collaboration::protobuf::RepeatedRevision = repeated_revision.try_into().unwrap();
let mut params = ResetDocumentParams::new(); let mut params = ResetDocumentParams::new();
params.set_doc_id(doc_id.to_owned()); params.set_doc_id(doc_id.to_owned());
params.set_data(json); params.set_revisions(pb);
params.set_rev_id(rev_id); // let _ = reset_document_handler(pool.get_ref(), params).await.unwrap();
let _ = update_doc(pool.get_ref(), params).await.unwrap();
} }

View File

@ -1,5 +1,5 @@
use crate::document::edit_script::{DocScript, DocumentTest}; use crate::document_test::edit_script::{DocScript, DocumentTest};
use flowy_collaboration::core::document::{Document, FlowyDoc}; use flowy_collaboration::document::{Document, FlowyDoc};
use lib_ot::{core::Interval, rich_text::RichTextAttribute}; use lib_ot::{core::Interval, rich_text::RichTextAttribute};
#[rustfmt::skip] #[rustfmt::skip]

View File

@ -1,2 +1,2 @@
// mod edit_script; mod edit_script;
// mod edit_test; mod edit_test;

View File

@ -17,10 +17,9 @@ use sqlx::{Connection, Executor, PgConnection, PgPool};
use uuid::Uuid; use uuid::Uuid;
pub struct TestUserServer { pub struct TestUserServer {
pub pg_pool: PgPool, pub inner: TestServer,
pub user_token: Option<String>, pub user_token: Option<String>,
pub user_id: Option<String>, pub user_id: Option<String>,
pub client_server_config: ClientServerConfiguration,
} }
impl TestUserServer { impl TestUserServer {
@ -176,12 +175,12 @@ impl TestUserServer {
response response
} }
pub fn http_addr(&self) -> String { self.client_server_config.base_url() } pub fn http_addr(&self) -> String { self.inner.client_server_config.base_url() }
pub fn ws_addr(&self) -> String { pub fn ws_addr(&self) -> String {
format!( format!(
"{}/{}", "{}/{}",
self.client_server_config.ws_addr(), self.inner.client_server_config.ws_addr(),
self.user_token.as_ref().unwrap() self.user_token.as_ref().unwrap()
) )
} }
@ -190,10 +189,9 @@ impl TestUserServer {
impl std::convert::From<TestServer> for TestUserServer { impl std::convert::From<TestServer> for TestUserServer {
fn from(server: TestServer) -> Self { fn from(server: TestServer) -> Self {
TestUserServer { TestUserServer {
pg_pool: server.pg_pool, inner: server,
user_token: None, user_token: None,
user_id: None, user_id: None,
client_server_config: server.client_server_config,
} }
} }
} }
@ -203,6 +201,7 @@ pub async fn spawn_user_server() -> TestUserServer {
server server
} }
#[derive(Clone)]
pub struct TestServer { pub struct TestServer {
pub pg_pool: PgPool, pub pg_pool: PgPool,
pub app_ctx: AppContext, pub app_ctx: AppContext,

View File

@ -1 +1 @@
mod revision_test; // mod revision_test;

View File

@ -2,7 +2,7 @@ use bytes::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
use flowy_collaboration::{entities::prelude::*, errors::CollaborateError, sync::*}; use flowy_collaboration::{entities::prelude::*, errors::CollaborateError, sync::*};
// use flowy_net::services::ws::*; // use flowy_net::services::ws::*;
use lib_infra::future::FutureResultSend; use lib_infra::future::{BoxResultFuture, FutureResultSend};
use lib_ws::{WSModule, WebSocketRawMessage}; use lib_ws::{WSModule, WebSocketRawMessage};
use std::{ use std::{
convert::TryInto, convert::TryInto,
@ -61,10 +61,10 @@ impl std::default::Default for MockDocServerPersistence {
} }
impl DocumentPersistence for MockDocServerPersistence { impl DocumentPersistence for MockDocServerPersistence {
fn read_doc(&self, doc_id: &str) -> FutureResultSend<DocumentInfo, CollaborateError> { fn read_doc(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError> {
let inner = self.inner.clone(); let inner = self.inner.clone();
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
FutureResultSend::new(async move { Box::pin(async move {
match inner.get(&doc_id) { match inner.get(&doc_id) {
None => { None => {
// //
@ -78,16 +78,16 @@ impl DocumentPersistence for MockDocServerPersistence {
}) })
} }
fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> FutureResultSend<DocumentInfo, CollaborateError> { fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> BoxResultFuture<DocumentInfo, CollaborateError> {
let doc_id = doc_id.to_owned(); let doc_id = doc_id.to_owned();
FutureResultSend::new(async move { DocumentInfo::from_revisions(&doc_id, revisions) }) Box::pin(async move { DocumentInfo::from_revisions(&doc_id, revisions) })
} }
fn get_revisions(&self, _doc_id: &str, _rev_ids: Vec<i64>) -> FutureResultSend<Vec<Revision>, CollaborateError> { fn get_revisions(&self, _doc_id: &str, _rev_ids: Vec<i64>) -> BoxResultFuture<Vec<Revision>, CollaborateError> {
FutureResultSend::new(async move { Ok(vec![]) }) Box::pin(async move { Ok(vec![]) })
} }
fn get_doc_revisions(&self, _doc_id: &str) -> FutureResultSend<Vec<Revision>, CollaborateError> { unimplemented!() } fn get_doc_revisions(&self, _doc_id: &str) -> BoxResultFuture<Vec<Revision>, CollaborateError> { unimplemented!() }
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -12,7 +12,7 @@ use crate::{
use async_stream::stream; use async_stream::stream;
use dashmap::DashMap; use dashmap::DashMap;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use lib_infra::future::FutureResultSend; use lib_infra::future::{BoxResultFuture, FutureResultSend};
use lib_ot::rich_text::RichTextDelta; use lib_ot::rich_text::RichTextDelta;
use std::{convert::TryFrom, fmt::Debug, sync::Arc}; use std::{convert::TryFrom, fmt::Debug, sync::Arc};
use tokio::{ use tokio::{
@ -21,10 +21,10 @@ use tokio::{
}; };
pub trait DocumentPersistence: Send + Sync + Debug { pub trait DocumentPersistence: Send + Sync + Debug {
fn read_doc(&self, doc_id: &str) -> FutureResultSend<DocumentInfo, CollaborateError>; fn read_doc(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError>;
fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> FutureResultSend<DocumentInfo, CollaborateError>; fn create_doc(&self, doc_id: &str, revisions: Vec<Revision>) -> BoxResultFuture<DocumentInfo, CollaborateError>;
fn get_revisions(&self, doc_id: &str, rev_ids: Vec<i64>) -> FutureResultSend<Vec<Revision>, CollaborateError>; fn get_revisions(&self, doc_id: &str, rev_ids: Vec<i64>) -> BoxResultFuture<Vec<Revision>, CollaborateError>;
fn get_doc_revisions(&self, doc_id: &str) -> FutureResultSend<Vec<Revision>, CollaborateError>; fn get_doc_revisions(&self, doc_id: &str) -> BoxResultFuture<Vec<Revision>, CollaborateError>;
} }
pub struct ServerDocumentManager { pub struct ServerDocumentManager {

View File

@ -1,4 +1,4 @@
use futures_core::ready; use futures_core::{future::BoxFuture, ready};
use pin_project::pin_project; use pin_project::pin_project;
use std::{ use std::{
fmt::Debug, fmt::Debug,
@ -63,6 +63,8 @@ where
} }
} }
pub type BoxResultFuture<'a, T, E> = BoxFuture<'a, Result<T, E>>;
#[pin_project] #[pin_project]
pub struct FutureResultSend<T, E> { pub struct FutureResultSend<T, E> {
#[pin] #[pin]