How to pass arguments to :simple_one_for_one GenServer to initialize state

1.2k views Asked by At

I am trying to set up a supervision tree for a scheduler application (note using Elixir 1.5 syntax). The application should work so that:

  • Application boots with a registry & the scheduler supervisor
  • The SchedulerSupervisor boots, and allows children to be dynamically added via the start_child callback. This takes the initialisation args that are used to build the schedule state.
  • The Scheduler, on initialisation, registers its name to the Registry, and initialises with a struct that holds the state (Scheduler then has functions from manipulating schedules, but that isn't relevant to the issue I'm having).

If I don't pass any arguments, I can get this working - the Schedules created are not registered, and I have to modify the state after creation. As soon as I try to add arguments, the system errors out - I know it's just a syntax misunderstanding on my part, but I cannot for the life of me figure out what I'm doing wrong. I haven't found the docs terribly helpful here, and I've tried copying and modifying examples from GH, GH Gists and from articles online, but I cannot get it to work.

Current setup - ideally I would want to pass id, period and targets as arguments to start_child, but can't even get it working with a single argument, so just sticking with the one until I can get it running:

The Application:

defmodule Assay.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Assay.SchedulerSupervisor, []},
      {Registry, keys: :unique, name: Assay.Scheduler.Registry}
    ]

    opts = [strategy: :one_for_all, name: Assay.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

The Supervisor:

defmodule Assay.SchedulerSupervisor do
    use Supervisor

    @name Assay.SchedulerSupervisor

    def start_link(_args) do
        Supervisor.start_link(__MODULE__, :ok, name: @name)
    end

    def start_schedule(id) do
        Supervisor.start_child(@name, [id])
    end

    def init(_) do
        Supervisor.init([Assay.Scheduler], [strategy: :simple_one_for_one, name: @name])
    end
end

The GenServer (only relevant initialisation functions shown)

defmodule Assay.Scheduler do
    use GenServer
    alias Assay.Scheduler
    require Logger

    defstruct targets: [], period: 60_000, id: nil, active: false

    def start_link(id) do
        GenServer.start_link(__MODULE__, [id], [name: via_tuple(id)])
    end

    def init([id]) do
        Logger.info "starting a new #{__MODULE__} with id #{id}"
        {:ok, %Scheduler{id: id}}
    end
end

edit: actual error might help - I can see that the args are wrong, I just can't figure out why:

{:error,
 {:EXIT,
  {:undef,
   [{Assay.Scheduler, :start_link, [[], 1], []},
    {:supervisor, :do_start_child_i, 3, [file: 'supervisor.erl', line: 381]},
    {:supervisor, :handle_call, 3, [file: 'supervisor.erl', line: 406]},
    {:gen_server, :try_handle_call, 4, [file: 'gen_server.erl', line: 636]},
    {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 665]},
    {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]}}}
1

There are 1 answers

1
Dogbert On BEST ANSWER

For :simple_one_for_one supervisors, Supervisor.start_child calls the start function with the arguments given to it with the arguments specified in the Child Specification. When using Supervisor.init, the child specification is taken from the module's child_spec/1 function in Elixir 1.5. Since you're using GenServer and not specifying a custom start function and [] is passed to child_spec/1, this defaults to [[]] which means your function ends up being called with two arguments, [] and 1 if the id is 1 and you get an undefined function error.

You can fix this by explicitly saying you don't want the GenServer to provide any arguments to the start function in child_spec by changing

use GenServer

to

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

Now the function will be called correctly, with only one argument, which will be the id.

IO.inspect Assay.SchedulerSupervisor.start_link []
IO.inspect Assay.SchedulerSupervisor.start_schedule 12

will print:

{:ok, #PID<0.82.0>}
[info]  starting a new Elixir.Assay.Scheduler with id 12
{:ok, #PID<0.83.0>}