Messages between GenServers

@gon782, thanks again for your help with this question. After our conversation in this post, I found the To spawn or not to spawn? article which helped me better understand processes should not be used as direct replacements for OOP objects (and how to use them properly). Nonetheless, I’ve still learned a lot with this project and worked on the supervision tree as you suggested.

Here’s what I came up with:

A. supervisor(PreparersSupervisor(:rest_for_one))
    a. supervisor(Registry.Preparers)
        i. worker(Registry.Preparers)
    b. worker(Mechanic)

B. supervisor(TripSystemsSupervisor(:rest_for_one))
    a. supervisor(Registry.ProcessRegistry)
        i. worker(Registry.ProcessRegistry)
    b. supervisor(TripSupervisor(:simple_one_for_one))
        i. supervisor(SingleTripSupervisor(:one_for_all))
           AA. supervisor(Trip.BikeSupervisor(:simple_one_for_one))
                  aa. worker(Bicycle)
           BB. worker(Trip)

Supervisors

defmodule PreparersSupervisor do
  use Supervisor

  def start_link() do
    IO.puts "Starting PreparersSupervisor"

    Supervisor.start_link(
      __MODULE__,
      nil
    )
  end

  def init(_) do
    children = [
      supervisor(Registry, [:duplicate, Registry.Preparers]),
      worker(Mechanic, []),
    ]
    supervise(children, strategy: :rest_for_one)
  end
end

defmodule Trip.SystemsSupervisor do
  use Supervisor

  def start_link() do
    IO.puts "Starting Trip.SystemSupervisor"

    Supervisor.start_link(
      __MODULE__,
      nil
    )
  end

  def init(_) do
    children = [
      supervisor(Registry, [:unique, Registry.ProcessRegistry], id: :proc),
      supervisor(TripSupervisor, [])
    ]

    supervise(children, strategy: :rest_for_one)
  end
end

defmodule TripSupervisor do
  use Supervisor

  def start_link() do
    IO.puts "Starting TripSupervisor"

    Supervisor.start_link(
      __MODULE__,
      nil,
      name: :trips_supervisor
    )
  end

  def start_child(name, num_bikes) do
    Supervisor.start_child(
      :trips_supervisor,
      [name, num_bikes]
    )
  end

  def init(_) do
    children = [
      supervisor(SingleTripSupervisor, []),
    ]

    supervise(children, strategy: :simple_one_for_one)
  end
end

defmodule SingleTripSupervisor do
  use Supervisor

  def start_link(name, num_bikes) do
    IO.puts "Starting SingleTripSupervisor"

    Supervisor.start_link(
      __MODULE__,
      {name, num_bikes},
      name: via_tuple(name)
    )
  end

  defp via_tuple(name) do
    {:via, Registry, {Registry.ProcessRegistry, {:trip_sup, name}}}
  end

  def init({name, num_bikes}) do
    children = [
      supervisor(Trip.BikeSupervisor, [name]),
      worker(Trip, [name, num_bikes]),
    ]

    supervise(children, strategy: :one_for_all)
  end
end

defmodule Trip.BikeSupervisor do
  use Supervisor

  def start_link(name) do
    IO.puts "Starting Trip.BikeSupervisor"

    Supervisor.start_link(
      __MODULE__,
      nil,
      name: via_tuple(name)
    )
  end

  defp via_tuple(name) do
    {:via, Registry, {Registry.ProcessRegistry, {:bike_sup, name}}}
  end

  def start_child(name, bike_id) do
    Supervisor.start_child(
      via_tuple(name),
      [bike_id]
    )
  end

  def init(_) do
    children = [
      worker(Bicycle, [])
    ]

    supervise(children, strategy: :simple_one_for_one)
  end
end

GenServers

defmodule Trip do
  use GenServer

  defstruct bicycles: :none, bikes_ready: 0, name: :none

  # Client API

  def start_link(name, num_bikes) do
    IO.puts "Starting Trip for #{name}"

    GenServer.start_link(
      __MODULE__,
      {name, num_bikes},
      name: via_tuple(name)
    )
  end

  defp via_tuple(name) do
    {:via, Registry, {Registry.ProcessRegistry, {:trip, name}}}
  end

  def new(name, num_bikes) do
    TripSupervisor.start_child(name, num_bikes)
  end

  def ready?(name) do
    GenServer.call(via_tuple(name), :trip_ready?)
  end

  def prepare(name) do
    GenServer.cast(via_tuple(name), :prepare)
  end

  # Server Callbacks

  def init({name, num_bikes}) do
    send(self(), {:real_init, name, num_bikes})

    {:ok, %Trip{name: name}}
  end

  def handle_call(:trip_ready?, _from, state) do
    {:reply, trip_ready?(state), state}
  end

  def handle_cast(:prepare, state) do
    preparers = get_preparers()
    prepare_trip(preparers)
    {:noreply, state}
  end

  def handle_cast({:bicycles, caller}, state) do
    send(caller, {:bicycles, state.bicycles, self()})
    {:noreply, state}
  end

  def handle_info({:bike_prepped, _bike}, state) do
    {:noreply, %Trip{state | bikes_ready: state.bikes_ready + 1}}
  end

  def handle_info({:real_init, name, num_bikes}, state) do
    bicycles =
      for bike <- 1..num_bikes do
        IO.puts "creating bike: #{bike}"
        bike_id = "#{name} bike: #{bike}"
        {:ok, _bike} =
          Trip.BikeSupervisor.start_child(name, bike_id)
        bike_id
      end

    {:noreply, %Trip{state | bicycles: bicycles}}
  end

  def handle_info(_, state) do
    {:noreply, state}
  end

  # Helper Functions

  defp get_preparers() do
    Registry.Preparers
    |> Registry.lookup(:preparer)
    |> Enum.map(fn({pid, module}) -> {pid, module} end)
  end

  defp prepare_trip(preparers) do
    Enum.each preparers, fn({_pid, module}) ->
      module.prepare()
    end
  end

  defp trip_ready?(%{bicycles: bicycles, bikes_ready: bikes_ready}) do
    length(bicycles) <= bikes_ready
  end
