Rebus.Object behaviour (rebus v0.4.0)
View SourceA behaviour for exported objects, run as a GenServer.
Rebus.Service hands each call on an exported object to a handler process
as a message, and leaves the process to answer it. use Rebus.Object
packages that for the common case: start_link/3 starts a GenServer that
exports the object and turns each call into a callback, answering it from
the callback's return value.
Example
defmodule Calculator do
use Rebus.Object
@calculator Rebus.Interface.new!("org.example.Calculator",
methods: [{"Add", in: [a: "i", b: "i"], out: [sum: "i"]}],
properties: [{"Precision", "u", :readwrite}]
)
@impl true
def interfaces, do: [@calculator]
@impl true
def init(precision), do: {:ok, %{precision: precision}}
@impl true
def handle_method(%{member: "Add"}, [a, b], state), do: {:reply, [a + b], state}
@impl true
def handle_get_property(_interface, "Precision", state),
do: {:ok, state.precision, state}
@impl true
def handle_set_property(_interface, "Precision", precision, state) when precision <= 16,
do: {:ok, %{state | precision: precision}}
def handle_set_property(_interface, "Precision", _precision, state),
do: {:error, "org.freedesktop.DBus.Error.InvalidArgs", "Precision above 16", state}
end
{:ok, conn} = Rebus.connect(:session)
{:ok, _pid} = Rebus.Object.start_link(Calculator, 2, conn: conn, path: "/org/example/Calculator")use Rebus.Object also defines child_spec/1, which takes {arg, opts}
as start_link/3 takes them, so the object can be supervised:
children = [{Calculator, {2, conn: conn, path: "/org/example/Calculator"}}]The child is :transient. An object that crashes is restarted and exports
itself again. An object whose connection stops exits with
{:shutdown, :disconnected}, which a supervisor treats as a normal exit,
so it is not restarted: a restart would try to export on the same stopped
connection and fail.
:temporary is the only other sensible value, for an object that should
never be restarted, such as one started on demand under a
DynamicSupervisor. Avoid :permanent. It restarts an object after its
connection stops, every restart fails, and the supervisor gives up after
its restart limit and exits, taking its other children with it.
use Rebus.Object takes the keys of Supervisor.child_spec/0, such as
:restart or :id, as use GenServer does.
Lifetime
The object exports itself before its init/1 runs, and its export lives
as long as the process. When the process exits, the connection fails every
call still waiting for its answer with org.freedesktop.DBus.Error.Failed,
and when a supervisor restarts it, the new process exports the object
again.
Exceptions in callbacks are not rescued. The process crashes, the caller
receives Failed, and the exception's detail stays off the wire.
The object stops with {:shutdown, :disconnected} when its connection
stops, since its export is gone with the connection.
Methods
Each call to a declared method reaches handle_method/3 with its
arguments, which have the declared types. The callback answers it by
returning {:reply, body, state} or {:error, name, message, state}.
Returning {:noreply, state} leaves the call open, to be answered later by
any process holding the Rebus.Service.Call:
@impl true
def handle_method(%{member: "SlowAdd"} = call, [a, b], state) do
Task.start(fn -> Rebus.Service.reply(call, [a + b]) end)
{:noreply, state}
endA reply that cannot be sent while the call is still open, such as a body
that does not fit the method's out-signature, is a bug in the object, which
stops with {:invalid_reply, reason}. Replying to a call that was already
answered or has expired, or a reply whose delivery timed out, is not a bug,
and the object carries on.
Properties
Get reaches handle_get_property/3, whose value is typed by the
property's declaration. GetAll calls it for every readable property of
the interface, in name order, and answers with the values together, or with
the first error returned. Set reaches handle_set_property/4 with the
new value, already checked against the declared type.
After a successful Set, the object emits
org.freedesktop.DBus.Properties.PropertiesChanged as the property's
emits_changed asks: with the new value when it is true, with the
property's name only when it is :invalidates, and not at all when it is
false or :const. A write-only property is never announced, since that
would broadcast a value no one may read. A callback that returns
{:ok, state, emit: false} suppresses the signal, for example when it
stores a value other than the one it was given, and can emit its own with
Rebus.Service.properties_changed/6.
The callbacks cannot carry Unix file descriptors, so an object whose
properties have a type containing h serves them through
Rebus.Service instead.
An object that declares readable properties without defining
handle_get_property/3, or writable ones without
handle_set_property/4, answers those calls with
org.freedesktop.DBus.Error.NotSupported.
Keeping your own loop
A GenServer that exports itself with Rebus.Service.export/4 can still map
its calls to these callbacks by passing them to dispatch/4 from its own
handle_info/2:
@impl GenServer
def handle_info({_ref, %Rebus.Service.Call{}} = call, state),
do: Rebus.Object.dispatch(call, __MODULE__, state)Such a module defines handle_method/3 and the property callbacks it
needs as plain functions. It cannot declare @behaviour Rebus.Object
beside GenServer, since both behaviours have init/1, handle_info/2
and terminate/2 callbacks.
Summary
Callbacks
Reads the property property of interface, for Get and GetAll.
Handles any other message, as GenServer.handle_info/2 does.
Handles a call to a declared method, whose arguments are args.
Writes value to the property property of interface, for Set.
Initialises the object's state from the arg given to start_link/3.
The interfaces the object exports, unless start_link/3 is given :interfaces.
Called when the object stops, as GenServer.terminate/2 is.
Functions
Answers a call by passing it to module's callbacks with state.
Starts an object of module, exports it, and runs module.init(arg).
Types
@type method_result() :: {:reply, body :: [term()], state()} | {:reply, body :: [term()], state(), [{:fds, [Rebus.Message.unix_fd()]}]} | {:error, name :: binary(), message :: binary(), state()} | {:noreply, state()} | {:stop, reason :: term(), state()}
What handle_method/3 returns.
@type state() :: term()
The object's own state, as its callbacks return it.
Callbacks
@callback handle_get_property(interface :: binary(), property :: binary(), state()) :: {:ok, value :: term(), state()} | {:error, name :: binary(), message :: binary(), state()}
Reads the property property of interface, for Get and GetAll.
Only readable properties of exported interfaces reach this callback.
@callback handle_info(msg :: term(), state()) :: {:noreply, state()} | {:stop, reason :: term(), state()}
Handles any other message, as GenServer.handle_info/2 does.
@callback handle_method(call :: Rebus.Service.Call.t(), args :: [term()], state()) :: method_result()
Handles a call to a declared method, whose arguments are args.
{:reply, body, state}answers withbody, encoded with the method's out-signature. A fourth element,fds: fds, sends descriptors with the reply, asRebus.Service.reply/3does.{:error, name, message, state}answers with the D-Bus errorname.{:noreply, state}leaves the call open, forRebus.Service.reply/3orRebus.Service.reply_error/4to answer later, from any process.{:stop, reason, state}stops the object, which fails the call.
@callback handle_set_property( interface :: binary(), property :: binary(), value :: term(), state() ) :: {:ok, state()} | {:ok, state(), [{:emit, boolean()}]} | {:error, name :: binary(), message :: binary(), state()}
Writes value to the property property of interface, for Set.
Only writable properties of exported interfaces reach this callback, and
value has the declared type. {:ok, state} answers the call and emits
PropertiesChanged as the property declares; {:ok, state, emit: false}
answers without emitting it.
Initialises the object's state from the arg given to start_link/3.
The object is already exported when this runs, so calls that arrive in
the meantime wait in the mailbox. Returning {:stop, reason} or :ignore
withdraws the export.
@callback interfaces() :: [Rebus.Interface.t()]
The interfaces the object exports, unless start_link/3 is given :interfaces.
Called when the object stops, as GenServer.terminate/2 is.
Functions
@spec dispatch({reference(), Rebus.Service.Call.t()}, module(), state(), keyword()) :: {:noreply, state()} | {:stop, reason :: term(), state()}
Answers a call by passing it to module's callbacks with state.
This is what the process start_link/3 starts does with each call, for a
GenServer that keeps its own loop. Give it the {ref, call} message the
connection sent, and return its result from handle_info/2: either
{:noreply, state} or {:stop, reason, state}. module defines
handle_method/3, and handle_get_property/3 and
handle_set_property/4 as its properties need.
Options
:interfaces- the exported interfaces, which property calls are typed by. Defaults tomodule.interfaces(), which is only called for a property call.
@spec start_link(module(), term(), keyword()) :: GenServer.on_start()
Starts an object of module, exports it, and runs module.init(arg).
Options
:conn- the connection to export on. Required.:path- the object path to export at. Required.:interfaces- the interfaces to export. Defaults tomodule.interfaces().:subtree,:call_timeoutand:timeout- as forRebus.Service.export/4.:timeoutbounds the export, not the GenServer'sinit/1.:name- a name for the process in the VM, as forGenServer.start_link/3: an atom,{:global, term}or{:via, module, term}. It is not a bus name; clients find the object by its:path, under a well-known name the connection requests withRebus.Bus.request_name/4.:debug,:spawn_optand:hibernate_after- passed toGenServer.start_link/3.
Any other option raises ArgumentError.
Return values
As for GenServer.start_link/3, with {:error, reason} for any
Rebus.Service.export/4 error, such as
{:error, {:already_exported, path, interface}}.