Atomic updates ⚛️
When you need to perform a series of related modifications as a single, indivisible unit, dbzero provides the dbzero.atomic() context manager. This powerful feature ensures that either all operations within the block succeed together, or none of them are applied, maintaining perfect data integrity.
Operations within an atomic block are isolated from the main application state. The changes are only merged and made permanent when the block exits successfully. This makes complex, multi-step updates safe and predictable.
# Create an object with a list
object_1 = MemoTestClass([0])
# All appends will be applied together as one single operation
with dbzero.atomic():
object_1.value.append(1)
object_1.value.append(2)
object_1.value.append(3)
# The changes are now visible and persisted together
assert object_1.value == [0, 1, 2, 3]Automatic Rollbacks
The key feature of dbzero.atomic() is its ability to automatically revert changes. If the block is exited due to an unhandled exception, or if you manually call atomic.cancel(), all modifications made within the block are instantly discarded, as if they never happened.
Cancelling Manually
You can gain fine-grained control by explicitly cancelling a transaction before the block ends.
object_1 = MemoTestClass(123)
with dbzero.atomic() as atomic:
object_1.value = 951
# The object's value is 951 inside the block
# Discard the changes made so far
atomic.cancel()
# The value is reverted to its original state
assert object_1.value == 123Cancelling on Exception
An unhandled exception will automatically and safely trigger a full rollback.
object_1 = MemoTestClass(123)
try:
with dbzero.atomic():
object_1.value = 951
raise Exception("Something went wrong!")
except Exception:
pass
# The update was automatically reverted due to the exception
assert object_1.value == 123Lightweight Locking: dbzero.locked()
For scenarios where you need to group operations into a single transaction but don't require automatic rollbacks, dbzero offers the dbzero.locked() context manager.
dbzero.locked() guarantees that the autocommit thread will not fire in the middle of your operations, ensuring they are all part of the same transaction state. However, it does not provide the safety net of automatic rollbacks. Modifications are applied immediately as they happen.
No Automatic Rollback: Use dbzero.locked() for performance-critical code paths where you manage state consistency manually. If an exception occurs, any changes made within the block before the exception will persist.
Tracking Modifications for Distributed Systems
A key feature of dbzero.locked(), especially for building distributed systems, is its ability to report exactly what was changed within the block. The context manager returns a lock object that you can inspect to see if any updates occurred, which data prefixes were affected, and their new state numbers.
This allows one part of your system to react precisely to changes made in another, for example, by broadcasting the new state numbers to other services.
dbzero.open("prefix-A", "rw")
dbzero.open("prefix-B", "rw")
# The 'lock' object will track any mutations
with dbzero.locked() as lock:
# Modify an object in prefix-A
obj_A = MemoScopedClass(1, prefix="prefix-A")
obj_A.value = 100
# After the block, inspect the mutation log
mutation_log = lock.get_mutation_log() # Returns a list of (prefix_name, new_state_num)
# The log shows that only prefix-A was modified
assert len(mutation_log) == 1
modified_prefix, new_state = mutation_log[0]
assert modified_prefix == "prefix-A"
# Now you can use this info to notify other services that
# prefix-A is at a new state.