From 54dce0ba965e4c2434bef80ebc5a70d87a22610b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 9 Mar 2026 15:43:44 +0700 Subject: [PATCH] docs: document single-writer transaction requirement for GroveDb GroveDb uses RocksDB's OptimisticTransactionDB which permits multiple concurrent transactions but only detects write conflicts at commit time. Since GroveDb builds in-memory Merk tree state (hashes, balancing, root propagation) during transactions that cannot be cheaply rolled back on commit failure, callers must ensure at most one write transaction is active at any given time. This adds documentation to: - GroveDb struct: concurrency and transaction safety overview - GroveDb::start_transaction: single-writer requirement details - GroveDb::commit_transaction: failure semantics - GroveDb::rollback_transaction: state invalidation after rollback - Storage trait: single-writer constraint at the trait level - RocksDbStorage struct: optimistic transaction semantics Co-Authored-By: Claude Opus 4.6 --- grovedb/src/lib.rs | 60 +++++++++++++++++++++++--- storage/src/rocksdb_storage/storage.rs | 15 ++++++- storage/src/storage.rs | 28 +++++++++--- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/grovedb/src/lib.rs b/grovedb/src/lib.rs index 57edd68d5..972ebc1e6 100644 --- a/grovedb/src/lib.rs +++ b/grovedb/src/lib.rs @@ -262,7 +262,28 @@ use crate::Error::MerkError; #[cfg(feature = "minimal")] type Hash = [u8; 32]; -/// GroveDb +/// GroveDb is a hierarchical authenticated data structure database. +/// +/// # Concurrency and Transaction Safety +/// +/// `GroveDb` is `Send + Sync` because the underlying RocksDB +/// `OptimisticTransactionDB` is thread-safe at the storage level. However, +/// **GroveDb is designed for single-writer access**. Callers must ensure that +/// at most one write transaction is active at any given time. +/// +/// While RocksDB's optimistic transaction mechanism will detect conflicting +/// concurrent writes and fail one transaction at commit time (returning a +/// `Busy` or `TryAgain` error), GroveDb builds in-memory Merk tree state +/// (hashes, balancing, root propagation) during the transaction that cannot +/// be cheaply rolled back. A commit failure therefore requires the caller to +/// discard all in-memory state derived from that transaction and retry the +/// entire operation from scratch. +/// +/// Concurrent **reads** (queries, proofs) are safe alongside a single writer. +/// +/// In Dash Platform, the primary consumer of GroveDb, this constraint is +/// naturally satisfied because block processing (state transitions) is +/// sequential. pub struct GroveDb { #[cfg(feature = "minimal")] db: RocksDbStorage, @@ -757,8 +778,21 @@ impl GroveDb { Ok(self.db.flush()?) } - /// Starts database transaction. Please note that you have to start - /// underlying storage transaction manually. + /// Starts a new database transaction. + /// + /// # Single-Writer Requirement + /// + /// Only one write transaction should be active at a time. While the + /// underlying RocksDB `OptimisticTransactionDB` permits multiple + /// concurrent transactions, GroveDb does not enforce mutual exclusion + /// internally. If two write transactions run concurrently and touch + /// overlapping keys, one will fail at commit time with a RocksDB `Busy` + /// or `TryAgain` error. In that case, all in-memory Merk state built + /// during the failed transaction is invalid and must be discarded; the + /// operation must be retried from the beginning. + /// + /// Concurrent read-only operations (e.g., `get`, `query`, `prove`) are + /// safe to perform alongside a single active write transaction. /// /// ## Examples: /// ``` @@ -827,15 +861,27 @@ impl GroveDb { self.db.start_transaction() } - /// Commits previously started db transaction. For more details on the - /// transaction usage, please check [`GroveDb::start_transaction`] + /// Consumes and commits a previously started transaction. + /// + /// On success the transaction's writes become visible to subsequent + /// operations. On failure (e.g., a `Busy` error from an optimistic + /// concurrency conflict) the transaction is consumed and all in-memory + /// Merk state derived from it must be discarded. + /// + /// For more details on the transaction usage, please check + /// [`GroveDb::start_transaction`]. pub fn commit_transaction(&self, transaction: Transaction) -> CostResult<(), Error> { self.db.commit_transaction(transaction).map_err(Into::into) } - /// Rollbacks previously started db transaction to initial state. + /// Rolls back a previously started transaction to its initial state. + /// + /// After rollback, any in-memory Merk state derived from the transaction + /// is invalid and must be discarded. The transaction object itself remains + /// valid and can be reused for new operations. + /// /// For more details on the transaction usage, please check - /// [`GroveDb::start_transaction`] + /// [`GroveDb::start_transaction`]. pub fn rollback_transaction(&self, transaction: &Transaction) -> Result<(), Error> { Ok(self.db.rollback_transaction(transaction)?) } diff --git a/storage/src/rocksdb_storage/storage.rs b/storage/src/rocksdb_storage/storage.rs index 0d5248c26..23f65e51e 100644 --- a/storage/src/rocksdb_storage/storage.rs +++ b/storage/src/rocksdb_storage/storage.rs @@ -113,6 +113,16 @@ pub(crate) type Db = OptimisticTransactionDB; pub(crate) type Tx<'db> = Transaction<'db, Db>; /// Storage which uses RocksDB as its backend. +/// +/// Uses `OptimisticTransactionDB` for transaction support. Optimistic +/// transactions defer conflict detection to commit time rather than +/// acquiring locks up front. This means multiple transactions can be +/// started concurrently, but at most one write transaction should be +/// active at a time. If two transactions modify overlapping keys, the +/// later commit will fail with a `Busy` or `TryAgain` error. +/// +/// See the [`Storage`] trait documentation for the single-writer +/// requirement. pub struct RocksDbStorage { db: OptimisticTransactionDB, } @@ -508,7 +518,10 @@ impl<'db> Storage<'db> for RocksDbStorage { } fn commit_transaction(&self, transaction: Self::Transaction) -> CostResult<(), Error> { - // All transaction costs were provided on method calls + // All transaction costs were provided on method calls. + // Note: for OptimisticTransactionDB, commit() performs conflict + // validation and may return a Busy or TryAgain error if another + // transaction modified the same keys concurrently. transaction .commit() .map_err(RocksDBError) diff --git a/storage/src/storage.rs b/storage/src/storage.rs index 3545317a5..ce8019036 100644 --- a/storage/src/storage.rs +++ b/storage/src/storage.rs @@ -44,9 +44,19 @@ use grovedb_visualize::visualize_to_vec; use crate::{worst_case_costs::WorstKeyLength, Error}; pub type SubtreePrefix = [u8; 32]; -/// Top-level storage_cost abstraction. -/// Should be able to hold storage_cost connection and to start transaction when +/// Top-level storage abstraction. +/// Should be able to hold a storage connection and to start transactions when /// needed. All query operations will be exposed using [StorageContext]. +/// +/// # Single-Writer Constraint +/// +/// Implementations assume at most one write transaction is active at a time. +/// The RocksDB-backed implementation uses `OptimisticTransactionDB`, which +/// allows multiple concurrent transactions at the storage level but detects +/// write conflicts only at commit time. Upper layers (GroveDb) build +/// in-memory Merk tree state during a transaction that cannot be cheaply +/// unwound on commit failure. Callers must therefore serialize write +/// transactions externally. pub trait Storage<'db> { /// Storage transaction type type Transaction; @@ -58,13 +68,21 @@ pub trait Storage<'db> { /// is replication process. type ImmediateStorageContext: StorageContext<'db>; - /// Starts a new transaction + /// Starts a new transaction. + /// + /// Only one write transaction should be active at a time. See the + /// [trait-level documentation](Storage) for details. fn start_transaction(&'db self) -> Self::Transaction; - /// Consumes and commits a transaction + /// Consumes and commits a transaction. + /// + /// For the `OptimisticTransactionDB` backend, commit may fail with a + /// `Busy` or `TryAgain` error if a concurrent transaction modified the + /// same keys. On failure the transaction is consumed and the caller must + /// discard any derived in-memory state. fn commit_transaction(&self, transaction: Self::Transaction) -> CostResult<(), Error>; - /// Rollback a transaction + /// Rolls back a transaction, reverting its pending writes. fn rollback_transaction(&self, transaction: &Self::Transaction) -> Result<(), Error>; /// Consumes and applies multi-context batch.