Idiom for naming workers in a post-child_spec world

In the days of using Supervisor.Spec to start workers, I could register a process, by name, rather easily:

worker(SolutionDesigner.Repo, [], name: :foo)

In the new idiom of child_spec the solution I’ve found involves providing an inline child_spec something like this:

%{
      id: SolutionDesigner.Repo,
      start: {SolutionDesigner.Repo, :start_link, [[], name: :foo]}
}

Which, in my opinion, is a long row to hoe just to give the process a name. For applications where I control the worker being started I could see moving the naming down into the worker itself - so the worker is in charge of its own name. However, when using workers provided by third-party libraries:

%{
    id: Redix,
    start: {Redix, :start_link, [[hostname: "localhost", port: 1234], name: :redis]}
}

I find this to be rather inconvenient. Is there a more terse, idiomatic way to name a worker from the start up function for a supervisor.

2 Likes

Note that newer releases of both Ecto and Redix provide child_spec function which would be automatically called in both of these cases:

children = [
  SomeRepo
  {Redix, [name: :redis]}
]
1 Like

Well, in this particular case SolutionDesigner.Repo is not an Ecto repo (it’s a custom “event sourcing” store) but I was asking more in the abstract than strictly about Ecto and Redix.

I suppose one strategy is to wait until library designers can update their workers to include child_spec implementations and hope they provide one that accommodates start_link/3 like Redix does.

children = [
  {Redix, [[hostname: "localhost", port: 1234],[name: :redis]]}
]

This does not name a process. The third element is the supervisor options, process name is not one of them. The name has to be passed as argument to start_link which means

worker(SolutionDesigner.Repo, [[name: :foo]], [])

which is the same as

{SolutionDesigner.Repo, name: :foo}

today.

1 Like

One of the additional goals of the child_spec change was to standardise on a start_link/1 function. There’s no support for other arities on purpose.

:thinking:

05%20PM

and Supervisor.Spec.worker/3 is a function.

In functional programming well named values and functions are the best terseness there is. It lets you be as terse as is reasonable at the call site while offering you the opportunity of being as verbose as is necessary at the definition site.

And in this particular case it makes it entirely irrelevant whether the spec (map) is assembled by the supervisor module or by the child module’s child_spec/1 because that detail is hidden inside the function definition.

1 Like

I understand that well. My point was that Supervisor.Spec.worker was a nice terse function which did what I wanted it to do, but it is now deprecated. I can, of course, take upon myself the responsibility for writing my own function that is a drop in replacement for Supervisor.Spec.worker but that does not seem to follow the spirit of the intent that deprecated it in the first place.

start_link/3 seems to separate the initialization options that are important to my worker (the args) from the initialization options that are relevant to the supervisor framework (called opts:name, :timeout, :debug, :spawn_opt).

If I understand your reply (which I may not) the intent is that a worker should provide a child_spec/1 function which accepts a single keyword list that mixes the initialization arguments (the args) for the worker with the framework concerns (the opts) and it is the responsibility of the child_spec/1 function to carefully separates those and indicate that control should be passed on to start_link/3.

At the same time, if an implementation of child_spec/1 did not recognize the name keyword, and treat it properly, then there would be no way for someone, at the point where the child list is passed to the supervisor, to provide that option.

I think there is something fundamental I am misunderstanding.

It was exactly the same before - if the implementation did not provide support for :name option in the start_link function, you can’t name the process. Nothing is really changed in that regard.

Ok I’ve gone back and tracked down a specific example of what I was doing that I can’t seem to do any longer.

I’ll use Agent as a worker just for the sake of illustration.

In the past I would use a child array like this:

children = [
  worker(Agent, [fn -> %{} end, [name: :fred]])
]

(I should not have relied on my memory for my original post and you have my apologies for that).

This, as I understand it, ends up invoking Agent.start_link/3 and the supervisor in question will start up the Agent, with the given function as the args parameter and the keyword array as the options) . There does not appear to be an equivalent way to do this same thing in the brave new world of the various child_spec functions. My first instinct would be that:

children = [
  {Agent, [fn -> %{} end, [name: :fred]]}
]

should behave just as worker did. However that is not the case as it is equivalent to:

children = [
  Agent.child_spec([fn -> %{} end, [name: :fred]])
]

which makes the child_spec

