Unexpected message
Do you know on which process the start_link
function of a GenServer
in Elixir is run?
GenServer
is one of the central building blocks often turned to when building something in Elixir but the above question is not something I had ever really given any thought.
I knew that you had to be mindful of which process is doing the work when working with agents but had somehow missed that the same might be the case with GenServers
.
Background
I’ve been playing around with nerves, a Raspberry Pi and an Arduino board. One the things I was trying to do was to read messages sent from the Arduino to the Pi over USB.
The nerves_uart
makes it simple to read lines from USB and to receive those lines as as messages. So the plan was to setup a GenServer
to start nerves_uart
and to receive the messages from it.
Unexpected messages
The first attempt at the code to start nerves_uart
and receive the messages looked a bit like this:
The HelloNerves.Messages
is then started with as a worker with worker(HelloNerves.Messages, [])
.
Compile, upload to the Raspberry PI, sit back and watch the data flow.
Unintended recipient
Well, not quite as it turns out. Because instead of the loging of the data I expected I started seeing the following error message instead:
Supervisor received unexpected message {:nerves_uart, "ttyACM0", "400"}
Let’s dig into the nerves_uart
source and see if we can find the source of our troubles.
The source for the Nerves.UART GenServer
can be found here: Github.
If we look at the function that handles the open
call that the pid
of the calling function gets stored in the state as controlling_process
. And messages are sent to the controlling_process
, but why do these messages end up at the Supervisor
?
The call sequence
HelloNerves.Messages
is started with the worker()
statement causing HelloNerves.Messages.start_link
to be called which then in turn calls Nerves.UART.start_link
.
In overview something like this:
So Nerves.UART.start_link
was in fact called from the Supervisor
process. Which in then makes it the recipient of messages from Nerves.UART
.
The solution
Digging trough first the Elixir and then the Erlang the GenServer
documentation revealed the following in the description of init/1
function:
this function is called by the new process to initialize
The point here being that while the HelloNerves.Messages.start_link
function is run on the Supervisors process the init/1
function is called on the newly spawned process for the HelloNerves.Messages
GenServer.
So if I want to Nerves.UART.start_link
from process of the HelloNerves.Messages GenServer
it has to be done from the init/1
callback.
This leads to the following rewrite:
Conclusion
In hindsight it seems obvious that start_link
would be run on the callers process. But it took me a fair while to figure out that it was init/1
I was looking for.
Coming from a mainly OOP background I think that there is a lot of hidden complexity lurking here not stemming from anything but lack of experience.