Don't duplicate events
This commit is contained in:
parent
4d8fc77665
commit
45a6b1c115
7 changed files with 75 additions and 100 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{
|
use super::{
|
||||||
Database, Transaction,
|
Database, Transaction,
|
||||||
models::{EventRecord, FileManifest, StoredDocumentVersion, VaultUpdateId},
|
models::{FileManifest, StoredDocumentVersion, VaultUpdateId},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|
@ -59,13 +59,4 @@ impl Database {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_event(tx: &mut Transaction<'_>, event: &EventRecord) -> Result<()> {
|
|
||||||
sqlx::query("UPDATE events SET event_json = ? WHERE event_id = ?")
|
|
||||||
.bind(serde_json::to_string(event)?)
|
|
||||||
.bind(event.event_id)
|
|
||||||
.execute(&mut **tx)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
use super::{
|
use super::{
|
||||||
Database, Transaction,
|
Database, Transaction,
|
||||||
models::{
|
models::{
|
||||||
DocumentId, DocumentVersionWithoutContent, EventBatch, FileManifest, StoredDocumentVersion,
|
DocumentId, DocumentVersionWithoutContent, EventBatch, EventRecord, FileManifest,
|
||||||
VaultId, VaultSnapshot, VaultUpdateId,
|
StoredDocumentVersion, VaultEvent, VaultId, VaultSnapshot, VaultUpdateId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, ensure};
|
use anyhow::{Context as _, Result, ensure};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn get_request_event(
|
pub async fn get_request_event(
|
||||||
tx: &mut Transaction<'_>,
|
tx: &mut Transaction<'_>,
|
||||||
request_id: uuid::Uuid,
|
request_id: uuid::Uuid,
|
||||||
) -> Result<Option<(Vec<u8>, String)>> {
|
) -> Result<Option<(Vec<u8>, VaultEvent)>> {
|
||||||
Ok(sqlx::query_as(
|
let row: Option<(Vec<u8>, VaultUpdateId)> =
|
||||||
"SELECT request_fingerprint, event_json FROM events WHERE request_id = ?",
|
sqlx::query_as("SELECT request_fingerprint, event_id FROM events WHERE request_id = ?")
|
||||||
)
|
|
||||||
.bind(request_id.to_string())
|
.bind(request_id.to_string())
|
||||||
.fetch_optional(&mut **tx)
|
.fetch_optional(&mut **tx)
|
||||||
.await?)
|
.await?;
|
||||||
|
match row {
|
||||||
|
Some((fingerprint, event_id)) => {
|
||||||
|
Ok(Some((fingerprint, Self::event_by_id(tx, event_id).await?)))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_document_version(
|
pub async fn get_latest_document_version(
|
||||||
|
|
@ -70,6 +75,24 @@ impl Database {
|
||||||
return Ok(FileManifest::default());
|
return Ok(FileManifest::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Self::file_manifest_by_id(tx, file_manifest_id)
|
||||||
|
.await?
|
||||||
|
.context("Latest file manifest is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn file_manifest_by_id(
|
||||||
|
tx: &mut Transaction<'_>,
|
||||||
|
file_manifest_id: VaultUpdateId,
|
||||||
|
) -> Result<Option<FileManifest>> {
|
||||||
|
let exists = sqlx::query_scalar::<_, VaultUpdateId>(
|
||||||
|
"SELECT file_manifest_id FROM file_manifests WHERE file_manifest_id = ?",
|
||||||
|
)
|
||||||
|
.bind(file_manifest_id)
|
||||||
|
.fetch_optional(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
if exists.is_none() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
let entries = sqlx::query_as::<_, (uuid::fmt::Hyphenated, String)>(
|
let entries = sqlx::query_as::<_, (uuid::fmt::Hyphenated, String)>(
|
||||||
"SELECT document_id, path FROM file_manifest_entries WHERE file_manifest_id = ?",
|
"SELECT document_id, path FROM file_manifest_entries WHERE file_manifest_id = ?",
|
||||||
)
|
)
|
||||||
|
|
@ -80,10 +103,28 @@ impl Database {
|
||||||
.map(|(document_id, path)| (document_id.into_uuid(), path))
|
.map(|(document_id, path)| (document_id.into_uuid(), path))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(FileManifest {
|
Ok(Some(FileManifest {
|
||||||
file_manifest_id,
|
file_manifest_id,
|
||||||
entries,
|
entries,
|
||||||
})
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn event_by_id(tx: &mut Transaction<'_>, event_id: VaultUpdateId) -> Result<VaultEvent> {
|
||||||
|
let document = sqlx::query_as::<_, DocumentVersionWithoutContent>(
|
||||||
|
"SELECT vault_update_id, document_id, updated_date, user_id, device_id,
|
||||||
|
length(content) AS content_size
|
||||||
|
FROM documents WHERE vault_update_id = ?",
|
||||||
|
)
|
||||||
|
.bind(event_id)
|
||||||
|
.fetch_optional(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
if let Some(document) = document {
|
||||||
|
return Ok(VaultEvent::Content { document });
|
||||||
|
}
|
||||||
|
let file_manifest = Self::file_manifest_by_id(tx, event_id)
|
||||||
|
.await?
|
||||||
|
.context("Event has no normalized content or file manifest")?;
|
||||||
|
Ok(VaultEvent::FileManifest { file_manifest })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn latest_event_id(tx: &mut Transaction<'_>) -> Result<VaultUpdateId> {
|
pub async fn latest_event_id(tx: &mut Transaction<'_>) -> Result<VaultUpdateId> {
|
||||||
|
|
@ -126,17 +167,21 @@ impl Database {
|
||||||
"Event cursor is outside this vault's history"
|
"Event cursor is outside this vault's history"
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows: Vec<String> = sqlx::query_scalar(
|
let rows: Vec<(VaultUpdateId, String)> = sqlx::query_as(
|
||||||
"SELECT event_json FROM events WHERE event_id > ? ORDER BY event_id",
|
"SELECT event_id, request_id FROM events WHERE event_id > ? ORDER BY event_id",
|
||||||
)
|
)
|
||||||
.bind(after)
|
.bind(after)
|
||||||
.fetch_all(&mut *tx)
|
.fetch_all(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let events = rows
|
let mut events = Vec::with_capacity(rows.len());
|
||||||
.iter()
|
for (event_id, request_id) in rows {
|
||||||
.map(|json| serde_json::from_str(json))
|
events.push(EventRecord {
|
||||||
.collect::<Result<_, _>>()?;
|
event_id,
|
||||||
|
request_id: request_id.parse()?,
|
||||||
|
event: Self::event_by_id(&mut tx, event_id).await?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(EventBatch {
|
Ok(EventBatch {
|
||||||
head_event_id,
|
head_event_id,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use models::{DocumentId, EventRecord, FileManifest, StoredDocumentVersion, VaultEvent};
|
use models::{DocumentId, FileManifest, StoredDocumentVersion};
|
||||||
|
|
||||||
fn config(directory: &tempfile::TempDir) -> DatabaseConfig {
|
fn config(directory: &tempfile::TempDir) -> DatabaseConfig {
|
||||||
DatabaseConfig {
|
DatabaseConfig {
|
||||||
|
|
@ -32,18 +32,6 @@ async fn insert_content(
|
||||||
Database::insert_document_version(&mut tx, &version)
|
Database::insert_document_version(&mut tx, &version)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Database::write_event(
|
|
||||||
&mut tx,
|
|
||||||
&EventRecord {
|
|
||||||
event_id: version.vault_update_id,
|
|
||||||
request_id,
|
|
||||||
event: VaultEvent::Content {
|
|
||||||
document: (&version).into(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
|
|
@ -133,18 +121,6 @@ async fn snapshot_returns_latest_referenced_metadata_and_byte_lengths() {
|
||||||
Database::insert_file_manifest(&mut tx, &manifest)
|
Database::insert_file_manifest(&mut tx, &manifest)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Database::write_event(
|
|
||||||
&mut tx,
|
|
||||||
&EventRecord {
|
|
||||||
event_id: manifest.file_manifest_id,
|
|
||||||
request_id,
|
|
||||||
event: VaultEvent::FileManifest {
|
|
||||||
file_manifest: manifest.clone(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
let snapshot = database.vault_snapshot(&vault).await.unwrap();
|
let snapshot = database.vault_snapshot(&vault).await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
database::{
|
database::{
|
||||||
Database,
|
Database,
|
||||||
models::{DocumentId, EventRecord, StoredDocumentVersion, VaultEvent},
|
models::{DocumentId, StoredDocumentVersion, VaultEvent},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config::user_config::User,
|
config::user_config::User,
|
||||||
|
|
@ -123,18 +123,6 @@ pub async fn put_file_content(
|
||||||
|
|
||||||
let response = DocumentUpdateResponse::Accepted((&version).into());
|
let response = DocumentUpdateResponse::Accepted((&version).into());
|
||||||
|
|
||||||
let event = EventRecord {
|
|
||||||
event_id: version.vault_update_id,
|
|
||||||
request_id: push.request_id,
|
|
||||||
event: VaultEvent::Content {
|
|
||||||
document: version.into(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Database::write_event(&mut tx, &event)
|
|
||||||
.await
|
|
||||||
.map_err(server_error)?;
|
|
||||||
|
|
||||||
tx.commit()
|
tx.commit()
|
||||||
.await
|
.await
|
||||||
.map_err(|error| server_error(error.into()))?;
|
.map_err(|error| server_error(error.into()))?;
|
||||||
|
|
@ -494,7 +482,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn failed_event_commit_rolls_back_content_and_allows_retrying_the_request() {
|
async fn failed_normalized_event_write_rolls_back_and_allows_retrying_the_request() {
|
||||||
let vault = TestVault::new().await;
|
let vault = TestVault::new().await;
|
||||||
let mut tx = vault
|
let mut tx = vault
|
||||||
.state
|
.state
|
||||||
|
|
@ -502,7 +490,7 @@ mod tests {
|
||||||
.create_write_transaction(&vault.vault)
|
.create_write_transaction(&vault.vault)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
sqlx::query("CREATE TRIGGER reject_event BEFORE UPDATE ON events BEGIN SELECT RAISE(ABORT, 'simulated event write failure'); END")
|
sqlx::query("CREATE TRIGGER reject_event BEFORE INSERT ON documents BEGIN SELECT RAISE(ABORT, 'simulated event write failure'); END")
|
||||||
.execute(&mut *tx).await.unwrap();
|
.execute(&mut *tx).await.unwrap();
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
database::{
|
database::{
|
||||||
Database,
|
Database,
|
||||||
models::{EventRecord, FileManifest, VaultEvent},
|
models::{FileManifest, VaultEvent},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors::{SyncServerError, client_error, server_error},
|
errors::{SyncServerError, client_error, server_error},
|
||||||
|
|
@ -91,16 +91,6 @@ pub async fn put_file_manifest(
|
||||||
file_manifest_id: file_manifest.file_manifest_id,
|
file_manifest_id: file_manifest.file_manifest_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = EventRecord {
|
|
||||||
event_id: file_manifest.file_manifest_id,
|
|
||||||
request_id: push.request_id,
|
|
||||||
event: VaultEvent::FileManifest { file_manifest },
|
|
||||||
};
|
|
||||||
|
|
||||||
Database::write_event(&mut tx, &event)
|
|
||||||
.await
|
|
||||||
.map_err(server_error)?;
|
|
||||||
|
|
||||||
tx.commit()
|
tx.commit()
|
||||||
.await
|
.await
|
||||||
.map_err(|error| server_error(error.into()))?;
|
.map_err(|error| server_error(error.into()))?;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::database::{
|
app_state::database::{Database, Transaction, models::VaultEvent},
|
||||||
Database, Transaction,
|
|
||||||
models::{EventRecord, VaultEvent},
|
|
||||||
},
|
|
||||||
errors::{SyncServerError, client_error, server_error},
|
errors::{SyncServerError, client_error, server_error},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -13,7 +10,7 @@ pub(super) async fn find_already_processed_event(
|
||||||
request_id: uuid::Uuid,
|
request_id: uuid::Uuid,
|
||||||
fingerprint: &[u8],
|
fingerprint: &[u8],
|
||||||
) -> Result<Option<VaultEvent>, SyncServerError> {
|
) -> Result<Option<VaultEvent>, SyncServerError> {
|
||||||
let Some((stored_fingerprint, event_json)) = Database::get_request_event(tx, request_id)
|
let Some((stored_fingerprint, event)) = Database::get_request_event(tx, request_id)
|
||||||
.await
|
.await
|
||||||
.map_err(server_error)?
|
.map_err(server_error)?
|
||||||
else {
|
else {
|
||||||
|
|
@ -26,7 +23,5 @@ pub(super) async fn find_already_processed_event(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_json::from_str::<EventRecord>(&event_json)
|
Ok(Some(event))
|
||||||
.map(|record| Some(record.event))
|
|
||||||
.map_err(|error| server_error(error.into()))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,7 @@ pub fn validate_file_manifest(entries: &FileManifestEntries) -> Result<()> {
|
||||||
let mut nodes: BTreeMap<String, (String, bool)> = BTreeMap::new();
|
let mut nodes: BTreeMap<String, (String, bool)> = BTreeMap::new();
|
||||||
|
|
||||||
for path in entries.values() {
|
for path in entries.values() {
|
||||||
ensure!(
|
ensure!(!path.is_empty(), "Path must not be empty");
|
||||||
!path.is_empty() && path.len() <= 240,
|
|
||||||
"Path must contain 1–240 UTF-8 bytes"
|
|
||||||
);
|
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
path.nfc().collect::<String>() == *path,
|
path.nfc().collect::<String>() == *path,
|
||||||
|
|
@ -136,16 +133,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn enforces_the_path_length_limit_in_utf8_bytes() {
|
fn does_not_impose_a_cross_platform_total_path_limit() {
|
||||||
for path in ["a".repeat(240), "é".repeat(120)] {
|
for path in ["a".repeat(300), "é".repeat(200)] {
|
||||||
validate_file_manifest(&manifest(&[&path])).unwrap();
|
validate_file_manifest(&manifest(&[&path])).unwrap();
|
||||||
|
|
||||||
let too_long = format!("{path}a");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
validate_file_manifest(&manifest(&[&too_long])).is_err(),
|
|
||||||
"accepted a path over 240 bytes: {too_long:?}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue