Transactions
Outside a transaction, every operation is its own commit. A with block makes
many operations one commit.
with db.transaction() as tx:
tx["users"]["bob"] = {"age": 41}
del tx["users"]["alice"]
The block commits on clean exit. Any exception aborts it and propagates:
with pytest.raises(RuntimeError):
with db.transaction() as tx:
tx["users"]["bob"] = {"age": 41}
raise RuntimeError("boom")
"bob" in db["users"] # False
Scope is the handle you hold
A collection from db runs each operation in its own transaction. A collection
from tx participates in that transaction. There is no ambient state — the
object you are holding tells you the scope you are in.
users = db["users"] # its own commit per operation
with db.transaction() as tx:
tusers = tx["users"] # part of the transaction
Reads through a transaction handle see the transaction's own uncommitted writes.
Atomicity of update()
This is a real difference from dict, and worth stating plainly. Outside a
transaction, update() is a sequence of independent commits, so a failure part
way through leaves the earlier writes in place:
c.update({"a": {"n": 1}, "b": 42, "c": {"n": 3}}) # 42 is not a mapping
# TypeError — and "a" is already stored
"a" in c # True
"c" in c # False
Inside a transaction the same call is all-or-nothing:
with db.transaction() as tx:
tx["t"].update({"a": {"n": 1}, "b": 42, "c": {"n": 3}})
# TypeError, and nothing was written
Concurrency
Readers never block. redb is MVCC, so a read snapshot is unaffected by concurrent writes — including a long iteration:
it = iter(c.items())
next(it)
c[99] = {"n": 99} # concurrent write
list(it) # the original snapshot, without 99
Writers serialize. One write transaction runs at a time. A writer that
cannot start within timeout raises BusyError rather than waiting forever:
db = monadb.open("app.db", timeout=5.0) # seconds; the default
try:
with db.transaction() as tx:
...
except monadb.BusyError:
... # another writer held the lock too long
Re-entry raises immediately
Opening a transaction while one is already open on the same thread raises
TransactionError at once, rather than waiting out a timeout that could never
clear:
with db.transaction():
db.transaction() # TransactionError
The same applies to an implicit write from inside a block:
with db.transaction() as tx:
db["users"]["x"] = {} # TransactionError, not a hang
Both cases would otherwise be a thread waiting on itself. Neither can hang.
Durability
Commits are durable by default. For bulk loads, where you would rather redo the work than pay for every commit:
db = monadb.open("app.db", durable=False)
One writing process
redb takes an exclusive file lock, so only one process may open a database for writing. Several processes may open the same file read-only.
This is the one capability MonaDB 0.2 gives up relative to 0.1, which used LMDB and permitted multi-process read-write access. If you need several processes writing the same data, MonaDB is not the right tool.