Signal subscriptions and match rules

View Source

A message bus forwards a broadcast signal only to connections that asked for it. Rebus gives you two ways to receive signals.

At a glance

  • Rebus.add_signal_handler/1 delivers every signal that arrives on the connection.
  • Rebus.add_match/3 asks the bus to route matching broadcast signals to you.
  • Both deliver {ref, %Rebus.Message{}} to the process that registered.
  • Build rules with Rebus.MatchRule.new!/1. Raw rule strings are not accepted.
  • Handlers are per connection, and never see signals from another connection.

Every signal on the connection

{:ok, ref} = Rebus.add_signal_handler(conn)
# {^ref, %Rebus.Message{type: :signal}} arrives here
:ok = Rebus.delete_signal_handler(conn, ref)

This asks the bus for nothing. It gives you the signals the connection already receives, such as signals directed at your unique name.

Routed subscriptions

Rebus.add_match/3 registers a rule with the bus and returns a reference:

rule =
  Rebus.MatchRule.new!(
    sender: "org.freedesktop.DBus",
    interface: "org.freedesktop.DBus",
    member: "NameOwnerChanged",
    args: %{0 => "org.example.Service"}
  )

{:ok, ref} = Rebus.add_match(conn, rule, 1_000)

receive do
  {^ref, %Rebus.Message{body: [name, old_owner, new_owner]}} ->
    handle(name, old_owner, new_owner)
end

:ok = Rebus.remove_match(conn, ref)

A connection opened with bus: false has no bus driver to ask, so add_match/3 returns {:error, :not_a_bus}.

Building a rule

Rebus.MatchRule.new!/1 accepts :sender, :interface, :member, :path, :path_namespace, :destination, :args, :arg_paths and :arg0namespace. :path and :path_namespace cannot be combined. Rules always match signals.

The encoded rule is limited to 1024 bytes, which is the D-Bus limit. eavesdrop is deliberately not accepted; use a dedicated monitoring connection for that.

Two equivalent rules share one registration with the bus. Each add_match/3 call still returns its own reference, and the bus rule is removed once the last reference is removed or its owning process exits.

Sender matching

Rebus checks a unique :sender name itself, on top of whatever the bus routed.

A well-known :sender is left to the bus for broadcast signals, because the bus forwards them under the sender's current unique name. A directed signal bypasses bus routing, so Rebus decides that one itself. It tracks who owns the name, with GetNameOwner and the bus driver's NameOwnerChanged signal, and delivers a directed signal whose sender is that owner, or the well-known name itself, which only the bus driver can send. A directed signal from anyone else is rejected. Both facts come from the bus driver, whose sender header a peer cannot forge.

Tracking is best effort. If it fails, Rebus logs a warning and retries with backoff. Directed delivery for the name stays off until ownership is known; broadcast delivery is unaffected. If the retry budget is exhausted, Rebus logs an error and keeps rejecting directed signals from that service for the life of the connection. A directed signal can also be missed briefly while a new subscription obtains its first ownership answer.

D-Bus does not say which rule admitted a signal. Rebus therefore rejects a rule that overlaps an existing one with a different sender, returning {:error, :sender_routing_ambiguous}, rather than delivering a signal to the wrong subscription.

When a subscription fails

{:error, :timeout} from add_match/3 is ambiguous. You get no reference, but the bus may already hold the rule. Rebus cleans up in the background while the connection lives. A later add_match/3 for the same rule waits behind that cleanup under its own timeout, and returns {:error, :match_rule_cleanup_pending} if the cleanup outlasts it.

A reply that exceeds Rebus's local decoding limit before its header fields can be trusted also appears as {:error, :timeout}: the connection cannot recover the reply serial needed to correlate it. This is the same conservative fallback described by the fuller Rebus.call/3 contract.

Rebus closes the connection when too many rules remain uncertain, when a rule's cleanup exhausts its retry budget, or when an operation returns {:error, :match_subscription_state_lost}. Closing stops the connection's handlers and makes the bus discard every rule it held.

Two application environment settings bound that cleanup. :match_recovery_max_rules (default 64) is how many rules may be uncertain at once before the connection is closed. :match_recovery_max_attempts (default 30) is how many cleanup attempts one rule receives before the same close happens. Recovery continues across transient local failures because those failures do not prove that the bus resolved the rule.

Errors

Rebus.add_match/3 and Rebus.remove_match/3 return these reasons.

ReasonMeaning
:not_a_busThe connection was opened with bus: false. Nothing was sent.
:sender_routing_ambiguousThe rule overlaps an existing one with a different sender.
:timeoutThe operation did not finish in time, or the reply header exceeded a local decoding limit. The bus may still hold the rule; see Rebus.call/3 for the header-limit fallback.
:match_rule_cleanup_pendingAn earlier ambiguous operation on the same rule is still being cleaned up.
:match_subscription_state_lostAn in-flight operation lost its state and cannot be resolved on this connection. Rebus ends it by closing a connection created by Rebus.connect/2; the state dies with the connection.
{:bus_error, error_name}The bus returned a D-Bus error reply.
:invalid_bus_replyThe bus reply did not have the expected shape.
:not_connectedConnection setup has not completed.
:disconnectedThe connection stopped.
:not_startedRebus could not start the local task for the operation. Nothing was sent to the bus and the rule and its references are unchanged, so retrying is safe.
:encode_failedThe request could not be encoded.
:serial_exhaustedEvery D-Bus serial is in use.
:fd_claim_expiredDescriptors arrived with the bus reply and were closed.
{:reply_dropped, outcome}The bus replied, but the reply exceeded a local decoding cap.
:remote_connection_unsupportedThe connection PID belongs to another node.