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.