Atomic updates

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.

atomic_success.py
# 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.

atomic_cancel.py
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 == 123

Cancelling on Exception

An unhandled exception will automatically and safely trigger a full rollback.

atomic_exception.py
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 == 123

Async Atomic Operations

dbzero-procommercial edition

Use dbzero.async_atomic() in asyncio code. It provides the same all-or-nothing mutation semantics as dbzero.atomic(), but waits cooperatively when another async task already owns an atomic block instead of blocking the event loop.

async_atomic_success.py
async def update_document(document, audit_log):
    async with dbzero.async_atomic():
        document.status = "approved"
        audit_log.append(("approved", document.id))
        await notify_reviewers(document)
        document.review_notified = True

Normal exit stages the changes for commit. An exception cancels the block and rolls back changes, and you can still cancel explicitly:

async_atomic_cancel.py
async def reserve_inventory(item, quantity):
    async with dbzero.async_atomic() as atomic:
        item.reserved += quantity
        await check_payment_hold()
 
        if item.reserved > item.available:
            atomic.cancel()
            return False
 
    return True

dbzero.atomic() is synchronous and is rejected inside an asyncio task, even if the block does not contain an await:

async def update(obj):
    # Raises RuntimeError: use dbzero.async_atomic() inside asyncio tasks.
    with dbzero.atomic():
        obj.value = 1

Concurrent async atomic blocks are serialized by awaiting an asyncio lock:

async def worker(task):
    async with dbzero.async_atomic():
        task.status = "running"
        await run_external_step(task)
        task.status = "done"
⚠️

While one asyncio task owns async_atomic(), unguarded dbzero mutations from another task on the same event loop may raise immediately. Wrap participating async mutations in async with dbzero.async_atomic(): so they serialize safely.


Lightweight 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.

locked_tracking.py
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.