GenServer and child_spec

I’m playing with new Supervisor specs, and I have noticed rather unexpected behaviour of GenServer.

When I define simple one for one supervisor taking GenServer module:

children = [
  Defa.Notifier
]
Supervisor.init(children, strategy: :simple_one_for_one)

And then create new child:

Supervisor.start_child(NotifierSup, [%{user_id: 1}])

I expect start_link to be called with one argument, %{user_id: 1}. Meanwhile, it is called with two arguments:

start_link([], %{user_id: 1})

When I define child spec with argument:

children = [
  {Defa.Notifier, ["hello"]}
]

again, arguments to start_link are [“hello”] and %{user_id: 1}.
Using old worker spec works as expected:

children = [
  worker(Defa.Notifier, [])
]

and:

Supervisor.start_child(NotifierSup, [%{user_id: 1}])

calls:

start_link(%{user_id: 1})

Is it bug or a feature?

2 Likes

When you use just a module name as a child spec for a GenServer module Stack then Stack.child_spec([]) is called which results in:

iex(1)> IO.inspect Stack.child_spec([])
%{id: Stack, restart: :permanent, shutdown: 5000,
  start: {Stack, :start_link, [[]]}, type: :worker}

So essentially that empty list becomes your first argument. This doesn’t matter if your not using arguments. You get around this by specifying

use GenServer, start: {__MODULE__, :start_link, []}

in the module implementing the GenServer behaviour.

iex(4)> IO.inspect Stack.child_spec([])   
%{id: Stack, restart: :permanent, shutdown: 5000,
  start: {Stack, :start_link, []}, type: :worker}

No idea if this is intentional behaviour.

2 Likes

OTP behaviour. If Elixir changed the default handling then embedding erlang OTP apps in your elixer supervisors would break.

I assume you are referring to the mfargs typespec:

mfargs() = {M :: module(), F :: atom(), A :: [term()] | undefined}

which requires a list of terms for A.

For me the disconnect is that I would expect:

  • there to be a ModuleName.child_spec/0 that simply populates A in mfargs with what’s specified in the use GenServer options or defaults to a plain empty list.
  • that it would be ModuleName.child_spec/0 (rather than ModuleName.child_spec([])) that is used when one simply uses the module name as a child spec.

As far as I’m aware there isn’t functionality equivalent to ModuleName.child_spec/1 with :gen_server.

Now to me personally it doesn’t really matter - it’s simply a convenience feature, so it will never be a showstopper as I can always just manually crank out

{child_id(), mfargs(), restart(), shutdown(), worker(), modules()}

or the equivalent Map. But from my perspective using ModuleName.child_spec([]) as the default and the consequent result is a bit of a surprise.

The alternative is to require that child_spec/1 always takes a list argument that defaults to an empty list - which basically becomes A.

1 Like