Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver guides transactions.md
Raw

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 struct — no I/O, no server contact yet:
```elixir
alias Aerospike.Txn
txn = Txn.new()
# %Aerospike.Txn{id: -3489123456789012345, timeout: 0}
```
`Txn.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` — the Recommended Approach
`Aerospike.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` raised inside the callback, the transaction aborts and
`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 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` and `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` 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` Handles This For You {: .tip}
>
> `Aerospike.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` in a transaction uses durable delete semantics
automatically.
- **Concurrent use is undefined**: passing the same `%Txn{}` 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` calls with the same
`%Txn{}` struct.
## Checking Transaction State
Inside a `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` or `abort/2` returns, the ETS tracking entry is deleted, so
`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` — transaction handle struct reference
- `Aerospike` — facade functions: `commit/2`, `abort/2`, `transaction/2`, `transaction/3`
- [Batch Operations](batch-operations.md) — combining batch with transactions