Write your first transaction
Tasks can be grouped into a common transaction using thetransaction context manager:
pipeline(contents="hello world!") it will fail.
Importantly, after the flow has exited, there is no "side-effect.txt" file in
your working directory.
This is because the write_file task’s on_rollback hook was executed due to the transaction failing.
Transaction lifecycle
Every transaction goes through at most four lifecycle stages:- BEGIN: in this phase, the transaction’s key is computed and looked up. If a record already exists at the key location the transaction considers itself committed.
- STAGE: in this phase, the transaction stages a piece of data to be committed to its result location. Whether this data is committed or rolled back depends on the commit mode of the transaction.
- ROLLBACK: if the transaction encounters any error after staging, it rolls itself back and does not proceed to commit anything.
- COMMIT: in this final phase, the transaction writes its record to its configured location. At this point the transaction is complete.
- the larger transaction that begins when
with transaction()is executed; this transaction remains active throughout the duration of the subtransactions within it. - the transaction associated with the
write_filetask. Upon completion of thewrite_filetask, this transaction is now STAGED. - the transaction associated with the
quality_testtask. This transaction fails before it can be staged, causing a rollback in its parent transaction which then rolls back any staged subtransactions. In particular, the stagedwrite_file’s transaction is rolled back.
Configure commit timing
Thecommit_mode parameter controls when a transaction commits its staged result and runs its
on_commit hooks. Import CommitMode from prefect.transactions to choose a mode:
When you omit
commit_mode, a transaction inherits its parent’s mode, or uses LAZY if it has no parent.
This also applies to the transactions created by task runs. An explicit mode on a nested transaction
overrides the inherited mode.
Reuse cached results with eager commits
WithLAZY, tasks inside a transaction stage their results until the outer transaction commits.
With EAGER, each task commits when its own transaction exits, making its persisted result available
to subsequent calls before the outer block finishes.
The following example uses a cache policy based on task inputs and enables result persistence:
Computing 2 twice and Computing 3 once.
The blocks use different inputs so the lazy block’s results do not populate the eager block’s cache.
On subsequent runs, both blocks can reuse previously committed results.
Eager commits do not enable caching on their own. Reuse requires a matching cache key and an
unexpired, persisted result accessible to the next task run. See caching
for configuration details. Concurrent task calls can both start before either commits, so EAGER
alone does not guarantee that a task body executes only once.
Understand rollback behavior
Once a task’s transaction commits, it cannot be rolled back. WithEAGER, a later failure in the
outer transaction does not run rollback hooks for tasks that have already committed, and their
persisted results remain available. Their commit hooks have already run as well.
For example, changing the earlier pipeline to use transaction(commit_mode=CommitMode.EAGER)
would commit write_file before quality_test runs. If quality_test fails, del_file would not
run and the file would remain. With the default LAZY mode, write_file remains staged and
its rollback hook deletes the file when quality_test fails.
Use LAZY when completed tasks should remain eligible for rollback until the group succeeds.
Use EAGER when tasks should commit independently and make their results available sooner.
In either mode, rollback hooks must implement cleanup of external side effects, such as file writes
or API calls; Prefect does not automatically undo those operations.
Commit manually
UseOFF to control the commit explicitly:
txn.commit() in this example, the transaction rolls back when the block exits.
Calling commit() also commits child transactions. As with eager commits, a later exception
cannot roll back a transaction that you have already committed manually.
Idempotency
You can ensure sections of code are functionally idempotent by wrapping them in a transaction. By specifying akey for your transaction, you can ensure that
your code is executed only once.
For example, here’s a flow that downloads some data from an API and writes it to a file:
key will cause the transaction to write a record on commit signifying that the transaction has completed.
The call to txn.is_committed() will return True only if the persisted record exists.
Handling race conditions
Persisting transaction records works well to ensure sequential executions are idempotent, but what about when about when multiple transactions with the same key run at the same time? By default, transactions have an isolation level ofREAD_COMMITED which means that they can see any previously committed records, but they are not prevented from overwriting
a record that was created by another transaction between the time they started and the time they committed.
To see this behavior in action in the following script:
key argument between runs.
To prevent race conditions, you can set the isolation_level of a transaction to SERIALIZABLE. This will cause each transaction to take a lock on the
provided key. This will prevent other transactions from starting until the first transaction has completed.
Here’s an updated example that uses SERIALIZABLE isolation:
SERIALIZABLE isolation level, you must also provide a lock_manager to the transaction context manager. The
lock manager is responsible for acquiring and releasing locks on the transaction key. In the example above, we use a FileSystemLockManager which
will manage locks as files on the current instance’s filesystem.
Prefect offers several lock managers for different concurrency use cases:
Access data within transactions
Key-value pairs can be set within a transaction and accessed elsewhere within the transaction, including within theon_rollback hook.
The code below shows how to set a key-value pair within a transaction and access it within the on_rollback hook:
contents is accessible within the on_rollback hook.
Use get_transaction() to access the transaction object and txn.get("key") to access the value of the key.