Chip as part of a Supervision tree

A supervision tree is a strategy used in the wider erlang ecosystem to monitor and provide a restart strategy in cases of failure, giving the system self-healing capabilities.

To make chip and other subjects part of a supervision tree we first need to define their respective child specifications, these specifications define their behaviour and state when starting or re-starting.

Lets assume we need to have multiple “sessions” and an indexed on our system, we can have a sessions.gleam module and define on our registry, we need to first define the child specification:

import chip
import gleam/erlang/process
import gleam/otp/supervisor

type Registry =
  chip.Registry(Message, Int)

// A context type will help carry round state at the supervisor.
type Context {
  Context(caller: process.Subject(Registry), registry: Registry, id: Int)
}

pub fn main() {
  let self = process.new_subject()

  let assert Ok(_supervisor) =
    supervisor.start_spec(
      supervisor.Spec(
        argument: self,
        max_frequency: 5,
        frequency_period: 1,
        init: fn(children) {
          children
          // First spawn the registry.
          |> supervisor.add(registry_spec())
          // Then spawn all sessions.
          |> supervisor.add(session_spec())
          |> supervisor.add(session_spec())
          |> supervisor.add(session_spec())
          // Finally notify the main process we're ready.
          |> supervisor.add(ready())
        },
      ),
    )

  // The ready helper will send back a message with our registry.
  let assert Ok(registry) = process.receive(self, 500)
  let assert [_session_2] = chip.members(registry, 2, 50)
}

fn registry_spec() {
  // The registry childspec first starts the registry.
  supervisor.worker(fn(_caller: process.Subject(Registry)) {
    chip.start(chip.Named("sessions"))
  })
  // After starting we transform the parameter from caller into a context for
  // the sessions we want to register.
  |> supervisor.returning(fn(caller, registry) { Context(caller, registry, 1) })
}

// Mock helpers to emulate a session.
type Message =
  Nil

fn start_session(
  with registry: Registry,
  id id: Int,
) -> supervisor.StartResult(Message) {
  // Mock function to startup a new session.
  let session = process.new_subject()
  chip.register(registry, id, session)
  Ok(session)
}

fn session_spec() {
  supervisor.worker(fn(context: Context) {
    start_session(context.registry, context.id)
  })
  |> supervisor.returning(fn(context: Context, _game_session) {
    // Increments the id for the next session.
    Context(..context, id: context.id + 1)
  })
}

// Helper to return the registry's subject to the main flow.
fn ready() {
  // This childspec is a noop addition to the supervisor, on return it
  // will send back the registry reference.
  supervisor.worker(fn(_context: Context) { Ok(process.new_subject()) })
  |> supervisor.returning(fn(context: Context, _self) {
    process.send(context.caller, context.registry)
    Nil
  })
}
Search Document