1
votes

I have two kind of processes to be started by a supervisor: some workers and a controller to which the workers need to report. For this they need to know the pid of the controller.

I was thinking about two ways to do this but none works.

Starting all at once

The principle is to start all the processes at once, then to retrieve the pid of the controller and then to send it to all the workers so that they can start to work. The code would look like this:

defmodule XYZ.MySup do
  use Supervisor

  def start_link(opts) do
  ...
  end

  def init(:ok) do
    children = [
      {XYZ.Controller, name: :ctrl},

      %{id: XYZ.Worker_1,
        start: {XYZ.Worker,
                :start_link, [[name: :w1, args: {"arg_1"}]]}},

      ... some other workers ...
    ]

    Supervisor.init(children, strategy: :one_for_one)

    ch = Supervisor.which_children(self())
    ...

  end
end

The line Supervisor.which_children(self()) generates the error ** (EXIT) process attempted to call itself.

Starting the controller first

The principle is to start the controller, then to get its pid, then start the workers with the pid of the controller as a parameter (That would be the preferred method, btw).

  1. of course, getting the pid of the controller gets into the same issue: the runtime refuses to call Supervisor.which_children(self)).
  2. even if this would be solved, adding a worker to the supervisor with:

    Supervisor.start_child(self(),
      %{id: XYZ.Worker_3,
        start: {XYZ.Worker,
                :start_link, [[name: :w3, args: {"arg_3"}]]}})
    

    fails with the error ** (EXIT) process attempted to call itself.

What am I doing wrong ?

1

1 Answers

2
votes

Ok so you cannot send a message to the supervisor from the supervisor while it is already handling a message. In this case it is the start_link/init message. Like everything in Elixir/Erlang, everything is a process and can only handle one message at a time. Essentially by executing this in the init call, you're creating a lock. This is because the supervisor will not start accepting messages until the init function successfully exits and the supervisor is part of the supervision tree. This is tantamount to calling a cast/call to a GenServer inside of its own handle_cast/handle_call callback.

It also looks like you want to use a :simple_one_for_one strategy to allow you to dynamically start workers.

Next, you should name your supervisor so you can call it without having to get the pid.

Lastly, you should just wrap your startup functionality in a task inside of your Application definition.

Personally, I like to do something like this:

defmodule MyApp.Supervisor do
  use Supervisor

  def start_link do
    # Name your supervisor `MyApp.Supervisor`
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init([]) do
    children = [
      worker(MyApp.Worker, [])
    ]

    supervise(children, strategy: :simple_one_for_one)
  end
end

Then use the task in your Application definition:

defmodule MyApp.Application do
  use Application

  def start(_, _) do
    import Supervisor.Spec, warn: false

    children = [
      supervisor(MyApp.WorkerSupervisor, []),
      worker(Task, [&on_boot/0], restart: :temporary)
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end

  defp on_boot do
    child_spec = MyApp.Worker.child_spec(args: [_arg1, _arg2, _arg3], name: :w1)
    Supervisor.start_child(MyApp.WorkerSupervisor, child_spec)
    # ... more workers
    :ok
  end
end

I'm not entirely sure about the child_spec call from above so you might want to check the docs for that: https://hexdocs.pm/elixir/Supervisor.html

Then you can get the children of the supervisor by calling Supervisor.which_chldren(MyApp.WorkerSupervisor) anywhere else in your application.

If you want to see the resulting supervision tree, using :observer.start() inside of your iex console and clicking on the Applications tab is always helpful.