Messages between GenServers

I’m playing around with GenServers and passing messages between them. As an experiment, I’m using them like objects that are passing messages between each other. I’m using one of the exercises in Practical Object Oriented Design for Ruby.

I’ve got a Trip, a Mechanic, and a Bicycle module. When I start the Trip GenServer, it also starts 2 Bicycle GenServers and holds their PIDs in the Trip struct. Then I start a Mechanic GenServer. I’m making a GenServer call from IEX to the Trip GenServer. When Trip handles that call, it sends a message with its PID to the Mechanic GenServer to {:prepare_trip, trip_pid}. I only want to send the Trip PID and expect the Mechanic GenServer to send a message back to the Trip GenServer to get the Bicycle PIDs so it can send a message to the Bicycle GenServers to get them “ready”.

The problem is when the Mechanic GenServer sends the message to the Trip GenServer to get the bicycles, it times out because the Trip GenServer is waiting for the reply from the earlier {:prepare_trip, trip_pid} message. I know I could just send the Trip struct (including the bicycle PIDs) with the {:prepare_trip} message, but does anyone have a way to facilitate message like I’ve described? I also tried to use a Task, but wasn’t able to get it to work.

Here is the error

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, trip} = Trip.start_link()                  
{:ok, #PID<0.140.0>}
iex(2)> {:ok, mechanic} = Mechanic.start_link            
{:ok, #PID<0.144.0>}
iex(3)> GenServer.call(trip, {:prepare, mechanic}, 10000) 
** (EXIT from #PID<0.138.0>) exited in: GenServer.call(#PID<0.144.0>, {:prepare_trip}, 5000)
    ** (EXIT) time out

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 
16:59:55.246 [error] GenServer #PID<0.140.0> terminating
** (stop) exited in: GenServer.call(#PID<0.144.0>, {:prepare_trip}, 5000)
    ** (EXIT) time out
    (elixir) lib/gen_server.ex:737: GenServer.call/3
    (poodr) lib/poodr/trip.ex:22: Trip.handle_call/3
    (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:prepare, #PID<0.144.0>}
State: %Trip{bicycles: [#PID<0.141.0>, #PID<0.142.0>]}

16:59:55.246 [error] GenServer #PID<0.144.0> terminating
** (stop) exited in: GenServer.call(#PID<0.140.0>, {:bicycles}, 5000)
    ** (EXIT) time out
    (elixir) lib/gen_server.ex:737: GenServer.call/3
    (poodr) lib/poodr/trip.ex:56: Mechanic.prepare_trip/1
    (poodr) lib/poodr/trip.ex:49: Mechanic.handle_call/3
    (stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:647: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:prepare_trip}
State: []

If I send the GenServer call to the Mechanic GenServer from the IEX process with the Trip pid, everything works as expected.

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, trip} = Trip.start_link()       
{:ok, #PID<0.140.0>}
iex(2)> {:ok, mechanic} = Mechanic.start_link
{:ok, #PID<0.144.0>}
iex(3)> GenServer.call(mechanic, {:prepare_trip, trip})
:ok

Code:

defmodule Trip do
  use GenServer

  defstruct bicycles: :none

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__, %Trip{})
  end

  # Server Callbacks

  def init(trip) do
    {:ok, bike1} = Bicycle.start_link
    {:ok, bike2} = Bicycle.start_link
    bicycles = [bike1, bike2]
    {:ok, %Trip{trip | bicycles: bicycles}}
  end

  def handle_call({:prepare, preparer}, _from, state) do
    prepare_trip(preparer)
    {:reply, :ok, state}
  end

  def handle_call({:bicycles}, _from, state) do
    {:reply, state.bicycles, state}
  end

  # Helper Functions

  defp prepare_trip(preparer) do
    trip = self()
    GenServer.call(preparer, {:prepare_trip, trip})
  end
end

defmodule Mechanic do
  use GenServer

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__,[])
  end

  # Server Callbacks

  def handle_call({:prepare_trip, trip}, _from, state) do
    prepare_trip(trip)
    {:reply, :ok, state}
  end

  # Helper Functions

  defp prepare_trip(trip) do
    bicycles = get_bicycles(trip)
    Enum.each bicycles, fn bicycle ->
      prepare_bicycle(bicycle)
    end
  end

  def get_bicycles(trip) do
    GenServer.call(trip, {:bicycles})
  end

  defp prepare_bicycle(bicycle) do
    GenServer.call(bicycle, {:service})
  end
end

defmodule Bicycle do
  use GenServer

  defstruct ready?: false, type: "mountain"

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__,%Bicycle{})
  end

  # Server Callbacks

  def init(bicycle) do
    {:ok, bicycle}
  end

  def handle_call({:service}, _from, state) do
    {:reply, :ok, %Bicycle{state | ready?: true}}
  end
end

You have a few things to consider in this case:

  1. You can make one of these calls a cast to make someone not wait for an answer. The obvious choice is to have the trip request a trip preparation from the mechanic asynchronously, simply telling the mechanic to prepare, since all the mechanic is replying with is :ok. The Trip will then not wait for an answer to its initial request and will then be able to respond to the subsequent call for bikes from the mechanic.

  2. Consider adding a trip coordinator that takes care of contacting the different parties, as they probably would in real life.

  3. Consider making the initial request a cast, but send a response of some kind as an :info message (a normal message sent via send). To make sure that this is properly tied to a previous request you can consider having a registry of refs corresponding to the different trips requested. In this case, though, since you do not seem to actually return anything of note as a response to :prepare_trip (sent to the mechanic), this reply likely won’t actually give you much anyway.

When you make calls, you are essentially saying that you will hold up the calling process until you get a response or you time out, so in most cases that response should be essential. You will never be able to have mutually dependent calls, so you will likely have to reconsider how your processes interact with eachother and if it makes sense to simply not do it like you would in Ruby.

Other things:

  1. One-element tuples are pointless, consider making them just the atoms that they contain.

  2. Make functions that call GenServer.call and so on, so that you have a better interface to the functionality of your processes.

  3. Consider making things that have a clear stateful flow gen_statems, as they are made specifically for state machines. It seems that most people don’t use them, because they think they can do everything with gen_servers, and while they’re not wrong in that you can do everything with gen_server, it’s mostly a waste of time encoding the same things in a clunkier way, having what is essentially simple state transitions in a state record/struct.

2 Likes

Thanks for the explanation with different options to consider along with the other recommendations!

1 Like

I decided to use a TripCoordinator. I also added a Caterer as another preparer. I’m going to check out gen_statem next. Thanks again!

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, coordinator} = TripCoordinator.start_link()    
{:ok, #PID<0.148.0>}
iex(2)> {:ok, mechanic} = Mechanic.start_link                
{:ok, #PID<0.150.0>}
iex(3)> {:ok, caterer} = Caterer.start_link()                
{:ok, #PID<0.152.0>}
iex(4)> {:ok, customer} = Customer.start_link(10)            
{:ok, #PID<0.155.0>}
iex(5)> {:ok, trip} = Trip.start_link()                      
{:ok, #PID<0.157.0>}
iex(6)> Trip.add_customer(trip, customer)                    
{:ok, #PID<0.155.0>}
iex(7)> preparers = [mechanic, caterer]                      
[#PID<0.150.0>, #PID<0.152.0>]
iex(8)> TripCoordinator.prepare(coordinator, trip, preparers)
:ok
defmodule Trip do
  use GenServer

  defstruct bicycles: :none, customer: :none, lunches: 0

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__, %Trip{})
  end

  def bicycles(trip) do
    GenServer.call(trip, :bicycles)
  end

  def add_customer(trip, customer) do
    GenServer.call(trip, {:add_customer, customer})
  end

  # Server Callbacks

  def init(trip) do
    {:ok, bike1} = Bicycle.start_link
    {:ok, bike2} = Bicycle.start_link
    bicycles = [bike1, bike2]
    {:ok, %Trip{trip | bicycles: bicycles}}
  end

  def handle_call(:bicycles, _from, state) do
    {:reply, state.bicycles, state}
  end

  def handle_call(:customer, _from, state) do
    {:reply, state.customer, state}
  end

  def handle_call({:add_customer, customer}, _from, state) do
    {:reply, {:ok, customer}, %Trip{state | customer: customer}}
  end

  def handle_call({:add_lunches, quantity}, _from, state) do
    {:reply, {:ok, quantity}, update_lunches(state, quantity)}
  end

  defp update_lunches(state, quantity) do
    lunches = state.lunches + quantity
    %Trip{state | lunches: lunches}
  end
end

defmodule TripCoordinator do
  use GenServer

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__, [])
  end

  def prepare(coordinator, trip, preparers) do
    GenServer.call(coordinator, {:prepare, trip, preparers})
  end

  # Server Callbacks

  def init(state) do
    {:ok, state}
  end

  def handle_call({:prepare, trip, preparers}, _from, state) do
    prepare_trip(trip, preparers)
    {:reply, :ok, state}
  end

  # Helper Functions

  defp prepare_trip(trip, preparers) do
    Enum.each(preparers, fn preparer ->
      GenServer.call(preparer, {:prepare_trip, trip})
    end)
  end
end

defmodule Mechanic do
  use GenServer

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__,[])
  end

  # Server Callbacks

  def handle_call({:prepare_trip, trip}, _from, state) do
    prepare_trip(trip)
    {:reply, :ok, state}
  end

  # Helper Functions

  defp prepare_trip(trip) do
    bicycles = bicycles(trip)
    Enum.each bicycles, fn bicycle ->
      prepare_bicycle(bicycle)
    end
  end

  def bicycles(trip) do
    GenServer.call(trip, :bicycles)
  end

  defp prepare_bicycle(bicycle) do
    GenServer.call(bicycle, :service)
  end
end

defmodule Bicycle do
  use GenServer

  defstruct ready?: false, type: "mountain"

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__,%Bicycle{})
  end

  # Server Callbacks

  def init(bicycle) do
    {:ok, bicycle}
  end

  def handle_call(:service, _from, state) do
    {:reply, :ok, %Bicycle{state | ready?: true}}
  end
end

defmodule Caterer do
  use GenServer

  defstruct food_storage: :none

  def start_link() do
    GenServer.start_link(__MODULE__, %Caterer{})
  end

  def init(caterer) do
    {:ok, food_storage} = FoodStorage.start_link()
    {:ok, %Caterer{caterer | food_storage: food_storage}}
  end

  def handle_call({:prepare_trip, trip}, _from, state) do
    {:reply, prepare_trip(state.food_storage, trip), state}
  end

  def prepare_trip(food_storage, trip) do
    trip
    |> customer
    |> people
    |> get_lunches(food_storage)
    |> add_lunches(trip)
  end

  def customer(trip) do
    GenServer.call(trip, :customer)
  end

  def people(customer) do
    GenServer.call(customer, :people)
  end

  def get_lunches(quantity, food_storage) do
    GenServer.call(food_storage, {:get_lunches, quantity})
  end

  def add_lunches(quantity, trip) do
    GenServer.call(trip, {:add_lunches, quantity})
  end
end

defmodule FoodStorage do
  use GenServer

  defstruct lunches: 100

  def start_link() do
    GenServer.start_link(__MODULE__, %FoodStorage{})
  end

  def init(food_storage) do
    {:ok, food_storage}
  end

  def handle_call(:lunches, _from, state) do
    {:reply, state.lunches, state}
  end

  def handle_call({:get_lunches, quantity}, _from, state) do
    lunches = state.lunches - quantity
    new_state = %FoodStorage{state | lunches: lunches}
    {:reply, quantity, new_state}
  end
end

defmodule Customer do
  defstruct people: 0

  def start_link(people) do
    GenServer.start_link(__MODULE__,%Customer{people: people})
  end

  def init(customer) do
    {:ok, customer}
  end

  def handle_call(:people, _from, state) do
    {:reply, state.people, state}
  end
end
1 Like

You’re most welcome, Axel. Great job on making your interfaces and generally cleaning up. As a small note, it could be better to put a function that takes a Trip as its first argument, for example, in the Trip module, and so on.

I really like how this is shaping up.

How is your supervision looking currently? Have you tried to run, as an example, Process.exit bike_pid, :kill to see what happens? Which processes die when you do this and does it make sense?

Now, this is a pretty contrived example, but I think it’s also good for learning this part anyway. You’ll want to deliberately design the flow of errors in your system, so it’s worth killing different parts of it to see which data you’re losing when something dies, etc.

1 Like

Thanks for the feedback! I have no supervisors and all my processes are linked. If I kill a bike PID it kills all my other processes. Haha, definitely not what I want. Setting up a supervision tree is something to work on next.

Also, I know you would typically put a function that takes a Trip PID as the first argument (i.e. interface to Trip GenServer) in the Trip module, but I wanted to see what it would look like to set up my interfaces so the calling GenServer knows the message format, but not the module name of the GenServer PID it was sending a message to. I’m not sure if it was all that helpful, although I thought using this approach in the TripCoordinator.prepare_trip/2 function was kind of interesting. TripCoordinator has “preparer” PIDs (mechanic & caterer) that both respond to a GenServer call with {:prepare_trip, trip}. It doesn’t need to check the GenServer module of the PID before making the call. Right now the two preparer GenServer calls are synchronous, but I was thinking about making them asynchronous (after I get my supervisors figured out).

defmodule TripCoordinator do
  use GenServer

  def prepare(coordinator, trip, preparers) do
    GenServer.call(coordinator, {:prepare, trip, preparers})
  end

  def handle_call({:prepare, trip, preparers}, _from, state) do
    prepare_trip(trip, preparers)
    {:reply, :ok, state}
  end

  defp prepare_trip(trip, preparers) do
    Enum.each(preparers, fn preparer ->
      GenServer.call(preparer, {:prepare_trip, trip})
    end)
  end
end

defmodule Mechanic do
  use GenServer

  def handle_call({:prepare_trip, trip}, _from, state) do
    prepare_trip(trip)
    {:reply, :ok, state}
  end
end

defmodule Caterer do
  use GenServer

  def handle_call({:prepare_trip, trip}, _from, state) do
    {:reply, prepare_trip(state.food_storage, trip), state}
  end
end

I just want to complement the existing answers and point out that this is problem is of course not specific to your original OO problem, it exists whenever you have concurrent systems and use synchronous communication. In your specific case where you want synchronous object calls you could rewrite it so that you send off asynchronous request and then explicitly wait for the reply. The trick being that while waiting for the reply you could process other requests coming in and then return to wait for the reply to your original request. It would get a bit tricky as you would potentially have to handle stacks of requests to make it fail-safe.

A much better solution is to restructure the whole solution to only use asynchronous sends and then sit and wait for the reply at suitable times. These systems very quickly become stateful, as in Finite State Machine stateful, but when done correctly they can guarantee never to block, which is usually critical. Basically each state has to know what to do with every message that can arrive. This is actually much less difficult than it sounds.

EDIT: I have a fantastic demo of the problem of synchronous communication with my luerl space ship demo. In one mode each ship goes and synchronously requests info from its neighbours and the whole system stops extremely quickly. :grinning:

5 Likes

Thanks for the additional info! I have a follow up question about using send vs cast. @gon782 provided an option in his initial answer:

Consider making the initial request a cast, but send a response of some kind as an :info message (a normal message sent via send). To make sure that this is properly tied to a previous request you can consider having a registry of refs corresponding to the different trips requested. In this case, though, since you do not seem to actually return anything of note as a response to :prepare_trip (sent to the mechanic), this reply likely won’t actually give you much anyway.

Is replacing my calls with casts for the requests and sends for the responses how you would restructure the solution to use asynchronous sends? Or does it not really matter which type of asynchronous message you use (cast vs send) except for making sure the callbacks match the message type you expect to receive and the state can handle that message?

I want to make sure I understand what you meant by:

A much better solution is to restructure the whole solution to only use asynchronous sends and then sit and wait for the reply at suitable times.

Thank you!
Axel

Here is my original code (Trip, Mechanic, and Bicycle) updated to use asynchronous messages (cast and send) instead of call for Trip.prepare/2.

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, trip} = Trip.start_link        
{:ok, #PID<0.132.0>}
iex(2)> {:ok, mechanic} = Mechanic.start_link
{:ok, #PID<0.136.0>}
iex(3)> Trip.ready?(trip)                    
false
iex(4)> Trip.prepare(trip, [mechanic])       
:ok
iex(5)> Trip.ready?(trip)                    
true
defmodule Trip do
  use GenServer

  defstruct bicycles: :none, bikes_ready: 0

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__, %Trip{})
  end

  def ready?(trip) when is_pid trip do
    GenServer.call(trip, :trip_ready?)
  end

  def prepare(trip, preparers) do
    GenServer.cast(trip, {:prepare, preparers})
  end

  # Server Callbacks

  def init(trip) do
    {:ok, bike1} = Bicycle.start_link
    {:ok, bike2} = Bicycle.start_link
    bicycles = [bike1, bike2]
    {:ok, %Trip{trip | bicycles: bicycles}}
  end

  def handle_call(:trip_ready?, _from, state) do
    case trip_ready?(state) do
      true  -> {:reply, true, state}
      false -> {:reply, false, state}
    end
  end

  def handle_cast({:prepare, preparers}, state) do
    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

  # Helper Functions

  defp prepare_trip(preparers) do
    Enum.each preparers, fn preparer ->
      request_prep(preparer)
    end
  end

  defp request_prep(preparer) do
    GenServer.cast(preparer, {:prepare_trip, self()})
  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
    GenServer.start_link(__MODULE__,%{})
  end

  # Server Callbacks

  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

  # 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 ->
      service_bike(bike)
      Map.put(state, bike, trip)
    end
  end

  defp service_bike(bike) do
    GenServer.cast(bike, {:service, self()})
  end
end

defmodule Bicycle do
  use GenServer

  defstruct ready?: false, type: "mountain"

  # Client API

  def start_link() do
    GenServer.start_link(__MODULE__,%Bicycle{})
  end

  # Server Callbacks

  def init(bicycle) do
    {:ok, bicycle}
  end

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

@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