diff --git a/sync-server/src/app_state/database.rs b/sync-server/src/app_state/database.rs index b47263bc..86f00d6d 100644 --- a/sync-server/src/app_state/database.rs +++ b/sync-server/src/app_state/database.rs @@ -5,13 +5,15 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result}; use log::info; use models::{ DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId, }; use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions, types::chrono::Utc}; +use crate::errors::{SyncServerError, database_error, server_error}; + pub mod models; /// Sentinel error indicating the `SQLite` database is busy (`SQLITE_BUSY`). @@ -20,6 +22,24 @@ pub mod models; #[error("Database is busy")] pub struct WriteBusyError; +/// Detects whether a `sqlx::Error` indicates the database is currently +/// unavailable for a retryable reason: a `SQLITE_BUSY` from the engine, or +/// a `PoolTimedOut` from our short acquire timeout. Both should surface as +/// 429 so the client retries instead of treating it as a server fault. +pub fn is_sqlite_busy_error(err: &sqlx::Error) -> bool { + match err { + sqlx::Error::Database(db_err) => { + // SQLITE_BUSY base code is 5. Extended codes share base 5. + let busy_by_code = db_err + .code() + .is_some_and(|c| c.parse::().is_ok_and(|n| n & 0xFF == 5)); + busy_by_code || db_err.message().contains("database is locked") + } + sqlx::Error::PoolTimedOut => true, + _ => false, + } +} + use sqlx::{ Pool, Sqlite, pool::PoolConnection, sqlite::SqliteConnection, sqlite::SqlitePoolOptions, }; @@ -32,7 +52,7 @@ use super::websocket::{ models::{WebSocketServerMessage, WebSocketVaultUpdate}, }; use crate::config::database_config::DatabaseConfig; -use crate::consts::IDLE_POOL_TIMEOUT; +use crate::consts::{IDLE_POOL_TIMEOUT, POOL_ACQUIRE_TIMEOUT}; fn duration_millis_u64(duration: Duration) -> u64 { u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) @@ -87,22 +107,16 @@ impl WriteTransaction { pool: &Pool, write_guard: tokio::sync::OwnedMutexGuard<()>, ) -> Result { - let mut conn = pool - .acquire() - .await - .context("Cannot acquire connection for write transaction")?; + let mut conn = match pool.acquire().await { + Ok(conn) => conn, + Err(e) if is_sqlite_busy_error(&e) => return Err(WriteBusyError.into()), + Err(e) => { + return Err(anyhow::Error::from(e) + .context("Cannot acquire connection for write transaction")); + } + }; if let Err(e) = sqlx::query("BEGIN IMMEDIATE").execute(&mut *conn).await { - let is_busy = match &e { - sqlx::Error::Database(db_err) => { - // SQLITE_BUSY base code is 5. Extended codes share base 5. - let busy_by_code = db_err - .code() - .is_some_and(|c| c.parse::().is_ok_and(|n| n & 0xFF == 5)); - busy_by_code || db_err.message().contains("database is locked") - } - _ => false, - }; - if is_busy { + if is_sqlite_busy_error(&e) { return Err(WriteBusyError.into()); } return Err(e).context("Cannot begin immediate transaction"); @@ -113,22 +127,24 @@ impl WriteTransaction { }) } - pub async fn commit(mut self) -> Result<()> { + pub async fn commit(mut self) -> Result<(), SyncServerError> { if let Some(mut conn) = self.conn.take() { sqlx::query("COMMIT") .execute(&mut *conn) .await - .context("Failed to commit transaction")?; + .context("Failed to commit transaction") + .map_err(database_error)?; } Ok(()) } - pub async fn rollback(mut self) -> Result<()> { + pub async fn rollback(mut self) -> Result<(), SyncServerError> { if let Some(mut conn) = self.conn.take() { sqlx::query("ROLLBACK") .execute(&mut *conn) .await - .context("Failed to rollback transaction")?; + .context("Failed to rollback transaction") + .map_err(database_error)?; } Ok(()) } @@ -265,10 +281,14 @@ impl Database { drop(init_conn); // Per-connection PRAGMAs shared by both reader and writer pools. - // journal_mode = WAL is a no-op on an already-WAL database. + // Database-level PRAGMAs (auto_vacuum, journal_mode) are deliberately + // omitted here: they require a write lock to verify or set, so issuing + // them on every new pool connection blocks behind any in-flight writer + // and can fail with SQLITE_BUSY just to open a connection. The init + // connection above set them once; the WAL mode persists in the database + // header, so subsequent opens pick it up automatically. let base_options = SqliteConnectOptions::new() .filename(file_name.clone()) - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .busy_timeout(Duration::from_secs(30)) .log_slow_statements(log::LevelFilter::Warn, Duration::from_secs(30)) // In WAL mode, NORMAL is safe: data survives OS crashes, only the @@ -292,6 +312,7 @@ impl Database { // Reader pool: multiple connections for concurrent reads. let reader = SqlitePoolOptions::new() .max_connections(config.max_connections_per_vault) + .acquire_timeout(POOL_ACQUIRE_TIMEOUT) .acquire_slow_threshold(Duration::from_secs(30)) // Disabled: the health-check query is subject to busy_timeout // and blocks all connection checkouts when a write is active, @@ -309,6 +330,7 @@ impl Database { // reader pool ensures writes never compete with reads for pool slots. let writer = SqlitePoolOptions::new() .max_connections(1) + .acquire_timeout(POOL_ACQUIRE_TIMEOUT) .acquire_slow_threshold(Duration::from_secs(30)) .test_before_acquire(false) .before_acquire(rollback_before_acquire) @@ -375,7 +397,10 @@ impl Database { Ok(self.get_vault_pools(vault).await?.reader) } - pub async fn create_write_transaction(&self, vault: &VaultId) -> Result { + pub async fn create_write_transaction( + &self, + vault: &VaultId, + ) -> Result { let write_lock = { let mut locks = self.write_locks.lock().await; locks @@ -384,8 +409,10 @@ impl Database { .clone() }; let write_guard = write_lock.lock_owned().await; - let pools = self.get_vault_pools(vault).await?; - WriteTransaction::new(&pools.writer, write_guard).await + let pools = self.get_vault_pools(vault).await.map_err(database_error)?; + WriteTransaction::new(&pools.writer, write_guard) + .await + .map_err(database_error) } /// Return the latest state of all documents in the vault, optionally @@ -397,7 +424,7 @@ impl Database { vault: &VaultId, up_to_vault_update_id: Option, connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { // `i64::MAX` makes the upper bound a no-op for callers that don't // care about an exact snapshot (they pass `None`). let upper = up_to_vault_update_id.unwrap_or(i64::MAX); @@ -423,7 +450,12 @@ impl Database { query.fetch_all(&mut *conn).await } else { query - .fetch_all(&self.get_connection_pool(vault).await?) + .fetch_all( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .context("Cannot fetch latest documents") @@ -441,6 +473,7 @@ impl Database { }) .collect() }) + .map_err(database_error) } /// Return the latest state of all documents (including deleted) in the @@ -454,7 +487,7 @@ impl Database { vault_update_id: VaultUpdateId, up_to_vault_update_id: Option, connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { // `i64::MAX` makes the upper bound a no-op for callers that don't // care about an exact snapshot (they pass `None`). let upper = up_to_vault_update_id.unwrap_or(i64::MAX); @@ -499,7 +532,12 @@ impl Database { query.fetch_all(&mut *conn).await } else { query - .fetch_all(&self.get_connection_pool(vault).await?) + .fetch_all( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .with_context(|| { @@ -519,13 +557,14 @@ impl Database { }) .collect() }) + .map_err(database_error) } pub async fn get_max_update_id_in_vault( &self, vault: &VaultId, connection: Option<&mut SqliteConnection>, - ) -> Result { + ) -> Result { let query = sqlx::query!( r#" select coalesce(max(vault_update_id), 0) as max_vault_update_id @@ -537,11 +576,17 @@ impl Database { query.fetch_one(&mut *conn).await } else { query - .fetch_one(&self.get_connection_pool(vault).await?) + .fetch_one( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .map(|row| row.max_vault_update_id) .context("Cannot fetch max update id in vault") + .map_err(database_error) } pub async fn get_latest_non_deleted_document_by_path( @@ -549,7 +594,7 @@ impl Database { vault: &VaultId, relative_path: &str, connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { let query = sqlx::query_as!( StoredDocumentVersion, r#" @@ -578,10 +623,16 @@ impl Database { query.fetch_optional(&mut *conn).await } else { query - .fetch_optional(&self.get_connection_pool(vault).await?) + .fetch_optional( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .context("Cannot fetch latest document version") + .map_err(database_error) } /// Find a doc whose CREATE was authored by this device with @@ -611,7 +662,7 @@ impl Database { last_seen_vault_update_id: VaultUpdateId, content: &[u8], connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { let query = sqlx::query_as!( StoredDocumentVersion, r#" @@ -646,10 +697,16 @@ impl Database { query.fetch_optional(&mut *conn).await } else { query - .fetch_optional(&self.get_connection_pool(vault).await?) + .fetch_optional( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .context("Cannot fetch lost-create candidate") + .map_err(database_error) } pub async fn get_latest_document( @@ -657,7 +714,7 @@ impl Database { vault: &VaultId, document_id: &DocumentId, connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { let document_id = document_id.as_hyphenated(); let query = sqlx::query_as!( StoredDocumentVersion, @@ -683,10 +740,16 @@ impl Database { query.fetch_optional(&mut *conn).await } else { query - .fetch_optional(&self.get_connection_pool(vault).await?) + .fetch_optional( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .context("Cannot fetch latest document version") + .map_err(database_error) } pub async fn get_document_version( @@ -694,7 +757,7 @@ impl Database { vault: &VaultId, vault_update_id: VaultUpdateId, connection: Option<&mut SqliteConnection>, - ) -> Result> { + ) -> Result, SyncServerError> { let query = sqlx::query_as!( StoredDocumentVersion, r#" @@ -718,10 +781,16 @@ impl Database { query.fetch_optional(&mut *conn).await } else { query - .fetch_optional(&self.get_connection_pool(vault).await?) + .fetch_optional( + &self + .get_connection_pool(vault) + .await + .map_err(database_error)?, + ) .await } .context("Cannot fetch document version") + .map_err(database_error) } // inserting the document must be the last step of the transaction @@ -730,7 +799,7 @@ impl Database { vault_id: &VaultId, version: &StoredDocumentVersion, mut transaction: WriteTransaction, - ) -> Result<()> { + ) -> Result<(), SyncServerError> { let document_id = version.document_id.as_hyphenated(); let query = sqlx::query!( r#" @@ -766,14 +835,12 @@ impl Database { let _send_guard = self.broadcasts.acquire_send_lock(vault_id).await; query - .execute(transaction.connection_mut()?) + .execute(transaction.connection_mut().map_err(server_error)?) .await - .context("Cannot insert document version")?; + .context("Cannot insert document version") + .map_err(database_error)?; - transaction - .commit() - .await - .context("Failed to commit transaction")?; + transaction.commit().await?; // Broadcast every commit to every connected client, including // the originator. The HTTP response is the originator's normal diff --git a/sync-server/src/app_state/websocket/utils.rs b/sync-server/src/app_state/websocket/utils.rs index 834684bb..ae75e3e3 100644 --- a/sync-server/src/app_state/websocket/utils.rs +++ b/sync-server/src/app_state/websocket/utils.rs @@ -65,13 +65,11 @@ pub async fn get_unseen_documents( .database .get_latest_documents_since(vault_id, update_id, Some(up_to_vault_update_id), None) .await - .map_err(server_error) } else { state .database .get_latest_documents(vault_id, Some(up_to_vault_update_id), None) .await - .map_err(server_error) } } diff --git a/sync-server/src/consts.rs b/sync-server/src/consts.rs index e03b848f..b92fb139 100644 --- a/sync-server/src/consts.rs +++ b/sync-server/src/consts.rs @@ -21,6 +21,10 @@ pub const DEFAULT_MAX_PENDING_WS_CONNECTIONS: usize = 128; pub const DEFAULT_LOG_DIRECTORY: &str = "logs"; pub const DEFAULT_LOG_ROTATION_INTERVAL: Duration = Duration::from_hours(24); pub const IDLE_POOL_TIMEOUT: Duration = Duration::from_mins(5); + +/// Fail fast on pool acquire so a transiently locked database surfaces as +/// a 429 in seconds, not after a 30s busy_timeout. Callers retry. +pub const POOL_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(5); pub const GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/sync-server/src/errors.rs b/sync-server/src/errors.rs index ef0d017d..4f27598b 100644 --- a/sync-server/src/errors.rs +++ b/sync-server/src/errors.rs @@ -163,15 +163,26 @@ pub fn too_many_requests_error(error: anyhow::Error) -> SyncServerError { SyncServerError::TooManyRequests(error) } -/// Maps a `create_write_transaction` error to 429 if the database is busy, -/// or 500 for all other failures. -pub fn write_transaction_error(error: anyhow::Error) -> SyncServerError { - if error - .downcast_ref::() - .is_some() - { +/// Maps a database-operation error to 429 if the database is busy or the +/// pool acquire timed out (both retryable), or 500 for all other failures. +pub fn database_error(error: anyhow::Error) -> SyncServerError { + if is_database_busy(&error) { too_many_requests_error(error) } else { server_error(error) } } + +fn is_database_busy(error: &anyhow::Error) -> bool { + if error + .downcast_ref::() + .is_some() + { + return true; + } + error.chain().any(|cause| { + cause + .downcast_ref::() + .is_some_and(crate::app_state::database::is_sqlite_busy_error) + }) +} diff --git a/sync-server/src/server/create_document.rs b/sync-server/src/server/create_document.rs index cd70c4e2..0812b052 100644 --- a/sync-server/src/server/create_document.rs +++ b/sync-server/src/server/create_document.rs @@ -14,7 +14,7 @@ use crate::{ database::models::{StoredDocumentVersion, VaultId}, }, config::user_config::User, - errors::{SyncServerError, client_error, server_error, write_transaction_error}, + errors::{SyncServerError, client_error, server_error}, server::{responses::DocumentUpdateResponse, update_document}, utils::{ find_first_available_path::find_first_available_path, is_binary::is_binary, @@ -49,8 +49,7 @@ pub async fn create_document( let mut transaction = state .database .create_write_transaction(&vault_id) - .await - .map_err(write_transaction_error)?; + .await?; let sanitized_relative_path = sanitize_path(&request.relative_path).map_err(client_error)?; let new_content = request.content.contents.to_vec(); @@ -62,8 +61,7 @@ pub async fn create_document( &sanitized_relative_path, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)?; + .await?; if let Some(latest_version) = latest_version { // Only merge with an existing document the client couldn't have @@ -131,8 +129,7 @@ pub async fn create_document( &new_content, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)? + .await? { info!( "Lost-create recovery: binding retry at `{sanitized_relative_path}` to existing doc {} (was at `{}`) in vault `{vault_id}` for device `{}`", @@ -161,8 +158,7 @@ pub async fn create_document( &vault_id, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)?; + .await?; let deduped_path = find_first_available_path( &vault_id, @@ -170,8 +166,7 @@ pub async fn create_document( &state.database, &mut transaction, ) - .await - .map_err(server_error)?; + .await?; if deduped_path != sanitized_relative_path { info!( @@ -198,8 +193,7 @@ pub async fn create_document( state .database .insert_document_version(&vault_id, &new_version, transaction) - .await - .map_err(server_error)?; + .await?; Ok(Json(DocumentUpdateResponse::FastForwardUpdate( new_version.into(), diff --git a/sync-server/src/server/delete_document.rs b/sync-server/src/server/delete_document.rs index 54360a3d..12e58f89 100644 --- a/sync-server/src/server/delete_document.rs +++ b/sync-server/src/server/delete_document.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, anyhow}; +use anyhow::anyhow; use axum::{ Extension, Json, extract::{Path, State}, @@ -16,7 +16,7 @@ use crate::{ }, }, config::user_config::User, - errors::{SyncServerError, not_found_error, server_error, write_transaction_error}, + errors::{SyncServerError, not_found_error, server_error}, utils::normalize::normalize, }; @@ -43,8 +43,7 @@ pub async fn delete_document( let mut transaction = state .database .create_write_transaction(&vault_id) - .await - .map_err(write_transaction_error)?; + .await?; let last_update_id = state .database @@ -52,8 +51,7 @@ pub async fn delete_document( &vault_id, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)?; + .await?; let latest_version = state .database @@ -62,26 +60,17 @@ pub async fn delete_document( &document_id, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)?; + .await?; let Some(latest_version) = latest_version else { - transaction - .rollback() - .await - .context("Failed to roll back transaction") - .map_err(server_error)?; + transaction.rollback().await?; return Err(not_found_error(anyhow!( "Document `{document_id}` not found in vault `{vault_id}`" ))); }; if latest_version.is_deleted { - transaction - .rollback() - .await - .context("Failed to roll back transaction") - .map_err(server_error)?; + transaction.rollback().await?; info!("Document `{document_id}` has already been deleted",); return Ok(Json(latest_version.into())); @@ -110,8 +99,7 @@ pub async fn delete_document( state .database .insert_document_version(&vault_id, &new_version, transaction) - .await - .map_err(server_error)?; + .await?; Ok(Json(new_version.into())) } diff --git a/sync-server/src/server/fetch_document_version.rs b/sync-server/src/server/fetch_document_version.rs index c30f1d76..657cea81 100644 --- a/sync-server/src/server/fetch_document_version.rs +++ b/sync-server/src/server/fetch_document_version.rs @@ -11,7 +11,7 @@ use crate::{ AppState, database::models::{DocumentId, DocumentVersion, VaultId, VaultUpdateId}, }, - errors::{SyncServerError, not_found_error, server_error}, + errors::{SyncServerError, not_found_error}, utils::normalize::normalize, }; @@ -40,8 +40,7 @@ pub async fn fetch_document_version( let result = state .database .get_document_version(&vault_id, vault_update_id, None) - .await - .map_err(server_error)? + .await? .map_or_else( || { Err(not_found_error(anyhow!( diff --git a/sync-server/src/server/fetch_document_version_content.rs b/sync-server/src/server/fetch_document_version_content.rs index 9fdd0ad8..f888c866 100644 --- a/sync-server/src/server/fetch_document_version_content.rs +++ b/sync-server/src/server/fetch_document_version_content.rs @@ -11,7 +11,7 @@ use crate::{ AppState, database::models::{DocumentId, VaultId, VaultUpdateId}, }, - errors::{SyncServerError, not_found_error, server_error}, + errors::{SyncServerError, not_found_error}, utils::normalize::normalize, }; @@ -40,8 +40,7 @@ pub async fn fetch_document_version_content( let result = state .database .get_document_version(&vault_id, vault_update_id, None) - .await - .map_err(server_error)? + .await? .map_or_else( || { Err(not_found_error(anyhow!( diff --git a/sync-server/src/server/update_document.rs b/sync-server/src/server/update_document.rs index ac2a2987..a071d1e9 100644 --- a/sync-server/src/server/update_document.rs +++ b/sync-server/src/server/update_document.rs @@ -22,9 +22,7 @@ use crate::{ }, }, config::user_config::User, - errors::{ - SyncServerError, client_error, not_found_error, server_error, write_transaction_error, - }, + errors::{SyncServerError, client_error, not_found_error, server_error}, server::requests::UpdateBinaryDocumentVersion, utils::{ find_first_available_path::find_first_available_path, is_binary::as_non_binary_text, @@ -58,8 +56,7 @@ pub async fn update_binary( let transaction = state .database .create_write_transaction(&vault_id) - .await - .map_err(write_transaction_error)?; + .await?; update_document( &parent_document.relative_path, @@ -104,8 +101,7 @@ pub async fn update_text( let transaction = state .database .create_write_transaction(&vault_id) - .await - .map_err(write_transaction_error)?; + .await?; update_document( &parent_document.relative_path, @@ -131,8 +127,7 @@ async fn get_parent_document( let parent = state .database .get_document_version(vault_id, parent_version_id, None) - .await - .map_err(server_error)? + .await? .map_or_else( || { Err(not_found_error(anyhow!( @@ -177,8 +172,7 @@ pub async fn update_document( &vault_id, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)?; + .await?; let latest_version = state .database @@ -187,8 +181,7 @@ pub async fn update_document( &document_id, Some(transaction.connection_mut().map_err(server_error)?), ) - .await - .map_err(server_error)? + .await? .map_or_else( || { Err(not_found_error(anyhow!( @@ -199,11 +192,7 @@ pub async fn update_document( )?; if latest_version.is_deleted { - transaction - .rollback() - .await - .context("Failed to roll back transaction") - .map_err(server_error)?; + transaction.rollback().await?; info!("Document `{document_id}` has been deleted, ignoring update to it",); return Ok(Json(DocumentUpdateResponse::FastForwardUpdate( @@ -221,11 +210,7 @@ pub async fn update_document( info!( "Document content is the same as the latest version for `{document_id}`, skipping update" ); - transaction - .rollback() - .await - .context("Failed to roll back transaction") - .map_err(server_error)?; + transaction.rollback().await?; return Ok(Json(DocumentUpdateResponse::FastForwardUpdate( latest_version.into(), @@ -289,8 +274,7 @@ pub async fn update_document( { let new_path = find_first_available_path(&vault_id, requested, &state.database, &mut transaction) - .await - .map_err(server_error)?; + .await?; if new_path != requested { info!( @@ -321,8 +305,7 @@ pub async fn update_document( state .database .insert_document_version(&vault_id, &new_version, transaction) - .await - .map_err(server_error)?; + .await?; Ok(Json(if is_same_as_request { DocumentUpdateResponse::FastForwardUpdate(new_version.into()) diff --git a/sync-server/src/server/websocket.rs b/sync-server/src/server/websocket.rs index 379f68fd..936eb483 100644 --- a/sync-server/src/server/websocket.rs +++ b/sync-server/src/server/websocket.rs @@ -162,8 +162,7 @@ async fn websocket( let cursor = state .database .get_max_update_id_in_vault(&vault_id, None) - .await - .map_err(server_error)?; + .await?; drop(send_guard); // Catch-up on versions committed while this client was offline, diff --git a/sync-server/src/utils/find_first_available_path.rs b/sync-server/src/utils/find_first_available_path.rs index 97361240..9e115eaf 100644 --- a/sync-server/src/utils/find_first_available_path.rs +++ b/sync-server/src/utils/find_first_available_path.rs @@ -1,6 +1,7 @@ use crate::app_state::database::{WriteTransaction, models::VaultId}; +use crate::errors::{SyncServerError, server_error}; use crate::utils::dedup_paths::dedup_paths; -use anyhow::{Result, anyhow}; +use anyhow::anyhow; use log::{debug, info}; pub async fn find_first_available_path( @@ -8,7 +9,7 @@ pub async fn find_first_available_path( sanitized_relative_path: &str, database: &crate::app_state::database::Database, transaction: &mut WriteTransaction, -) -> Result { +) -> Result { info!("Finding first available path for `{sanitized_relative_path}` in vault `{vault_id}`"); for candidate in dedup_paths(sanitized_relative_path) { debug!("Checking candidate path for deconflicting names: `{candidate}`"); @@ -16,7 +17,7 @@ pub async fn find_first_available_path( .get_latest_non_deleted_document_by_path( vault_id, &candidate, - Some(transaction.connection_mut()?), + Some(transaction.connection_mut().map_err(server_error)?), ) .await? .is_none() @@ -30,7 +31,7 @@ pub async fn find_first_available_path( ); } - Err(anyhow!( + Err(server_error(anyhow!( "No available path candidates produced for `{sanitized_relative_path}` in vault `{vault_id}`" - )) + ))) }