Using data from a supervised processes to start another

147 Views Asked by At

The documentation details the following:

When the supervisor starts, it traverses all child specifications and then starts each child in the order they are defined. This is done by calling the function defined under the :start key in the child specification and typically defaults to start_link/1.

Consider a Supervisor that has two processes to start, from two specs.

These processes are not named.

When the first process is started, some value is generated:

  @impl GenServer
  def init(_arg) do
    ...
    # Note that the value does not have to be in the state.
    {:ok, some_value}
  end 

How can we pass this value as an argument to the second spec?

{SomeWorker, value_from_first_worker}
6

There are 6 best solutions below

4
On

The first worker should expose some kind of API for the second worker to call.

Besides, since the second worker relies on the first, you may want to set the supervisor's strategy: :rest_for_one.

2
On

I would go with persistent_term.

In the first worker, you set it, in the second one you get it and use it.

3
On

Or, you can use a Supervisor and implement a custom child_spec/1 function in the Worker module, which will allow you to manipulate the child specs that the Supervisor uses to create the Workers.

defmodule Worker do
  use GenServer

  def child_spec({:set_cached_val, value_server, init_arg}) do
    calculated_value = 50
    GenServer.cast(value_server, {:set_val, calculated_value})

    %{
      id: Worker1,
      start: {Worker, :start_link, [init_arg]}
    }
  end
  def child_spec({:get_cached_val, value_server} ) do
    cached_value = GenServer.call(value_server, :get_val)

    %{
      id: Worker2,
      start: {Worker, :start_link, [cached_value]}
    }
  end

  # Client Interface

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end


  # GenServer callbacks
    
  @impl true
  def init(val) do
    {:ok, val}
  end

  @impl true
  def handle_call(:get_val, _from, state) do
    {:reply, state, state}
  end

end
    

defmodule MySupervisor do
  use Supervisor

  #Client Interface

  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  #Callback functions
  
  @impl true
  def init({child1_args, child2_args} ) do
    children = [
      {Worker, child1_args},  # The Supervisor calls Worker.child_spec(child1_args) to get the child_spec for this child
      {Worker, child2_args}  # The Supervisor calls Worker.child_spec(child2_args) to get the child spec for this child
    ]

    Supervisor.init(children, strategy: :one_for_one)

  end
end

defmodule ValueServer do
  use GenServer

  # Client Interface:
  
  def start(val) do
    GenServer.start_link(__MODULE__, val)
  end

  # GenServer callbacks:
  
  @impl true
  def init(val) do
    {:ok, val}
  end

  @impl true
  def handle_call(:get_val, _from, val) do
    {:reply, val, val}
  end

  @impl true
  def handle_cast({:set_val, new_val}, _val) do
    {:noreply, new_val}
  end

end

In iex:

~/elixir_programs% iex a.ex
Erlang/OTP 24 [erts-12.3.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.14.4) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> {:ok, pid} = ValueServer.start(:no_cached_value)                   
{:ok, #PID<0.122.0>}

iex(2)> {:ok, sup} = MySupervisor.start_link({ {:set_cached_val, pid, 10}, {:get_cached_val, pid} })
{:ok, #PID<0.124.0>}

iex(3)> workers = Supervisor.which_children(sup)                                [                   
  {Worker2, #PID<0.126.0>, :worker, [Worker]},
  {Worker1, #PID<0.125.0>, :worker, [Worker]}
]

iex(4)> Enum.map(workers, fn worker -> GenServer.call(elem(worker, 1), :get_val) end)  
'2\n'

iex(5)> IEx.configure(inspect: [charlists: :as_lists])
:ok   
 
iex(6)> Enum.map(workers, fn worker -> GenServer.call(elem(worker, 1), :get_val) end) 
[50, 10]
0
On

A DynamicSupervisor is started with no children. Then you start the children one at a time calling DynamicSupervisor.start_child and passing in a child spec. You can start the first child, and in the first child's init() you can calculate a value and store the calculated value in another process. Then before you call DynamicSupervisor.start_child to start the second child, you can retrieve the calculated value from the other process and add it to the child spec for the second child. Here's an example:

defmodule MySupervisor do

  # Client Interface:

  def start({module, init_arg}, value_server) do

    child = [{DynamicSupervisor, 
      strategy: :one_for_one, 
      name: MyDynamicSupervisor}]

    {:ok, sup} = Supervisor.start_link(child, strategy: :one_for_one)

    {:ok, worker1} = DynamicSupervisor.start_child(
      MyDynamicSupervisor, 
      {module, {init_arg, value_server} } # {init_arg, value_server} will be the
                                          # argument sent to the init() method in `module`
    )

    needed_val = GenServer.call(value_server, :get_val)

    {:ok, worker2} = DynamicSupervisor.start_child(
      MyDynamicSupervisor, 
      {module, {needed_val, :skip} } 
    )

    {sup, worker1, worker2}  

  end

end

defmodule ValueServer do
  use GenServer

  # Client Interface:

  def start(val) do
    GenServer.start_link(__MODULE__, val)
  end

  # GenServer callbacks:
  
  @impl true
  def init(val) do
    {:ok, val}
  end


  @impl true
  def handle_call(:get_val, _from, val) do
    {:reply, val, val}
  end

  @impl true
  def handle_cast({:set_val, new_val}, _val) do
    {:noreply, new_val}
  end

end


defmodule Worker do
  use GenServer

  # Client Interface

  def start_link(val_and_pid) do
    GenServer.start_link(__MODULE__, val_and_pid)
  end


  # GenServer callbacks
    
  @impl true
  def init({val, :skip}) do
    {:ok, val}
  end
  def init({val, pid}) do
    calculated_val = 50
    GenServer.cast(pid, {:set_val, calculated_val})
    {:ok, val}
  end

  @impl true
  def handle_call(:get_val, _from, state) do
    {:reply, state, state}
  end

end

In iex:

~/elixir_programs% iex a.ex
Erlang/OTP 24 [erts-12.3.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
Interactive Elixir (1.14.4) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> {:ok, pid} = ValueServer.start(:none)                                   {:ok, #PID<0.122.0>}

iex(2)> {sup, worker1, worker2} =  MySupervisor.start({Worker, 10}, pid)        {#PID<0.124.0>, #PID<0.126.0>, #PID<0.127.0>}

iex(3)> GenServer.call(worker1, :get_val)
10 

iex(4)> GenServer.call(worker2, :get_val)
50

iex(5)> Process.exit(worker1, :kill)
true

iex(6)> GenServer.call(worker1, :get_val)
** (exit) exited in: GenServer.call(#PID<0.126.0>, :get_val, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir 1.14.4) lib/gen_server.ex:1038: GenServer.call/3

iex(6)> DynamicSupervisor.count_children(MyDynamicSupervisor)
%{active: 2, specs: 2, supervisors: 0, workers: 2}

iex(7)> workers = DynamicSupervisor.which_children(MyDynamicSupervisor)
[
  {:undefined, #PID<0.127.0>, :worker, [Worker]},
  {:undefined, #PID<0.132.0>, :worker, [Worker]}
]  

iex(8)> Enum.map(workers, fn worker -> GenServer.call(elem(worker, 1), :get_val) end)   
'2\n'

iex(9)> IEx.configure(inspect: [charlists: :as_lists])
:ok

iex(10)> Enum.map(workers, fn worker -> GenServer.call(elem(worker, 1), :get_val) end) 
[50, 10]
0
On

ideally workers would not start outside of a Supervisor.init/1 callback

defmodule MySupervisor do
  use Supervisor

  # Client Interface
  
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  # Callback functions:

  @impl true
  def init({child1_arg, child2_arg} ) do
    children = [
      {ValueServer, :no_cached_value},
      %{
        id: Worker1,
        start: {Worker, :start_link, [child1_arg] }
      },
      %{
        id: Worker2, 
        start: {Worker, :start_link, [child2_arg] }
      }  
    ]

    Supervisor.init(children, strategy: :one_for_one)

  end  

end


defmodule ValueServer do
  use GenServer

  # Client Interface:
  
  def start_link(val) do
    GenServer.start_link(__MODULE__, val)
  end

  # GenServer callbacks:
  
  @impl true
  def init(val) do
    Process.register(self(), __MODULE__)
    {:ok, val}
  end

  @impl true
  def handle_call(:get_val, _from, val) do
    {:reply, val, val}
  end

  @impl true
  def handle_cast({:set_val, new_val}, _val) do
    {:noreply, new_val}
  end

end


defmodule Worker do
  use GenServer

  # Client Interface

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end


  # GenServer callbacks
    
  @impl true
  def init({:set_cached_val, init_arg} ) do
    calculated_val = 50
    GenServer.cast(ValueServer, {:set_val, calculated_val})
    {:ok, init_arg}
  end
  def init(:get_cached_val) do
    cached_val = GenServer.call(ValueServer, :get_val)
    {:ok, cached_val}
  end

  @impl true
  def handle_call(:get_val, _from, state) do
    {:reply, state, state}
  end

end

In iex:

~/elixir_programs% iex a.ex
Erlang/OTP 24 [erts-12.3.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
Interactive Elixir (1.14.4) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> {:ok, sup} = MySupervisor.start_link({ {:set_cached_val, 10}, :get_cached_val})
{:ok, #PID<0.122.0>}

iex(2)> children = Supervisor.which_children(sup)
[
  {Worker2, #PID<0.125.0>, :worker, [Worker]},
  {Worker1, #PID<0.124.0>, :worker, [Worker]},
  {ValueServer, #PID<0.123.0>, :worker, [ValueServer]}
]

iex(3)> [worker2, worker1 | _] = children
[
  {Worker2, #PID<0.125.0>, :worker, [Worker]},
  {Worker1, #PID<0.124.0>, :worker, [Worker]},
  {ValueServer, #PID<0.123.0>, :worker, [ValueServer]}
]

iex(4)> GenServer.call(elem(worker1, 1), :get_val)
10  
   
iex(5)> GenServer.call(elem(worker2, 1), :get_val)
50     
0
On

data passed from the first Worker process to the second (on startup), which is what I was aiming for.

defmodule MySupervisor do
  use Supervisor

  # Client Interface
  
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  # Callback functions:

  @impl true
  def init({child1_arg, child2_arg} ) do
    children = [
      %{
        id: Worker1,
        start: {Worker, :start_link, [child1_arg]}
      },
      %{
        id: Worker2, 
        start: {Worker, :start_link, [child2_arg] }
      }  
    ]

    Supervisor.init(children, strategy: :one_for_one)

  end  

end


defmodule Worker do
  use GenServer

  # Client Interface

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end


  # GenServer callbacks
    
  @impl true
  def init(:use_cached_val) do
    {_, cached_val} = GenServer.call(Worker1, :get_val)
    {:ok, cached_val}
  end
  def init(init_arg) do
    Process.register(self(), Worker1)
    calculated_val = 50
    {:ok, {init_arg, calculated_val} }
  end

  @impl true
  def handle_call(:get_val, _from, state) do
    {:reply, state, state}
  end

end

In iex:

~/elixir_programs% iex a.ex
Erlang/OTP 24 [erts-12.3.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.14.4) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> {:ok, sup} = MySupervisor.start_link({10, :use_cached_val})         
{:ok, #PID<0.122.0>}

iex(2)> children = Supervisor.which_children(sup)                           
[
  {Worker2, #PID<0.124.0>, :worker, [Worker]},
  {Worker1, #PID<0.123.0>, :worker, [Worker]}
]

iex(3)> Enum.map(children, fn x -> GenServer.call(elem(x, 1), :get_val) end)
[50, {10, 50}]

For Worker1, its state consists of its own state of 10, and a cached value of 50. If Worker2 should have the same state as Worker1, then you can just store one value for Worker1's state, e.g.:

  def init(init_arg) do
    Process.register(self(), Worker1)
    calculated_val = 50  # Possibly use init_arg to get this result?
    {:ok, calculated_val }
  end

These processes are not named.

Oh, well.