# Automatic Idempotency

Warning

Automatic idempotency is currently experimental and not recommended for use in production.

## Synopsis

Use the `automatic_idempotency` transaction option to prevent commits from failing with `commit_unknown_result` at a small performance cost. `transaction_timed_out` and `cluster_version_changed` still indicate an unknown commit status.

## Details

Transactions are generally run in retry loops that retry the error `commit_unknown_result`. When an attempt fails with `commit_unknown_result` it’s possible that the attempt succeeded, and the retry will perform the effect of the transaction twice! This behavior can be surprising at first, and difficult to reason about.

As an example, consider this simple transaction:

    @fdb.transactional
    def atomic_increment(tr, key):
        tr.add(key, struct.pack("<q", 1))

This transaction appears to be correct, and behaves as expected most of the time, but incorrectly adds to the key multiple times if `commit_unknown_result` is thrown.

To mitigate this, it’s common to write something unique to a transaction, so that retries can detect whether or not a commit succeeded by reading the database and looking for the unique change.

For example:

    def atomic_increment(db, key):
        tr_id = uuid.UUID(int=random.getrandbits(128)).bytes
        # tr_id can be used to detect whether or not this transaction has already committed
        @fdb.transactional
        def do_it(tr):
            if tr[b"ids/" + tr_id] == None:
                tr[b"ids/" + tr_id] = b""
                tr.add(key, struct.pack("<q", 1))
        do_it(db)
        # Clean up unique state
        del db[b"ids/" + tr_id]

This approach has several problems

1. We’re now doing about twice the work as before, and we need an extra transaction to clean up.

2. This will slowly leak space over time if clients fail before cleaning up their unique state.

Automatic Idempotency is an implementation of a conceptually similar pattern, but taking advantage of fdb implementation details to provide better performance and clean up after itself. Here’s our atomic increment with Automatic Idempotency:

    @fdb.transactional
    def atomic_increment(tr, key):
        tr.options.set_automatic_idempotency()
        tr.add(key, struct.pack("<q", 1))

This will correctly add exactly once, provided you allow it to retry until success.

## Caveats

Automatic Idempotency only prevents `commit_unknown_result`, and it does not prevent `transaction_timed_out` or `cluster_version_changed`. Each of these can indicate an unknown commit status. To mitigate this, the current recommendation is do not retry transactions that fail with `transaction_timed_out`. The default retry loop does not retry `transaction_timed_out`, so this is mostly relevant if you have a custom retry loop. This feature is not recommended for users of the Multi-version client at this time since it does not prevent `cluster_version_changed`. Support may be added in the future.