# Transactions (MRT)

Multi-Record Transactions (MRT) let you group several reads and writes across multiple keys into a single atomic unit. Either all writes are committed or none are — other clients never see a partially applied transaction.

> #### Enterprise Edition Required {: .warning}
>
> MRT requires **Aerospike Enterprise Edition** with server version 8.0 or later. Connecting to
> Community Edition and calling transaction APIs will return an error.

## Creating a Transaction

A transaction starts as a plain [`Aerospike.Txn`](Aerospike.Txn.html) struct — no I/O, no server contact yet:

```elixir
alias Aerospike.Txn

txn = Txn.new()
# %Aerospike.Txn{id: -3489123456789012345, timeout: 0}
```

[`Txn.new/0`](Aerospike.Txn.html#new/0) generates a random signed 64-bit ID. Pass `timeout:` (in milliseconds) to set a
server-side MRT record expiry. If the client crashes before committing, the server releases
write locks after this window:

```elixir
txn = Txn.new(timeout: 30_000)   # 30-second server-side timeout
```

## Using [`transaction/2`](Aerospike.html#transaction/2) — the Recommended Approach

[`Aerospike.transaction/2`](Aerospike.html#transaction/2) is the highest-level API. It creates a transaction, runs your
callback, and commits or aborts automatically:

```elixir
{:ok, _} =
  Aerospike.transaction(:aero, fn txn ->
    key1 = Aerospike.key("inventory", "items", "widget-A")
    key2 = Aerospike.key("accounts", "users", "user-42")

    {:ok, item} = Aerospike.get(:aero, key1, txn: txn)

    if item.bins["stock"] < 1 do
      raise Aerospike.Error.from_result_code(:parameter_error, message: "out of stock")
    end

    :ok = Aerospike.put(:aero, key1, %{"stock" => item.bins["stock"] - 1}, txn: txn)
    :ok = Aerospike.put(:aero, key2, %{"last_order" => "widget-A"}, txn: txn)
  end)
```

On any [`Aerospike.Error`](Aerospike.Error.html) raised inside the callback (for example via
[`Aerospike.Error.from_result_code/2`](Aerospike.Error.html#from_result_code/2)), the transaction aborts and
[`transaction/2`](Aerospike.html#transaction/2) returns `{:error, e}`. On any other exception, the transaction aborts and
the exception is re-raised — server-side write locks are always released immediately.

### Setting a Timeout

```elixir
{:ok, _} =
  Aerospike.transaction(:aero, [timeout: 10_000], fn txn ->
    Aerospike.put(:aero, key, %{"x" => 1}, txn: txn)
  end)
```

### Returning a Value

The return value of the callback is wrapped in `{:ok, value}`:

```elixir
{:ok, balance} =
  Aerospike.transaction(:aero, fn txn ->
    {:ok, rec} = Aerospike.get(:aero, account_key, txn: txn)
    rec.bins["balance"]
  end)
```

## Using `txn:` on CRUD Operations

Pass `txn: txn` to any CRUD operation on [`Aerospike`](Aerospike.html) to enlist it in the transaction. All operations in one
transaction must touch the **same namespace**:

```elixir
txn = Txn.new()

# Reads — tracked for version verification at commit time
{:ok, rec} = Aerospike.get(:aero, key1, txn: txn)

# Writes — registered with the server-side monitor record
:ok = Aerospike.put(:aero, key2, %{"score" => 99}, txn: txn)
:ok = Aerospike.delete(:aero, key3, txn: txn)
```

Supported operations: `get`, `exists`, `put`, `delete`, `touch`, `append`, `prepend`, `add`,
`operate`, `batch_get`, `batch_exists`, `batch_operate`.

## Manual Commit and Abort

When you need full control over the transaction lifecycle, call [`commit/2`](Aerospike.html#commit/2) and [`abort/2`](Aerospike.html#abort/2)
directly. You must also initialize ETS tracking explicitly via `TxnOps.init_tracking/2`:

```elixir
alias Aerospike.{Txn, TxnOps}

txn = Txn.new()
TxnOps.init_tracking(:aero, txn)

:ok = Aerospike.put(:aero, key1, %{"a" => 1}, txn: txn)
:ok = Aerospike.put(:aero, key2, %{"b" => 2}, txn: txn)

case Aerospike.commit(:aero, txn) do
  {:ok, :committed} ->
    IO.puts("committed")

  {:error, %Aerospike.Error{in_doubt: true} = err} ->
    # Mark-roll-forward timed out — may or may not have committed.
    # Safe to retry commit/2; the server returns :mrt_committed if already done.
    IO.puts("in-doubt, retrying: #{err.message}")
    Aerospike.commit(:aero, txn)

  {:error, err} ->
    Aerospike.abort(:aero, txn)
    IO.puts("commit failed, rolled back: #{err.message}")
end
```

### Aborting

```elixir
{:ok, :aborted} = Aerospike.abort(:aero, txn)
```

[`abort/2`](Aerospike.html#abort/2) rolls back all tracked writes and deletes the server-side monitor record.
Roll-back is best-effort: if some writes fail (e.g., a network partition), the server
releases locks automatically when the MRT timeout expires.

## Commit Protocol

Understanding the commit phases helps diagnose errors:

| Phase | What happens |
|-------|-------------|
| **Verify** | For each read tracked in the transaction, the server checks that the record version has not changed since the read. If any version mismatches, commit fails. |
| **Mark roll-forward** | The server-side monitor record is flagged for roll-forward. After this point, the transaction is durably committed from the server's perspective. |
| **Roll forward** | Each write is finalized (locks released, record versions updated). |
| **Close** | The monitor record is deleted. |

## Error Handling

### Verify Failure

A read was invalidated by a concurrent external write between your read and commit:

```elixir
case Aerospike.commit(:aero, txn) do
  {:ok, :committed} ->
    :ok

  {:error, %Aerospike.Error{code: :txn_verify_failed}} ->
    # Transaction was auto-rolled-back.
    # Retry the whole transaction from scratch.
    :retry
end
```

### In-Doubt Commit

The mark-roll-forward message timed out before the server responded. The transaction
**may or may not** have committed server-side:

```elixir
case Aerospike.commit(:aero, txn) do
  {:ok, :committed} ->
    :ok

  {:error, %Aerospike.Error{in_doubt: true}} ->
    # Retry commit — the server returns :mrt_committed if it already committed,
    # which is accepted as success. Do NOT call abort here.
    Aerospike.commit(:aero, txn)

  {:error, err} ->
    # Definitive failure — safe to abort and retry from scratch.
    Aerospike.abort(:aero, txn)
    {:error, err}
end
```

### Server Aborted

The server aborted the transaction (MRT timeout expired while it was open):

```elixir
case Aerospike.commit(:aero, txn) do
  {:error, %Aerospike.Error{code: :mrt_aborted}} ->
    # The server timed out the transaction. Retry from scratch.
    :retry
end
```

> #### [`transaction/2`](Aerospike.html#transaction/2) Handles This For You {: .tip}
>
> [`Aerospike.transaction/2`](Aerospike.html#transaction/2) calls `best_effort_abort` on any commit failure, cleans up
> server-side state, and returns `{:error, e}`. For most use cases you do not need to
> handle these error cases manually.

## Constraints

- **Same namespace**: all keys in one transaction must belong to the same namespace.
- **No scans or queries**: `scan/3` and `query/3` do not accept a `txn:` option.
- **Writes are durable deletes**: [`delete/3`](Aerospike.html#delete/3) in a transaction uses durable delete semantics
  automatically.
- **Concurrent use is undefined**: passing the same [`%Txn{}`](Aerospike.Txn.html) to operations from multiple
  processes concurrently is not supported. The ETS tracking state is not protected by a lock.
- **One transaction at a time per handle**: do not nest [`transaction/2`](Aerospike.html#transaction/2) calls with the same
  `%Txn{}` struct.

## Checking Transaction State

Inside a [`transaction/2`](Aerospike.html#transaction/2) callback you can inspect the current state:

```elixir
Aerospike.transaction(:aero, fn txn ->
  {:ok, :open} = Aerospike.txn_status(:aero, txn)
  Aerospike.put(:aero, key, %{"x" => 1}, txn: txn)
end)
```

After [`commit/2`](Aerospike.html#commit/2) or [`abort/2`](Aerospike.html#abort/2) returns, the ETS tracking entry is deleted, so
[`txn_status/2`](Aerospike.html#txn_status/2) returns `{:error, %Aerospike.Error{}}` rather than `{:ok, :committed}`.
The state is only observable during the commit/abort execution window.

## Next Steps

- [`Aerospike.Txn`](Aerospike.Txn.html) — transaction handle struct reference
- [`Aerospike`](Aerospike.html) — facade functions: [`commit/2`](Aerospike.html#commit/2), [`abort/2`](Aerospike.html#abort/2), [`transaction/2`](Aerospike.html#transaction/2), [`transaction/3`](Aerospike.html#transaction/3)
- [Batch Operations](batch-operations.md) — combining batch with transactions
