Transactions

Bloop supports reading and updating items in transactions similar to the way you already load, save, and delete items using an engine. A single read or write transaction can have at most 10 items.

To create a new transaction, call Engine.transaction(mode="w") and specify a mode:

wx = engine.transaction(mode="w")
rx = engine.transaction(mode="r")

When used as a context manager the transaction will call commit() on exit if no exception occurs:

# mode defaults to "w"
with engine.transaction() as tx:
    tx.save(some_obj)
    tx.delete(other_obj)


# read transaction loads all objects at once
user = User(id="numberoverzero")
meta = Metadata(id=to_load.id)
with engine.transaction(mode="r") as tx:
    tx.load(user, meta)

You may also call prepare() and commit() yourself:

import bloop

tx = engine.transaction()
tx.save(some_obj)
p = tx.prepare()
try:
    p.commit()
except bloop.TransactionCanceled:
    print("failed to commit")

See TransactionCanceled for the conditions that can cause each type of transaction to fail.

Write Transactions

A write transaction can save and delete items, and specify additional conditions on objects not being modified.

As with Engine.save and Engine.delete you can provide multiple objects to each WriteTransaction.save() or WriteTransaction.delete() call:

with engine.transaction() as tx:
    tx.delete(*old_tweets)
    tx.save(new_user, new_tweet)

Item Conditions

You can specify a condition with each save or delete call:

with engine.transaction() as tx:
    tx.delete(auth_token, condition=Token.last_used <= now())

Transaction Conditions

In addition to specifying conditions on the objects being modified, you can also specify a condition for the transaction on an object that won't be modified. This can be useful if you want to check another table without changing its value:

user_meta = Metadata(id="numberoverzero")

with engine.transaction() as tx:
    tx.save(new_tweet)
    tx.check(user_meta, condition=Metadata.verified.is_(True))

In the above example the transaction doesn't modify the user metadata. If we want to modify that object we should instead use a condition on the object being modified:

user_meta = Metadata(id="numberoverzero")
engine.load(user_meta)
user_meta.tweets += 1

with engine.transaction() as tx:
    tx.save(new_tweet)
    tx.save(user_meta, condition=Metadata.tweets <= 500)

Idempotency

Bloop automatically generates timestamped unique tokens (tx_id and first_commit_at) to guard against committing a write transaction twice or accidentally committing a transaction that was prepared a long time ago. While these are generated for both read and write commits, only TransactWriteItems respects the "ClientRequestToken" stored in tx_id.

When the first_commit_at value is too old, committing will raise TransactionTokenExpired.

Read Transactions

By default engine.transaction(mode="w") will create a WriteTransaction. To create a ReadTransaction pass mode="r":

with engine.transaction(mode="r") as rx:
    rx.load(user, tweet)
    rx.load(meta)

All objects in the read transaction will be loaded at the same time, when commit() is called or the transaction context closes.

Multiple Commits

Every time you call commit on the prepared transaction, the objects will be loaded again:

rx = engine.transaction(mode="r")
rx.load(user, tweet)
prepared = rx.prepare()

prepared.commit()  # first load
prepared.commit()  # second load

Missing Objects

As with Engine.load if any objects in the transaction are missing when commit is called, bloop will raise MissingObjects with the list of objects that were not found:

import bloop

engine = bloop.Engine()
...


def tx_load(*objs):
    with engine.transaction(mode="r") as rx:
        rx.load(*objs)

...

try:
    tx_load(user, tweet)
except bloop.MissingObjects as exc:
    missing = exc.objects
    print(f"failed to load {len(missing)} objects: {missing}")