@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