Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Supervision Trees 🌳

13-supervision-trees.livemd

Supervision Trees 🌳

Hello Fault Tolerence

In Elixir, we usually dont start GenServers with start_link

Instead we are using Supervisors which allows you to create multiple GenServers and monitor them

Lets create a PizzaShop GenServer and start few of them using Supervisor

Take a note in here that we are passing name option to start_link/3. This registers GenServer under the name given by user

So when we start Supervisor we can pass this argument and find the correct PizzaShop by the name we given earlier

defmodule PizzaShop do
  use GenServer

  # Define a maximum order limit for the shop
  @max_orders 5

  ## Client API

  def start_link(name) do
    IO.puts("Starting PizzaShop #{inspect(name)}")
    GenServer.start_link(__MODULE__, %{orders: 0}, name: name)
  end

  # Place an order
  def place_order(pid) do
    GenServer.call(pid, :place_order)
  end

  # Check the current number of orders
  def check_orders(pid) do
    GenServer.call(pid, :check_orders)
  end

  # Reset the order count (for demonstration purposes)
  def reset_orders(pid) do
    GenServer.cast(pid, :reset_orders)
  end

  ## Server Callbacks
  @impl true
  def init(state) do
    {:ok, state}
  end

  @impl true
  def handle_call(:place_order, _from, %{orders: orders} = state) when orders < @max_orders do
    # Increment the order count
    IO.puts("Placing order")
    {:reply, :ok, %{state | orders: orders + 1}}
  end

  @impl true
  def handle_call(:place_order, _from, %{orders: orders}) when orders >= @max_orders do
    # Simulate a crash if the maximum order limit is reached
    IO.puts("Ohh we are overloaded")
    {:stop, :max_orders_reached, :error}
  end

  @impl true
  def handle_call(:check_orders, _from, state) do
    # Return the current order count
    {:reply, state.orders, state}
  end

  @impl true
  def handle_cast(:reset_orders, _state) do
    # Reset the order count to zero
    {:noreply, %{orders: 0}}
  end

  @impl true
  def terminate(reason, _state) do
    IO.puts("PizzaShop terminated due to: #{reason}")
    :ok
  end
end

Creating the Supervisor

Supervisor can have many children as shown below.

When we call Supervisor.init its expecting a list of children as child_specs

In simple terms it simply tells about the name to identify each PizzaShop. Take a look at id field in the map(:shop1 etc). This is like a unique name to identify each shop

defmodule PizzaShopSupervisor do
  use Supervisor

  def start_link() do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    # Define child specs for multiple pizza shops
    children = [
      %{
        id: :shop1,
        start: {PizzaShop, :start_link, [:shop1]},
        restart: :permanent
      },
      %{
        id: :shop2,
        start: {PizzaShop, :start_link, [:shop2]},
        restart: :permanent
      },
      %{
        id: :shop3,
        start: {PizzaShop, :start_link, [:shop3]},
        restart: :permanent
      }
    ]

    # Start the supervisor with the defined child specs
    Supervisor.init(children, strategy: :one_for_one)
  end
end

Lets start it

{:ok, _sup} = PizzaShopSupervisor.start_link()

Lets lookup for the children started by Supervisor This is for demo purpose only

In real world cases you may use Registry to lookup the children processes

PizzaShop.place_order(:shop1)