Reasons behind changes on starting modules after v1.5?

After version 1.5 Elixir changed mechanism of starting children under a supervisor.
Personally I preferred the previous way using:

children = [
worker(MyApp.MyModule, [])
]

This worked just fine and I actually understood it.
In the newest version, placing:

children = [
MyApp.MyModule
]

and then defining a child.spec function directly in the module like:

def child_spec(opts) do
  %{
    id: __MODULE__,
    start: {__MODULE__, :start_link, [opts]},
    type: :worker,
    restart: :permanent,
    shutdown: 500
  }
end

or, as the documentation mentions no need to do that if the module imports:

use GenServer

Can someone shed some light on this unintuitive process, is it a bug?

1 Like

Can you try to clarify which part is non-intuitive? In those scenarios I usually ask myself: would I dislike the new approach if I had learned it first? It helps you remove any bias and put what is non-intuitive into words.

If I had to guess, you don’t like that the definition is now a bit more hidden away from you. Although between worker(Foo) and Foo, the only information being hidden is the type of worker.

The release announcement covers the rationale behind this change. In a nutshell, the way the previous syntax passed arguments was confusing to most. Especially when it came to options.

Folks would write:

worker(Foo, [option1: :foo, option2: :bar])

Which would actually be called as:

Foo.start_link({:option1, :foo}, {:option2, :bar})

The new syntax always uses Foo.start_link/1 and completely removes the argument dance.

Finally, the fact the user of the module had to pass the supervision specification led to misuses. For example, earlier versions of Phoenix used worker(Ecto.Repo) instead of supervisor(Ecto.Repo). I have seen folks using tasks but forgetting to declare them as temporary, etc. In most cases, I would say it makes the most sense for the service to declare how it is supervised and not otherwise.

EDIT: I have changed the supervisor docs to explain first the child specification with maps and then I explain the shortcut with the reason why it matters: https://github.com/elixir-lang/elixir/blob/eddf6b372d873802debd22e63da4be785b49f83a/lib/elixir/lib/supervisor.ex#L2

11 Likes

What is the best way of handling the case where you want to start the child with different options?

2 Likes

I believe the new syntax is {MyApp.MyModule, different_opts}

1 Like

Doesn’t this just mean that in one sense you are splitting the startup options?

Do you mean we are splitting arguments like url: "ecto://..." as in

children = [
  {MyApp.Repo, url: "ecto://localhost:4567/my_dev"},
  MyApp.Endpoint
]

and options like restart: :transient?

If you look at the six fields in a child only two are really only dependent on the child itself and they are whether it is a worker or supervisor and its dependent modules wrt code upgrading. All the others are really dependent on the supervisor which manages the child. So having them in a child callback seems a bit misplaced, that’s all.

1 Like

You can still specify them in the supervisor if you want to. You can update any child_spec with the child_spec/2 function:

Supervisor.child_spec SomeModule, restart: :temporary

Since child specs are maps, you can also use the functions in the map modules, but the child_spec/2 makes sure you are using the proper keys and setting the proper values.

1 Like

Yes, of course, but it just seems out of place to specify things in the child code which is supervisor specific. It gives the impression that there is only one way to use this child.

2 Likes

Many of child specification options tie directly with how the process is implemented. If you are using {:stop, :shutdown, state} for clean shutdown of your GenServer, you likely do not want your supervisor to restart it on shutdown signals. If you need to do something that may take more than 5 seconds on terminate, the supervisor shutdown time needs to reflect that.

That’s why many libraries including poolboy and a couple modules in OTP expose their own child_spec function that returns the child specification. The child specification they return is not set in stone but it should be considered as the default way to run those processes unless you know what you are doing.

Luckily the impression there is only one way to use those children is easily solvable and we do mention such mechanisms in the Supervisor docs.