How to supervise OTP server turned into a component

Hi all,

New to Elixir and loving it so far :smiley: I’ve been making my way through Programming Elixir >= 1.6 by Dave Thomas and really liking the parts where he goes beyond just teaching you the language.

One of these parts is in Chapter 17 where he states that he likes to seperate the API, GenServer and Implementation specifics into seperate modules and files. I really like that too.

Hence, I implemented the exercise (the Stack GenServer built throughout Chapter 17) in that way as well. However, when you get to the next chapter (OTP: Supervisors) and want to supervise that GenServer by adding it to the children of a standard OTP application, there is a little bit of a hickup. This seems to be due to the split of the API and the GenServer.

My API (module Stack defined in stack.ex) contains:

  • start_link
  • push
  • pop

My GenServer (module Stack.Server defined in stack/server.ex) contains:

  • init
  • handle_call
  • handle_cast
  • terminate

Forgive me to cut to the chase, I can give more details later if needed, but the big question with “splitting this server up into a component” is: which of these do I pass as a child to my supervisor? (both give errors)"

Best regards,
Nick

Hi Nick!

Glad to hear you’re enjoying the language!

Your supervisor needs to know which module contains the exported function for starting the module. By the looks of your API, it appears to be Stack.start_link.

You might consider reading this post before going further.

– angelo

For completeness, let me add the split proposed by Dave Thomas in Chapter 17 - OTP: Servers

The API - lib/sequence.ex

defmodule Sequence do
  @server Sequence.Server
  
  def start_link(current_number) do
    GenServer.start_link(@server, current_number, name: @server)
  end

  def next_number do
    GenServer.call(@server, :next_number)
  end

  def increment_number(delta) do
    GenServer.cast(@server, {:increment_number, delta})
  end
end

The GenServer implementation - lib/sequence/server.ex

defmodule Sequence.Server do
  use GenServer
  alias Sequence.Impl
  
  def init(initial_number) do
    { :ok, initial_number }
  end
  
  def handle_call(:next_number, _from, current_number) do
    { :reply, current_number, Impl.next(current_number) }
  end

  def handle_cast({:increment_number, delta}, current_number) do
    { :noreply, Impl.increment(current_number, delta) }
  end

  def format_status(_reason, [ _pdict, state ]) do
    [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] 
  end
end

The actual implementation specifics - lib/sequence/impl.ex

defmodule Sequence.Impl do
  def next(number),             do: number + 1
  def increment(number, delta), do: number + delta
end

The problem I face when wanting to supervise this is:

  • You pass { Sequence, 123 } as child and get the error that Sequence does not define child_spec/1 (which you normally get by ‘use GenServer’, that is now in a seperate module Sequence.Server defined in server.ex)

  • You pass { Sequence.Server, 123 } as child and get the error that Sequence.Server does not define start_link/1 (which is now in a seperate API module Sequence defined in sequence.ex)

I eventually added the following a start_link/1 definition to Sequence.Server to be able start the Sequence.Server with an OTP supervisor.

def start_link(current_number) do
  GenServer.start_link(__MODULE__, current_number, name: __MODULE__)
end

You could then have the start_link/1 definition from the Sequence API module call the start_link/1 definition from Sequence.Server instead of calling GenServer.start_link itself. What still bothers me then is the @server definition in the Sequence module. If you would rename either the variable or the Sequence.Server module, calling this breaks the API (calls or casts will end up at a non-existing server).

You should be able to pass in a child spec directly, e.g. instead of defining the child as {Sequence, 123} per your example, use

  %{
    id: Sequence,
    start: {Sequence, :start_link, [123]}
  }

Or define the child_spec/1 within Server (per https://hexdocs.pm/elixir/Supervisor.html#module-child_spec-1):

def child_spec(current_number) do
  %{
    id: __MODULE__,
    start: {__MODULE__, :start_link, [current_number]}
  }
end

with that definition in place, you can revert to defining the child as {Sequence, 123} and it should work.

You can read more about child specs and how to make them work for you in a blog post I wrote: http://davidsulc.com/blog/2018/07/09/pooltoy-a-toy-process-pool-manager-in-elixir-1-6-part-1-2/