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