Worker arguments for elixir 1.5

Hi,

I was playing around with the new elixir version and i noticed that if my worker doesnt have any arguments

def start_link do ...

it does not like it and seems like it wants start_link/1 even if the application code doesnt call any arguments.

childen = [
  App.WorkerName
]

just wanted to double check if I missed something. it seems odd to put in def start_link(opts \ []) for each function that doesnt use the argument.

thanks!

1 Like

The GenServers start option defaults to __MODULE__.start_link/1, perhaps you can change it using use GenServer, start: {__MODULE__, :start_link, []}.

But I’m not hundred percent sure about this, I prefered the old way to specify workers and will use it as long as possible.

I think it is cluttered and not explained well, but I’m not sure how to do better.

3 Likes

Just define it as def start_link(_opts). Think of it as a contract: sometimes you may use the argument in the contract, sometimes you won’t, but it is always there. It may be a bit awkward when you don’t have anything to pass but you get the benefit of a uniform API.

You are probably aware but the old way will be deprecated eventually. It would be great if we tried our best to get used with the new way so we can help everyone migrate when the time comes.

3 Likes

In my opinion it would be best to simply drop all those implicit magic and pass a list of maps, just as Erlang does…

I still don’t understand why I’m forced to read a workers source code to understand why a supervisor behaves as it does.

4 Likes

I also had problems with the new child_spec system. My main issue is that my GenServer start_link functions almost always take two arguments, one for the actual named arguments, and the second one for gen_server options. This gives callers the flexibility to pass debug flags or register the process the way they want. It took me quite some fumbling (and eventually checking the GenServer source) to understand how to get that working; the doc in Supervisor really assumes that you’re going to define child_spec via “use GenServer” and not roll your own. In the end I got it working, but I felt child_spec added magic indirection to my code for no real benefit.

Creating the map directly in the supervisor (without child_spec) still works fine though. I eventually settled on that.

1 Like

It depends if you see the logic as part of the supervisor or part of the child. Most of the times, I consider it to be part of the child. For example, if you are implementing the terminate callback, you want to revisit the shutdown value. You are more likely to do so if the definition is in the same file.

I also always mention how Ecto eventually changed the repository from worker to supervisor and a bunch of applications suddenly had wrong supervision trees, because up to that point they were taught to use worker(MyApp.Repo). Type, shutdown, restart are mostly intrinsic to the child.

Those are all strong signs that the child specification, or at least its defaults, belongs to the child and not the supervisor. Of course there are cases where a child is used in different ways under the same or different trees, then you can override those in the supervisor.

If you don’t like the implicit aspect, you can still define child_spec/1. I don’t think there is anything magic about it though. Forcing everyone to define maps certainly won’t solve those problems.

I would pass any debug option under a key:

{MyApp, extra: [:debug, :log, ...]}

and then in your start_link/1:

def start_link(opts) do
  {extra, opts} = Keyword.pop(opts, :extra, [])
  GenServer.start_link(__MODULE__, opts, extra)
end

One of the most confusing aspects of the worker/supervisor API was that the worker/supervisor arguments became a call to start_link/N but then GenServer.start_link would always call init/1. Now it is a single argument all the way and you can rely on keyword options, maps or tuples to pass data through.

3 Likes

Is this documented? All I was able to find was, that I can controll how it is created via some options to use GenServer, not even telling what kind of options(?) is expected as default, nor if it is as catchall… Nothing states it were overidable.

How are these optiones meant to be used from the supervisor? What is used as option to child_spec/1 when I use MyGenServer instead of {MyGenServer, [:hello]}?

1 Like

https://hexdocs.pm/elixir/master/Supervisor.html#module-child_spec-1

If you start from the beginnig, the Supervisor docs should cover the whole process from a list of maps up to child_spec/1. Let me know if you any questions left after reading it.

1 Like

