Transactions

pyrex.TransactionDB opens RocksDB with pessimistic transaction support. Use it when multiple writes and reads must be grouped into one atomic unit.

Basic Transaction

Transactions use explicit commit semantics. The context manager rolls back if the transaction is still active when the with block exits.

import pyrex

with pyrex.TransactionDB("example_txn_db") as db:
    with db.transaction() as txn:
        txn.put(b"k", b"v")
        assert txn.get(b"k") == b"v"
        txn.commit()

    assert db.get(b"k") == b"v"

Rollback On Exception

If an exception leaves the transaction block before commit() is called, __exit__ rolls the transaction back.

import pyrex

with pyrex.TransactionDB("example_txn_db") as db:
    try:
        with db.transaction() as txn:
            txn.put(b"k", b"temporary")
            raise RuntimeError("abort")
    except RuntimeError:
        pass

    assert db.get(b"k") is None

Write Batch Inside A Transaction

Existing PyWriteBatch objects can be applied inside a transaction. The current transaction batch support covers default column-family put and delete operations.

import pyrex

with pyrex.TransactionDB("example_txn_db") as db:
    batch = pyrex.PyWriteBatch()
    batch.put(b"a", b"1")
    batch.delete(b"old")

    with db.transaction() as txn:
        txn.write(batch)
        txn.commit()

Transaction Iterators

Transaction iterators see the transaction view, including local writes and deletes where RocksDB supports them. Prefix scans are performed by seeking to a prefix and stopping when keys no longer match.

import pyrex

with pyrex.TransactionDB("example_txn_db") as db:
    with db.transaction() as txn:
        txn.put(b"user:1", b"alice")
        txn.put(b"user:2", b"bob")

        it = txn.new_iterator()
        it.seek(b"user:")
        while it.valid() and it.key().startswith(b"user:"):
            print(it.key(), it.value())
            it.next()

        txn.rollback()

Read For Update

get_for_update reads a key and tracks it for transaction conflict checking. Pass read_value=False to lock or track the key without fetching its value.

import pyrex

with pyrex.TransactionDB("example_txn_db") as db:
    db.put(b"account:alice", b"100")

    with db.transaction() as txn:
        assert txn.get_for_update(b"account:alice") == b"100"
        txn.get_for_update(b"account:bob", read_value=False)
        txn.commit()

Transaction Options

TransactionDBOptions controls database-level transaction settings such as lock timeouts and lock table sizing. TransactionOptions controls each transaction.

import pyrex

db_options = pyrex.TransactionDBOptions()
db_options.default_lock_timeout = 1000
db_options.transaction_lock_timeout = 1000

txn_options = pyrex.TransactionOptions()
txn_options.set_snapshot = True
txn_options.lock_timeout = 1000
txn_options.expiration = 30000
txn_options.deadlock_detect = True

with pyrex.TransactionDB("example_txn_db", None, db_options) as db:
    txn = db.begin_transaction(None, txn_options)
    txn.put(b"k", b"v")
    txn.commit()

Write Options And Durability

WriteOptions can be supplied when a transaction begins and when it commits. disable_wal is preserved, but disable_wal=True means a successful commit is not fully durable across process or machine crashes.

import pyrex

write_options = pyrex.WriteOptions()
write_options.sync = True

with pyrex.TransactionDB("example_txn_db") as db:
    with db.transaction(write_options) as txn:
        txn.put(b"durable", b"value")
        txn.commit(write_options)

Retryable Errors

Transaction lock conflicts and timeouts are mapped to specific Python exception subclasses so callers can decide whether to retry.

import pyrex

try:
    with pyrex.TransactionDB("example_txn_db") as db:
        with db.transaction() as txn:
            txn.put(b"k", b"v")
            txn.commit()
except (pyrex.RocksDBBusyError,
        pyrex.RocksDBTimeoutError,
        pyrex.RocksDBConflictError):
    # Retry according to the application's policy.
    raise

Complete Example

transactions.py
 1import os
 2import shutil
 3
 4import pyrex
 5
 6
 7db_path = "/tmp/pyrex_example_transactions"
 8if os.path.exists(db_path):
 9    shutil.rmtree(db_path)
10
11
12with pyrex.TransactionDB(db_path) as db:
13    # Context-manager transactions are explicit-commit: if commit() is not
14    # called, __exit__ rolls the transaction back.
15    with db.transaction() as txn:
16        txn.put(b"account:alice", b"100")
17        txn.put(b"account:bob", b"50")
18        assert txn.get(b"account:alice") == b"100"
19        txn.commit()
20
21    print(db.get(b"account:alice").decode())  # 100
22
23    # Rollback discards uncommitted changes.
24    with db.transaction() as txn:
25        txn.put(b"account:alice", b"0")
26
27    print(db.get(b"account:alice").decode())  # 100
28
29    # Existing PyWriteBatch objects can be applied inside a transaction.
30    batch = pyrex.PyWriteBatch()
31    batch.put(b"account:carol", b"25")
32    batch.delete(b"account:bob")
33
34    txn = db.begin_transaction()
35    txn.write(batch)
36    txn.commit()
37
38    print(db.get(b"account:carol").decode())  # 25
39    print(db.get(b"account:bob"))  # None
40
41    # Transaction iterators include transaction-local writes.
42    with db.transaction() as txn:
43        txn.put(b"account:dave", b"75")
44        it = txn.new_iterator()
45        it.seek(b"account:")
46        while it.valid() and it.key().startswith(b"account:"):
47            print(it.key(), it.value())
48            it.next()
49        txn.rollback()
50
51shutil.rmtree(db_path)