TessariDBDocs
  • 0.35.3latest
  • 0.35.2
  • 0.35.1
  • 0.35.0
  • 0.34.4
  • 0.34.3-beta
  • 0.34.2-beta
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

Transactions

Snapshot isolation by default, serializable on request: reads at one snapshot plus the transaction's own writes, savepoints, and an atomic commit that refuses a conflict without taking a lock.

A transaction reads at its own snapshot, keeps its writes private, and commits them as one batch.

A transaction

use tessarikv::{Db, Error, KeyspaceId, ReadOptions, WriteOptions};

async fn reserve(db: &Db) -> tessarikv::Result<bool> {
    let mut transaction = db.begin()?;
    let stock = transaction.get(KeyspaceId::DEFAULT, b"sku:1001", &ReadOptions::default()).await?;
    if stock.is_none() {
        return Ok(false); // dropping the transaction discards it
    }
    transaction.put(KeyspaceId::DEFAULT, b"reservation:78", b"sku:1001")?;
    match transaction.commit(&WriteOptions::default()).await {
        Ok(_sequence) => Ok(true),
        // Someone wrote a key this transaction writes after it began. Nothing was written.
        Err(Error::Conflict) => Ok(false),
        Err(other) => Err(other),
    }
}
  • Reads (get, multi_get, scan, multi_scan) see the snapshot the transaction began at, plus its own writes.
  • Writes (put, delete, delete_range) stay private until the commit.
  • commit(&WriteOptions) writes everything as one atomic batch, with the durability the options name, and returns its sequence number.
  • Dropping a transaction without committing discards it.

Conflicts: first committer wins

When a transaction commits, it checks whether any key it writes was written by someone else after it began. If so, the commit is refused with Error::Conflict and writes nothing. Run the transaction again from begin; Error::is_retryable is true for a conflict.

The check takes no lock. A commit waits only for commits that claimed earlier sequence numbers to publish their keys.

Isolation levels

IsolationProtects againstAdmits
Snapshot (default)lost updates: two transactions writing the same keywrite skew: two transactions each reading what the other writes
Serializablewrite skew too: the commit is also refused when a key it read, or a key in a range it scanned, was written after it begannothing: committed transactions behave as if they ran one at a time, in commit order

Serializable transactions need DbOptions::serializable_transactions set when the database opens. Every write then also records the prefixes of the keys it writes, so a transaction can tell whether a range it scanned changed. That costs every write a few more atomic updates.

use tessarikv::{Db, DbOptions, Isolation, TransactionOptions};

async fn open_serializable() -> tessarikv::Result<Db> {
    let options = DbOptions::builder().serializable_transactions(true).build()?;
    Db::open("data/ledger", options).await
}

fn serializable() -> TransactionOptions {
    let mut options = TransactionOptions::default();
    options.isolation = Isolation::Serializable;
    options
}

Begin one with db.begin_with(&serializable())?.

Savepoints

savepoint() marks the transaction's writes so far. rollback_to(savepoint) discards every write made after it and keeps the ones before.

use tessarikv::{Db, KeyspaceId, WriteOptions};

async fn partial(db: &Db) -> tessarikv::Result<()> {
    let mut transaction = db.begin()?;
    transaction.put(KeyspaceId::DEFAULT, b"order:9", b"placed")?;
    let mark = transaction.savepoint();
    transaction.put(KeyspaceId::DEFAULT, b"coupon:9", b"applied")?;
    transaction.rollback_to(mark); // the coupon is gone, the order stays
    transaction.commit(&WriteOptions::default()).await?;
    Ok(())
}

A transaction holds its snapshot until it commits or is dropped, so a long-lived one keeps old versions on disk. See Reads and snapshots.