Current section
Files
Jump to
Current section
Files
guides/transactions.md
# 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