I read your link, and I think it is a pity, that this feature is not documented in 1.5.x as good as it is on master. Still, some question remain open:

  1. Where can I learn that I am allowed to override child_spec/1 in a GenServer?
  2. Which arguments to child_spec/1 genereted by use GenServer are accepted by default?

The link to the Supervisor documentation does not answer a single of the questions I asked before.

1 Like

Well, the only reason it is better on master is because people tried it out, gave us feedback and sent PRs. :slight_smile: This goes back to my earlier point on why it is important for us to get used with the new way before the old way is deprecated.

Thanks. I have added those to master now.

4 Likes

And I need to learn to look on masters documentation more closely even when using stable only. Thank you for your help understanding the new concepts.

2 Likes

Hello,

I think this is related: I had a hard time using in combination start_child/2 to spawn children dynamically and start_supervised/2 for testing them, using the same child_spec for both.

In a supervisor if I start children (from let say a Game module) like this:

def start_game(player1, player2) do
  Supervisor.start_child(__MODULE__, [{player1, player2}])
end
def init(:ok) do
  Supervisor.init([Game], strategy: :simple_one_for_one)
end

I get this error (truncated) when I start_game:

** (MatchError) no match of right hand side value: {:error, {:EXIT, {:undef, [{Game, :start_link, [[], {"player1", "player2"}], []},...

From what I understand, the {"player1", "player2"} args don’t override the default [] arg but append to it, so I am trying to spawn a child with an unexisting Game.start_link/2 function passing to it the args [], {"player1", "player2"}.

The documentation is clear: “The child process will then be started by appending the given list to the existing function arguments in the child specification” except for the fact that I did not ask for a default [] arg when I wrote Supervisor.init([Game], strategy: :simple_one_for_one).

At this point I am thinking that it would be nice if:

  • Supervisor.init([Game, :hello], strategy: :simple_one_for_one) would rely on Game.child_spec/1 passing it the default :hello arg (this is the actual behavior)
  • but also if Supervisor.init([Game], strategy: :simple_one_for_one) would rely on a new Game.child_spec/0 that defines a child_spec with no default arg.

Since this second feature does not exist (maybe it’s a bad idea anyway), the solution I came up with is to override the child_spec so that there is no default “arguments in the child specification” and then Game.start_link/1 will be called with {player1, player2}.

defmodule Game do
  use GenServer, start: {Game, :start_link, []}, restart: :transient
  
  def start_link(args) do
    GenServer.start_link(__MODULE__, args)
  end
  ...

And indeed it works!

But in my tests:

start_supervised {Game, {player1, player2}}

Is now failing:

** (MatchError) no match of right hand side value: {:error, {{:EXIT, {:undef, [{Game, :start_link, [], []}

If I remove the child_spec :start option in the Game module the tests will run, but I am back with the first error.

Finally, I have this solution:

  • don’t specify a :start key as in use GenServer, start: {Game, :start_link, []}, restart: :transient so that start_supervised works
  • but define it in a custom spec within the supervisor:
def init(:ok) do
  game_spec = %{id: Game, restart: :transient, shutdown: 5000, start: {Game, :start_link, []}, type: :worker}
  Supervisor.init([game_spec], strategy: :simple_one_for_one)
end

I wanted to share this because it was hard to find a solution, and hard remembering why it is needed (maybe I am wrong somewhere).

Adding to it the fact that:

  • the Phoenix app generator does not use Elixir 1.5 style of supervisor children declaration
  • DynamicSupervisors are coming in master
  • start_supervised {Game, {player1, player2}} does not require args to be in a list when Supervisor.start_child(__MODULE__, [{player1, player2}]) does

…my brains fry a little :slight_smile:

2 Likes

I am glad you were able to figure out and apologies for the confusion. To confirm: you are correct in everything you said.

The DynamicSupervisor is the answer to your problems as it integrates with the new child specifications. New Phoenix versions will migrate to the new child specs as well once Elixir v1.6 is out, so hopefully it will all be ironed out really soon.

7 Likes