About separate GenServer into a client module and a server module

When I read the getting start document.

I saw the words below.

You can either combine both parts into a single module or you can separate them into a client module and a server module.

So I tried to split my code. I defined the server callback as below.

defmodule Server do
  use GenServer

  @impl true
  def init(:ok) do
    {:ok, %{}}
  end
  @impl true
  def handle_cast({:test, args}, state) do
    IO.inspect "Hello, #{inspect args}}"
    {:noreply, state}
  end
end

And defined the client api as below.

defmodule Client do
  def start_link(opts) do
    GenServer.start_link(Server, :ok, name: :cache)
  end
  def test(message) do
    GenServer.cast(:cache, {:test, message})
  end
end

But when I run it, I get the follow error.

** (Mix) Could not start application minnow_cms: exited in: MinnowCMS.Application.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (ArgumentError) The module MinnowCMS.Cache was given as a child to a supervisor
but it does not implement child_spec/1.

If you own the given module, please define a child_spec/1 function
that receives an argument and returns a child specification as a map.
For example:

    def child_spec(opts) do
      %{
        id: __MODULE__,
        start: {__MODULE__, :start_link, [opts]},
        type: :worker,
        restart: :permanent,
        shutdown: 500
      }
    end
Note that "use Agent", "use GenServer" and so on automatically define
this function for you.

Client dosen’t have use GenServer, so child_spec(opts) could not be generated.
At first I add a use GenServer to the Client. But when I run, I got this warning.

We will inject a default implementation for now:

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

You can copy the implementation above or define your own that converts the arguments given to GenServer.start_link/3 to the server state.

This time, there is no init/1 callback defined, mix generate it for me, but didn’t use the init/1 defined in Server.:rofl:

Then I remove the use GenServer, I tried add a child_spec to my Client. Everything was fine.
But use GenServer defined in Server will automatically define a child_spec/1 for me, When I define it my self there were two child_spec defined. Is that okay?

At last I move the start_link/1 to server, and leave the test/1 in Client module.It worked for me.

But, I think neither add child_spec myself nor move start_link to Server was the right answer.

How do you split the GenServer?

Personally I’m in favor of keeping client and server in the same module.

But if you want to keep them separated, the easiest thing is probably to defdelegate child_spec(opts), to: Server

1 Like

Thanks for your answer.:smiley:
I tried to add delegate, but I got this error.

23:30:20.117 [info]  Application  exited: Application.start(:normal, []) returned an error: shutdown: failed to start child: Server
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function Server.start_link/1 is undefined or private
            (minnow_cms) Server.start_link([])
            (stdlib) supervisor.erl:379: :supervisor.do_start_child_i/3
            (stdlib) supervisor.erl:365: :supervisor.do_start_child/2
            (stdlib) supervisor.erl:349: anonymous fn/3 in :supervisor.start_children/2
            (stdlib) supervisor.erl:1157: :supervisor.children_map/4
            (stdlib) supervisor.erl:315: :supervisor.init_children/2
            (stdlib) gen_server.erl:374: :gen_server.init_it/2
            (stdlib) gen_server.erl:342: :gen_server.init_it/6

I think the MOUDLE in child_spec was Server, so the supervisor try to find the start_link/1 from the Server module. But the start_link/1 was defined in client.

# file: gs_demo/lib/server.ex
defmodule Server do
  use GenServer

  # behaviour callbacks for behaviour module

  @impl true
  def init(init_arg) do
    state = init_arg
    {:ok, state}
  end

  @impl true
  def handle_cast({:test, args}, state) do
    IO.puts("Hello, #{inspect(args)}")
    {:noreply, state}
  end

  # API for the **supervisor**

  # Server is the child to the supervisor (not Client)
  def start_link(opts) do
    init_arg = []

    opts = [{:name, :cache} | opts]
    GenServer.start_link(__MODULE__, init_arg, opts)
  end
end
# file: gs_demo/lib/client.ex
defmodule Client do
  def test(message),
    do: GenServer.cast(:cache, {:test, message})
