Process events in parallel

Two tools run each event in its own process: a worker pool for lightweight, best-effort work, and a consumer supervisor for work that needs restart policies.

Worker pools

pool.sink builds a sink that runs each event in its own worker process, with a cap on how many workers run at once. Demand follows the workers: the pool asks for one more event whenever a worker finishes. Subscribe it with the Manual demand mode; the pool does its own asking. A crashing worker doesn’t take the pool down:

import sluice/pool

let assert Ok(deliveries) =
  pool.sink(concurrency: 8, run: fn(order) { deliver(order) })
  |> sink.start()
let assert Ok(_) =
  sluice.subscription(to: orders.data)
  |> sluice.demand_mode(sluice.Manual)
  |> sluice.subscribe(consumer: deliveries.data)

Supervised event workers

consumer_supervisor starts one linked OTP child for each event. max_demand sets the number of slots for each subscription: every running child occupies one slot, and the slot frees up only when the child finally terminates. An abnormal exit followed by a transient restart keeps the same event and slot:

import gleam/erlang/process
import gleam/otp/actor
import sluice/consumer_supervisor

fn start_delivery(order: Order) -> actor.StartResult(Nil) {
  let pid = process.spawn(fn() { deliver(order) })
  Ok(actor.Started(pid:, data: Nil))
}

let assert Ok(deliveries) =
  consumer_supervisor.new(start_delivery)
  |> consumer_supervisor.restart(consumer_supervisor.Transient)
  |> consumer_supervisor.restart_tolerance(
    max_restarts: 3,
    within_seconds: 5,
  )
  |> consumer_supervisor.start()

let assert Ok(_) =
  sluice.subscription(to: orders.data)
  |> sluice.min_demand(4)
  |> sluice.max_demand(8)
  |> sluice.subscribe(consumer: consumer_supervisor.inlet(deliveries.data))

The child start function must return a process linked to its caller, as normal OTP child start functions do. Initialisation is synchronous and must finish quickly; the child does the long-running work after it starts. Children are Temporary by default. Transient children restart only after abnormal exits. Permanent children are not supported, because an event that completed successfully must not restart.

The consumer supervisor owns its demand loop, so it accepts the default Automatic mode and rejects Manual subscriptions. If a producer connection ends, the existing children keep running. The subscription’s cancel mode then decides whether the consumer supervisor stays alive; when it stops, it shuts down all remaining children and kills any that exceed shutdown_timeout.

pool.sink remains useful for lightweight, best-effort work: its workers are unlinked, failures are logged and dropped, and each completion immediately asks for a replacement. consumer_supervisor gives you OTP restart policy, restart tolerance, bounded shutdown, and child inspection instead.

Search Document