Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions grovedb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
/// ```
Expand Down Expand Up @@ -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)?)
}
Expand Down
15 changes: 14 additions & 1 deletion storage/src/rocksdb_storage/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 23 additions & 5 deletions storage/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
Loading