%{
  id: Agent,
  start: {Agent, :start_link, [[#Function<.. details omitted...>, [name: :fred]]]}
}

Which means that Agent.start_link/1 will be invoked with a single parameter that is an array (not what I want).

All that is to say that, there doesn’t appear to be a way to duplicate the behavior of my original array:

children = [
  worker(Agent, [fn -> %{} end, [name: :fred]])
]

Short of either

  • Continuing to use the (deprecated) Supervisor.Spec.worker/2,
  • Using my own complex child spec, e.g. %{id: Agent, start: {Agent, :start_link, [#Function<...>, [name: :fred]]}
  • Or (as @peerreynders points out) writing my own function to generate the child spec

Is that correct?

What is so terrible about?

def agent_spec(args),
  do: %{id: Agent, start: {Agent, :start_link, args}}
...

children = [agent_spec([fn -> %{} end, [name: :fred]])]

or even better:

def agent_spec(intial_state, name),
  do: %{id: Agent, start: {Agent, :start_link, [fn -> initial_state end, [name: name]]}}
...

children = [agent_spec(%{}, :fred)]

Use the opportunity that these custom functions present to elaborate on the “what and why” rather than focusing on the “how”.

1 Like

Absolutely nothing is “so terrible” about it :smiley:. I just wanted to confirm that such measures are necessary to accomplish my goals and that I wasn’t missing something in the new idiom that I did not understand.

2 Likes

You’re right, looking at Agent.child_spec/1 there’s no way to separately pass options (which would end up as last argument to start_link).

So yeah, you won’t be able to do: children = [{Agent, [fn -> %{} end, [name: :fred]]}], however I wonder if it was practical to do so in the first place or was it just an example for the sake of discussion? I’m personally always wrapping Agents with modules so if I wanted to allow name registration I’d likely do that in start_link/1 function on that wrapper module.

In the case of Agent you have no control over the internals of the module and child_spec/1 will prevent you from using Agent.start_link/2. At the same time Agent cannot force it’s Supervisor to use Agent.child_spec/1. So anything is still possible by using the long form spec map.

GenServer.start_link/3 is primarily targeted at use cases where you need to spawn highly generic processes as is described in Replacing GenEvent by a Supervisor + GenServer.

There the Supervisor has a generic worker template of

{GenServer, {GenServer, :start_link, []}, :permanent, 5_000, :worker, [GenServer]}

which is equivalent to

%{
  id: GenServer,
  start: {GenServer, :start_link, []},
  restart: :permanent,
  shutdown: 5_000,
  type: :worker,
  modules: [GenServer]
}

via Supervisor.start_child(sup, [handler, args]) this in effect for this one child turns into

%{
  id: GenServer,
  start: {GenServer, :start_link, [handler,args]},
  restart: :permanent,
  shutdown: 5_000,
  type: :worker,
  modules: [GenServer]
}

i.e GenServer.start_link(handler,args, [])

I imagine that the child_spec/1/start_link/1 standard is primarily targeting your own modules that implement GenServer processes. child_spec/1 gives the module an opportunity to implement a reasonable default spec for itself. The Supervisor isn’t bound to it - it can bypass it entirely or even modify whatever child_spec/1 returns before accepting or using it. Meanwhile for the module’s start_link/1 there really isn’t a need for anything but a single keyword list. For example if the process needs a process name then that name should be part of it’s arguments (list). Expect to use Keyword.pop/3 and friends a lot.

The use of Agent in particular was just an example for the sake of discussion. The discussion seemed to have gotten bogged down a bit by the specific examples I posted earlier.

Fair enough.

I think it’s worth re-iterating Michał’s point:

It was exactly the same before - if the implementation did not provide support for :name option in the start_link function, you can’t name the process. Nothing is really changed in that regard.

which makes sense looking at the code [1] [2], worker function doesn’t seem to have any special capabilities for options (like name registration) so you were able to register process name only because your module’s start_link allowed such. So yeah, use child_spec provided by use Agent/GenServer/etc and adjust start_link for name registration, and wait for library authors to do the same.

[1] elixir/lib/elixir/lib/supervisor/spec.ex at v1.6.4 · elixir-lang/elixir · GitHub
[2] elixir/lib/elixir/lib/supervisor/spec.ex at v1.6.4 · elixir-lang/elixir · GitHub

Sure. It happens to be the case that I (and a least a couple of others to the extent that my experience is any judge) pattered our start_link functions after the start_link/3 function of things like GenServer and Agent :smiley:. In my own case I would just pass through the “opts” and take advantage of :name.

Now I will go update my own classes and work around others as the need arises!

Thank you all for your help!