Durable Objects for Elixir.
A library that provides persistent, single-instance objects that are accessed by ID. Each object is backed by a GenServer that:
- Has global uniqueness per (module, object_id) pair
- Automatically hibernates after inactivity
- Optionally shuts down after extended inactivity
- Dispatches calls to
handle_<name>/Nfunctions on the module
Using the DSL
The recommended way to define Durable Objects is with the Spark DSL:
defmodule MyApp.Counter do
use DurableObject
state do
field :count, :integer, default: 0
end
handlers do
handler :increment, args: [:amount]
handler :get
end
options do
hibernate_after 300_000
end
def handle_increment(amount, state) do
new_count = state.count + amount
{:reply, new_count, %{state | count: new_count}}
end
def handle_get(state) do
{:reply, state.count, state}
end
endThe DSL generates client API functions automatically:
{:ok, count} = MyApp.Counter.increment("user-123", 5)
{:ok, count} = MyApp.Counter.get("user-123")Manual Usage (without DSL)
You can also call Durable Objects directly without the DSL:
defmodule Counter do
def handle_increment(n \\ 1, state) do
new_count = Map.get(state, :count, 0) + n
{:reply, new_count, Map.put(state, :count, new_count)}
end
def handle_get(state) do
{:reply, Map.get(state, :count, 0), state}
end
end
{:ok, 1} = DurableObject.call(Counter, "test", :increment)
{:ok, 2} = DurableObject.call(Counter, "test", :increment)
{:ok, 2} = DurableObject.call(Counter, "test", :get)
Summary
Functions
Use DurableObject to define a Durable Object with the Spark DSL.
Calls a handler on a Durable Object, starting it if necessary.
Cancels a pending alarm.
Cancels all pending alarms for an object.
Returns the configured default repo, or nil if not configured.
Ensures a Durable Object is started, starting it if necessary.
Gets the current state of a Durable Object.
Lists all pending alarms for an object.
Schedules an alarm to fire after delay_ms milliseconds.
Stops a running Durable Object.
Returns the pid of a running Durable Object, or nil if not running.
Functions
Use DurableObject to define a Durable Object with the Spark DSL.
This enables the declarative DSL for defining state fields, handlers, and lifecycle options.
Example
defmodule MyApp.Counter do
use DurableObject
state do
field :count, :integer, default: 0
end
handlers do
handler :increment, args: [:amount]
handler :get
end
def handle_increment(amount, state) do
new_count = state.count + amount
{:reply, new_count, %{state | count: new_count}}
end
def handle_get(state) do
{:reply, state.count, state}
end
end
Calls a handler on a Durable Object, starting it if necessary.
Dispatches to handle_<name>/N function on the module, where N is
the number of args plus one (for state).
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil):hibernate_after- Hibernate after this many ms of inactivity (default: 5 minutes):shutdown_after- Stop process after this many ms of inactivity (default: nil):timeout- Call timeout in ms (default: 5000)
Returns
{:ok, result}- Handler returned{:reply, result, new_state}{:ok, :noreply}- Handler returned{:noreply, new_state}{:error, reason}- Handler returned{:error, reason}or error occurred
Examples
{:ok, 1} = DurableObject.call(Counter, "test", :increment)
{:ok, 5} = DurableObject.call(Counter, "test", :increment, [5])
# With persistence
{:ok, 1} = DurableObject.call(Counter, "test", :increment, [], repo: MyApp.Repo)
Cancels a pending alarm.
Returns :ok even if the alarm doesn't exist.
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil)
Examples
:ok = DurableObject.cancel_alarm(Counter, "user-123", :cleanup)
Cancels all pending alarms for an object.
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil)
Examples
:ok = DurableObject.cancel_all_alarms(Counter, "user-123")
Returns the configured default repo, or nil if not configured.
Configure in your application config:
config :durable_object, repo: MyApp.Repo
Ensures a Durable Object is started, starting it if necessary.
Returns {:ok, pid} if the object is running or was started successfully.
Returns {:error, reason} if the object could not be started.
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil):hibernate_after- Hibernate after this many ms of inactivity (default: 5 minutes):shutdown_after- Stop process after this many ms of inactivity (default: nil)
Examples
{:ok, pid} = DurableObject.ensure_started(Counter, "test")
{:ok, ^pid} = DurableObject.ensure_started(Counter, "test")
Gets the current state of a Durable Object.
Returns the state if the object is running, or raises if not.
To check if an object is running, use whereis/2.
Examples
state = DurableObject.get_state(Counter, "test")
Lists all pending alarms for an object.
Returns a list of {alarm_name, scheduled_at} tuples, ordered by scheduled time.
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil)
Examples
{:ok, alarms} = DurableObject.list_alarms(Counter, "user-123")
# => [{:cleanup, ~U[2024-01-15 10:30:00Z]}, {:daily_reset, ~U[2024-01-16 00:00:00Z]}]
Schedules an alarm to fire after delay_ms milliseconds.
When the alarm fires, handle_alarm(alarm_name, state) will be called on
the object's module. If no handle_alarm/2 is defined, the alarm is
silently acknowledged.
Options
:repo- Ecto repo for persistence (default: configured or nil):prefix- Table prefix for multi-tenancy (default: nil)
Examples
# Schedule an alarm to fire in 1 hour
:ok = DurableObject.schedule_alarm(Counter, "user-123", :cleanup, :timer.hours(1))Handler
Define handle_alarm/2 in your module:
def handle_alarm(:cleanup, state) do
# Do cleanup
{:noreply, state}
end
def handle_alarm(:daily_reset, state) do
# Reset and reschedule
{:noreply, %{state | count: 0}, {:schedule_alarm, :daily_reset, :timer.hours(24)}}
end
Stops a running Durable Object.
Examples
:ok = DurableObject.stop(Counter, "test")
Returns the pid of a running Durable Object, or nil if not running.
Examples
nil = DurableObject.whereis(Counter, "not-started")
{:ok, _} = DurableObject.ensure_started(Counter, "test")
pid = DurableObject.whereis(Counter, "test")