# 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