sinal/forwarder

A bounded forwarder: a process that runs the handlers of the events handed to it, so a slow or blocked handler stalls the forwarder instead of the emitter.

Isolating a library’s events

A library emits with sinal.emit. The application decides, per event-name prefix, whether those events run synchronously (the default) or in a forwarder. It supervises one forwarder and routes the library’s prefix to it, once, at start:

import gleam/erlang/process
import gleam/otp/static_supervisor as supervisor
import sinal/forwarder

pub fn start(name: process.Name(forwarder.Message)) {
  let observations = forwarder.new(name)
  let assert Ok(_) =
    supervisor.new(supervisor.OneForOne)
    |> supervisor.add(forwarder.supervised(observations))
    |> supervisor.start
  forwarder.route(["http_gun"], observations)
}

Every sinal.emit of an event whose name starts with http_gun then returns as soon as the event is handed off. A package that owns its forwarder (one per database, say) calls forwarder.emit instead, which returns the Refusal when the event is dropped.

Operational limits

When drops are reported

The three counts survive every incarnation. Only a running incarnation reports them, as dropped_event from its own process, and each report resets exactly the counts it carries:

Types

Measurements of dropped_event, one best-effort diagnostic report. Read it by label; sinal may add fields.

  • rejected: sends refused with CapacityExceeded.
  • lost: admitted events still in flight when the previous incarnation stopped (zero outside the report an incarnation makes when it starts).
  • unavailable: sends refused with ForwarderUnavailable.
pub type Dropped {
  Dropped(rejected: Int, lost: Int, unavailable: Int)
}

Constructors

  • Dropped(rejected: Int, lost: Int, unavailable: Int)

Metadata of dropped_event: the forwarder’s process name.

pub type DroppedMetadata {
  DroppedMetadata(forwarder: String)
}

Constructors

  • DroppedMetadata(forwarder: String)

A forwarder’s process name, capacity and shared drop counters. Build it with new, run it with supervised.

pub opaque type Forwarder

The forwarder process’s message type, needed only to create its process.Name.

pub opaque type Message

Why emit dropped an event. The union is closed.

pub type Refusal {
  CapacityExceeded
  ForwarderUnavailable
}

Constructors

  • CapacityExceeded

    The forwarder already holds its capacity of events.

  • ForwarderUnavailable

    No incarnation of the forwarder is running.

Values

pub const default_capacity: Int

The capacity of a forwarder built with new: queued plus executing events.

pub fn describe_refusal(refusal: Refusal) -> String

Describes a refusal for logs.

pub fn dropped_event() -> sinal.Event(Dropped, DroppedMetadata)

The [sinal, forwarder, dropped] event a forwarder emits from its own process when a drain finds a nonzero count. Its measurements are the integer keys rejected, lost and unavailable; its metadata is the forwarder’s name under forwarder. It is synchronous and never routed.

pub fn emit(
  forwarder: Forwarder,
  event: sinal.Event(m, d),
  measurements: m,
  metadata: d,
) -> Result(Nil, Refusal)

Hands an event to this forwarder, ignoring routes, and returns as soon as it is handed off or refused; it never waits for a handler. A refused event is dropped and counted in the forwarder’s next dropped_event. Use it in a package that owns its forwarder; a library that does not calls sinal.emit and lets the application route it.

pub fn new(name: process.Name(Message)) -> Forwarder

Describes a forwarder with the default capacity of 1,024 events. Starts no process; pair it with supervised. Every Forwarder built from one name shares its drop counters.

pub fn route(prefix: List(String), forwarder: Forwarder) -> Nil

Sends every sinal.emit event whose name starts with prefix through forwarder, for the whole node. The empty prefix matches every event; when several prefixes match, the longest wins. Routing a prefix again replaces its forwarder.

This is application setup, like attaching a handler: call it once the forwarder is supervised. Changing a route is expensive (routes live in persistent_term), so do not route per request.

Panics when a segment of prefix breaks the name grammar.

pub fn supervised(
  forwarder: Forwarder,
) -> supervision.ChildSpecification(Forwarder)

Describes the forwarder’s process for an OTP supervisor. Drop counts survive a restart; admission counters and the event subject do not.

pub fn unroute(prefix: List(String)) -> Nil

Removes the route of exactly prefix, if there is one. Events it matched fall back to a shorter routed prefix, or to synchronous delivery. Events already handed to its forwarder are still delivered there, so they can run after later, now synchronous, events.

pub fn with_capacity(
  forwarder: Forwarder,
  capacity: Int,
) -> Forwarder

Sets how many events the forwarder holds, queued plus executing. A value below 1 makes the start fail with InitFailed.

✨ Search Document