end
# file: gs_demo/lib/gs_demo/application.ex
defmodule GsDemo.Application do
  use Application

  def start(_type, _args) do
    opts = []
    # {ServerModule, start_link_arg}
    children = [
      {Server, opts}
    ]

    opts = [strategy: :one_for_one, name: GsDemo.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
$ iex -S mix
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Client.test("there")
Hello, "there"
:ok
iex(2)> Server.__info__(:functions)
[
  child_spec: 1,
  code_change: 3,
  handle_call: 3,
  handle_cast: 2,
  handle_info: 2,
  init: 1,
  start_link: 1,
  terminate: 2
]
iex(3)> opts = []
[]
iex(4)> Server.child_spec(opts)
%{id: Server, start: {Server, :start_link, [[]]}}
iex(5)> 

So the {Server, opts} spec tuple is responsible for a Server.child_spec(opts) call to generate the actual child_spec that is passed to the supervisor.

As the result shows the generated child_spec expects the start_link function to be in the Server module.


I think the wording in the guide is a bit on the confusing side. From what I can tell:

  • start_link isn’t part of the behaviour callback.
  • poking around in random Erlang sources exported (i.e. public) functions not belonging to the behaviour callbacks are often categorized as belonging to the service API. The service API consists of functions that are used to manage the server (by the supervisor) or communicate with it (by the clients).
  • somewhere the “Service API” became referenced as the “Client API” - even though the actual client API was only part of the service API.
  • given that the supervisor manages the Server as its child process it makes sense that start_link should be in the server module.

So it’s the {Module, args} spec tuple that is creating the need for the child_spec/1 function in Module.

Without the child_spec/1 function in the Client module you have to supply your own, hand-crafted child specification (Erlang) to get rid of the error:

# file: gs_demo/lib/server.ex
defmodule Server do
  use GenServer

  @impl true
  def init(init_arg) do
    state = init_arg
    {:ok, state}
  end

  @impl true
  def handle_cast({:test, args}, state) do
    IO.puts("Hello, #{inspect(args)}")
    {:noreply, state}
  end
end
# file: gs_demo/lib/client.ex
defmodule Client do
  def start_link(opts) do
    init_arg = []

    opts = [{:name, :cache} | opts]
    GenServer.start_link(Server, init_arg, opts)
  end

  def test(message),
    do: GenServer.cast(:cache, {:test, message})
end
# file: gs_demo/lib/gs_demo/application.ex
defmodule GsDemo.Application do
  use Application

  def start(_type, _args) do
    opts = []
    # supply an explicit  child_spec instead
    child_spec = %{id: Server, start: {Client, :start_link, [opts]}}

    children = [
      child_spec
    ]

    opts = [strategy: :one_for_one, name: GsDemo.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

# file: gs_demo/lib/gs_demo/application.ex
defmodule GsDemo.Application do
  use Application

  def start(_type, _args) do
    opts = []
    module = Server
    arg = opts

    # child_spec values
    #
    # Note: "start" is an mfargs() tuple
    #   mfargs() = {module(), atom(), [term()]}
    #
    # Think Kernel.apply/3
    # https://hexdocs.pm/elixir/Kernel.html#apply/3
    #
    id = module
    start = {module, :start_link, [arg]}
    restart = :permanent
    shutdown = 5000
    type = :worker
    modules = [module]

    # 1. Old style child specification
    child_spec = {id, start, restart, shutdown, type, modules}

    # 2. Full map style child specification
    # child_spec = %{
    #   id: id,
    #   start: start,
    #   restart: restart,
    #   shutdown: shutdown,
    #   type: type,
    #   modules: modules
    # }

    # 3. Minimal map style child specification
    # child_spec = %{
    #   id: id,
    #   start: start
    # }

    # 4. Newer {module, arg} tuple child specification
    # Note:
    # A. Relies on
    #   module.child_spec(arg)
    # to generate the actual child specification
    #
    # B. Starting function (defaults to "start_link/1") has to
    # accept exactly one single argument
    # - an empty list for no parameters OR
    # - parameters are passed as elements of the list that is
    #   that one single argument
    #
    # child_spec = {module, arg}

    # 5. Newer module-only child specification
    # Note:
    # This is treated as {module, []},
    # i.e. relies on
    #   module.child_spec([])
    # to generate the actual child specification
    #
    # child_spec = module

    children = [
      child_spec
    ]

    sup_opts = [strategy: :one_for_one, name: GsDemo.Supervisor]
    Supervisor.start_link(children, sup_opts)
  end
end

3 Likes

This pattern by Dave Thomas is very useful for working with GenServers.

It aims for separating execution strategy from logic on one side, and separating execution strategy from implementation on other side.

A behavior can be defined, which describes the intention behind the API of the server - and will be injected at test/development time, and runtime, with different implementation. Remember start_link does not describe any intention - and should not be part of the API behaviour. Some function like deposite or withdraw does.

This API will be wired to the GenServer - from outside-in.

On the other side, the GenServer make calls to its implementation - which now can be easily and in a clean way, be unit tested, separately.

And at last, it is possible to write an integration test (or just in your acceptance test) which will test that the all wiring between the API, GenServer and implementation works.

1 Like