Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver guides udfs.md
Raw

guides/udfs.md

# User Defined Functions (UDFs)
User Defined Functions are server-side scripts written in Lua. They execute inside the
Aerospike server process, co-located with the data, making them suitable for atomic
read-modify-write operations, data transformations, and computations that would otherwise
require multiple round-trips.
## Lua Basics
A UDF package is a `.lua` source file containing one or more named functions. Each function
receives a record as its first argument and may accept additional user-supplied arguments:
```lua
-- multiply.lua
-- Multiplies the value in bin "n" by a factor and returns the result.
function multiply(rec, factor)
local val = rec["n"]
if val == nil then return nil end
rec["n"] = val * factor
aerospike:update(rec)
return rec["n"]
end
-- Returns the bin value unchanged (useful for testing registration).
function echo(rec, arg)
return arg
end
```
## Registering a UDF
[`Aerospike.register_udf/3`](Aerospike.html#register_udf/3) uploads the package to the cluster. Pass either a filesystem
path ending in `.lua` or the raw Lua source as a string:
```elixir
# From a file path
{:ok, task} = Aerospike.register_udf(:aero, "/path/to/multiply.lua", "multiply.lua")
# From a string
lua = ~S"""
function echo(rec, arg)
return arg
end
"""
{:ok, task} = Aerospike.register_udf(:aero, lua, "echo.lua")
```
`server_name` (the third argument) is the package name as stored on the server. Use the
filename convention (`"module.lua"`) for clarity — this is the value you pass to
[`apply_udf/5`](Aerospike.html#apply_udf/5) as the `package` argument (without the `.lua` extension).
Registration is asynchronous. The server propagates the package across cluster nodes in the
background.
## Waiting for Registration to Complete
[`register_udf/3`](Aerospike.html#register_udf/3) returns an [`Aerospike.RegisterTask`](Aerospike.RegisterTask.html). Block until registration is
propagated to all nodes:
```elixir
:ok = Aerospike.RegisterTask.wait(task, timeout: 15_000)
```
Or poll without blocking:
```elixir
case Aerospike.RegisterTask.status(task) do
{:ok, :complete} -> IO.puts("registered on all nodes")
{:ok, :in_progress} -> IO.puts("still propagating...")
{:error, err} -> IO.puts("status check failed: #{err.message}")
end
```
[`wait/2`](Aerospike.AsyncTask.html#c:wait/2) options (see [`Aerospike.AsyncTask`](Aerospike.AsyncTask.html)):
| Option | Default | Description |
|--------|---------|-------------|
| `:timeout` | no limit | Maximum time to wait in milliseconds |
| `:poll_interval` | 1000 | Milliseconds between status checks |
## Applying a UDF to a Record
[`Aerospike.apply_udf/5`](Aerospike.html#apply_udf/5) executes a Lua function on a single record:
```elixir
key = Aerospike.key("test", "items", "item:1")
:ok = Aerospike.put(:aero, key, %{"n" => 10})
{:ok, result} = Aerospike.apply_udf(:aero, key, "multiply", "multiply", [3])
# result => 30
```
Arguments:
| Argument | Description |
|----------|-------------|
| `conn` | Connection name atom |
| `key` | [`Aerospike.Key`](Aerospike.Key.html) for the record to operate on |
| `package` | Lua module name **without** the `.lua` extension |
| `function` | Lua function name |
| `args` | List of arguments passed to the function (after the record) |
The Lua function's return value becomes the Elixir return value. Aerospike's MessagePack
encoding maps Lua types to Elixir terms:
| Lua type | Elixir type |
|----------|-------------|
| `nil` | `nil` |
| `number` (integer) | `integer()` |
| `number` (float) | `float()` |
| `string` | `binary()` |
| `boolean` | `boolean()` |
| `table` (array) | `list()` |
| `table` (map) | `map()` |
### Options
```elixir
{:ok, result} =
Aerospike.apply_udf(:aero, key, "multiply", "multiply", [3],
timeout: 500,
replica: :master
)
```
The above call uses [`apply_udf/6`](Aerospike.html#apply_udf/6) (extra keyword options). Available options: `:timeout`, `:pool_checkout_timeout`, `:filter`, `:replica`.
## Full Lifecycle Example
```elixir
alias Aerospike.RegisterTask
# 1. Write the Lua source
lua = ~S"""
function transform(rec, factor)
local val = rec["value"]
if val == nil then return nil end
rec["value"] = val * factor
aerospike:update(rec)
return rec["value"]
end
"""
# 2. Register and wait
{:ok, task} = Aerospike.register_udf(:aero, lua, "transform.lua")
:ok = RegisterTask.wait(task, timeout: 15_000)
# 3. Prepare test data
key = Aerospike.key("test", "demo", "k1")
:ok = Aerospike.put(:aero, key, %{"value" => 5})
# 4. Apply the UDF
{:ok, result} = Aerospike.apply_udf(:aero, key, "transform", "transform", [6])
IO.puts("result: #{result}") # => "result: 30"
# 5. Remove the package when done
:ok = Aerospike.remove_udf(:aero, "transform.lua")
```
## Removing a UDF
[`Aerospike.remove_udf/2`](Aerospike.html#remove_udf/2) deletes the package from all cluster nodes. Returns `:ok` whether or not
the package was registered:
```elixir
:ok = Aerospike.remove_udf(:aero, "multiply.lua")
```
## Error Handling
### UDF Runtime Errors
When the Lua function raises an error, [`apply_udf/5`](Aerospike.html#apply_udf/5) returns `{:error, %Aerospike.Error{code: :udf_bad_response}}`. The `message` field contains the Lua error string:
```elixir
case Aerospike.apply_udf(:aero, key, "mymodule", "risky_fn", [arg]) do
{:ok, result} ->
process(result)
{:error, %Aerospike.Error{code: :udf_bad_response, message: msg}} ->
Logger.error("UDF runtime error: #{msg}")
{:error, %Aerospike.Error{} = err} ->
Logger.error("request failed: #{err.message}")
end
```
### Record Not Found
If the key does not exist, the Lua function still runs — `rec` is a new, empty record. You
can distinguish this case inside Lua:
```lua
function safe_read(rec, bin_name)
if not aerospike:exists(rec) then
return nil
end
return rec[bin_name]
end
```
Or guard in Elixir:
```elixir
case Aerospike.exists(:aero, key) do
{:ok, true} ->
Aerospike.apply_udf(:aero, key, "module", "fn", [])
{:ok, false} ->
{:error, :not_found}
end
```
## Notes
- **Packages are cluster-wide**: a registered package is available on all nodes and
persists across server restarts.
- **Package name**: the `server_name` in [`register_udf/3`](Aerospike.html#register_udf/3) and the `package` argument to
[`apply_udf/5`](Aerospike.html#apply_udf/5) are both the filename **without** the `.lua` extension. That is, if you
register with `server_name: "my_module.lua"`, call [`apply_udf/5`](Aerospike.html#apply_udf/5) with `package: "my_module"`.
- **Lua version**: Aerospike uses Lua 5.1.
- **Thread safety**: the server runs UDFs in a single-threaded context per record —
concurrent `apply_udf` calls on different keys are safe.
- **No network calls inside UDFs**: Lua code cannot make external calls; UDFs are purely
in-process transformations.
## Next Steps
- [`Aerospike.RegisterTask`](Aerospike.RegisterTask.html) — polling task reference
- [`Aerospike`](Aerospike.html) — facade functions: [`register_udf/3`](Aerospike.html#register_udf/3), [`apply_udf/5`](Aerospike.html#apply_udf/5), [`apply_udf/6`](Aerospike.html#apply_udf/6), [`remove_udf/2`](Aerospike.html#remove_udf/2)
- [Batch Operations](batch-operations.md)[`Aerospike.Batch.udf/5`](Aerospike.Batch.html#udf/5) for multi-key UDF invocations