end

defmodule Mechanic do
  use GenServer

  # Client API

  def start_link() do
    IO.puts "Starting Mechanic"

    GenServer.start_link(__MODULE__, nil, name: :mechanic)
  end

  def prepare() do
    GenServer.cast(:mechanic, {:prepare_trip, self()})
  end

  # Server Callbacks

  def init(_) do
    Registry.register(Registry.Preparers, :preparer, Mechanic)
    {:ok, %{}}
  end

  def handle_cast({:prepare_trip, trip}, state) do
    request_bicycles(trip)
    {:noreply, state}
  end

  def handle_info({:bicycles, bicycles, trip}, state) do
    {:noreply, service_bicycles(bicycles, trip, state)}
  end

  def handle_info({:bicycle_serviced, bike}, state) do
    {trip, new_state} = Map.pop(state, bike)
    send(trip, {:bike_prepped, bike})
    {:noreply, new_state}
  end

  def handle_info(_, state) do
    {:noreply, state}
  end

  # Helper Functions

  def request_bicycles(trip) do
    GenServer.cast(trip, {:bicycles, self()})
  end

  defp service_bicycles(bicycles, trip, state) do
    Enum.reduce bicycles, state, fn bike, state ->
      Bicycle.service(bike)
      Map.put(state, bike, trip)
    end
  end
end

defmodule Bicycle do
  use GenServer

  defstruct ready?: false, type: "mountain", id: :none

  # Client API

  def start_link(id) do
    IO.puts "Starting bike: #{id}"

    GenServer.start_link(
      __MODULE__,
      id,
      name: via_tuple(id)
    )
  end

  defp via_tuple(id) do
    {:via, Registry, {Registry.ProcessRegistry, {:bike, id}}}
  end

  def service(bike) do
    GenServer.cast(via_tuple(bike), {:service, self()})
  end

  # Server Callbacks

  def init(name) do
    {:ok, %Bicycle{id: name}}
  end

  def handle_cast({:service, caller}, state) do
    send(caller, {:bicycle_serviced, state.id})
    {:noreply, %Bicycle{state | ready?: true}}
  end
end

Trip.Application

defmodule Trip.Application do
  use Application

  def start(_type, _args) do
    PreparersSupervisor.start_link()
    Trip.SystemsSupervisor.start_link()
  end
end

Testing Supervisors in IEx

Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]

Starting PreparersSupervisor
Starting Mechanic
Starting Trip.SystemSupervisor
Starting TripSupervisor
Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> :observer.start
:ok
iex(2)> Trip.new("Axel", 3)  
Starting SingleTripSupervisor
Starting Trip.BikeSupervisor
Starting Trip for Axel
creating bike: 1
Starting bike: Axel bike: 1
creating bike: 2
Starting bike: Axel bike: 2
creating bike: 3
Starting bike: Axel bike: 3
{:ok, #PID<0.166.0>}

iex(3)> [{bike_pid, _}] = Registry.lookup(Registry.ProcessRegistry, {:bike, "Axel bike: 1"}) 
[{#PID<0.169.0>, nil}]
iex(4)> Process.exit(bike_pid, :kill)
Starting bike: Axel bike: 1
true

iex(5)> [{trip_pid, _}] = Registry.lookup(Registry.ProcessRegistry, {:trip, "Axel"})   [{#PID<0.168.0>, nil}]
iex(6)> Process.exit(trip_pid, :kill)                                                
true
iex(7)> Starting Trip.BikeSupervisor
Starting Trip for Axel
creating bike: 1
Starting bike: Axel bike: 1
creating bike: 2
Starting bike: Axel bike: 2
creating bike: 3
Starting bike: Axel bike: 3

iex(8)> Trip.ready?("Axel")                                                            
false
iex(9)> Trip.prepare("Axel")                                                           
:ok
iex(10)> Trip.ready?("Axel")                                                            
true
1 Like