elixiractorerlang-supervisoractor-model

Add/remove child from supervisor children list at runtime in Elixir


I know how to create a supervisor with an initial list of children.

defmodule TweetProcesser.DummySupervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    children = [
      Supervisor.child_spec({TweetProcesser.Worker, []}, id: :my_worker_1),
      Supervisor.child_spec({TweetProcesser.Worker, []}, id: :my_worker_2),
      Supervisor.child_spec({TweetProcesser.Worker, []}, id: :my_worker_3),
      Supervisor.child_spec({TweetProcesser.Worker, []}, id: :my_worker_4),
      Supervisor.child_spec({TweetProcesser.Worker, []}, id: :my_worker_5)
    ]

    opts = [strategy: :one_for_one, name: TweetProcesser.WorkerSupervisor]

    Supervisor.init(children, opts)
  end
end

But how can I make the functionality for adding new children or removing children from this list at runtime? In such way that other actors could call these functions while running in order to add or remove children.


Solution

  • A possible approach would be to use a DynamicSupervisor, followed by a one-off worker that gives the supervisor it's initial children.

    Assuming this is the bare skeleton code for you processes

    defmodule TweetProcesser.Worker do
      use GenServer
    
      def start_link() do
        GenServer.start_link(__MODULE__, [], [])
      end
    
      def init(_) do
        {:ok, nil}
      end
    end
    
    defmodule StartInitialChildren do
      use Task
    
      def start_link([]) do
        Task.start_link(fn ->
          [:my_worker_1, :my_worker_2, :my_worker_3, :my_worker_4, :my_worker_5]
          |> Enum.each(fn id ->
            spec = %{id: id, start: {TweetProcesser.Worker, :start_link, []}}
            {:ok, _pid} = DynamicSupervisor.start_child(TweetProcesser.DummySupervisor, spec)
          end)
        end)
      end
    end
    
    

    You can then add this to your application children

    defmodule MyApp.Application do
      # ...
      def start(_, _) do
        def children = [
          # ...
          {DynamicSupervisor, name: TweetProcesser.DummySupervisor, strategy: :one_for_one},
          StartInitialChildren
          # ...
        ]
      end
      #...
    end
    

    What happens is, your dynamic supervisor will start with no children.

    Then the one off task will run, and start the 5 children you want it to start with.

    After that, you can use the same line of code to start more children.

    spec = %{id: some_id, start: {TweetProcesser.Worker, :start_link, []}}
    DynamicSupervisor.start_child(TweetProcesser.DummySupervisor, spec)
    

    Of course, you can make it nicer by defining helper functions, but to get the base functionality going, this should do the trick.

    Also note that, as others here have said, you should probably avoid atoms as ids when you're starting processes dynamically, as they don't get cleaned up fully and there's an overall limit on the system.