Hi,
In my efforts to level up my OTP skills, I’m currently working through https://www.manning.com/books/the-little-elixir-and-otp-guidebook
At one point in the book, a pool library gets implemented. The book uses an older Elixir version (e.g. using Supervisor.Spec
functions) and I wanted to try and follow along and update the code to Elixir 1.6 (e.g. using a DynamicSupervisor
).
The book’s code can be found at https://github.com/benjamintanweihao/pooly/tree/version-1/lib/pooly (note that this points to the “version-1” branch), and my conversion attempt is at https://github.com/davidsulc/pooly/tree/master/lib
I’m able to run my code, but if I open Observer and kill either the Pooly.Server
or Pooly.WorkerSupervisor
process, multiple {:error, {:already_started, _}}
errors get triggered seemingly because Pooly.Server
's start_link
function gets called multiple times. I do not understand why that is the case.
How can I fix my code for the supervision tree to work as expected?
Note that at this stage in the book, the project is in an intermediary state (i.e. not the final implementation) so what I’m mainly looking for is the equivalent code to the book’s code linked above, but with Elixir 1.6 features/idioms (I’m also aware that certain options passed in to the supervisors are the same as the default values).
In the interest of future-proofing this thread, I’ll reproduce the relevant code below (as available in the 2nd github link above):
# lib/pooly.ex
defmodule Pooly do
use Application
def start(_type, _args) do
pool_config = [
worker_sup_mod: Pooly.WorkerSupervisor,
worker_spec: Pooly.SampleWorker,
size: 5
]
start_pool(pool_config)
end
defdelegate start_pool(pool_config), to: Pooly.Supervisor, as: :start_link
end
# pooly/supervisor.ex
defmodule Pooly.Supervisor do
use Supervisor
def start_link(pool_config) do
IO.puts("Supervisor start_link")
Supervisor.start_link(__MODULE__, pool_config)
end
def init(pool_config) do
IO.puts("Supervisor init")
children = [
{Pooly.Server, [self(), pool_config]}
]
Supervisor.init(children, strategy: :one_for_all)
end
end
# pooly/server.ex
defmodule Pooly.Server do
use GenServer
@name __MODULE__
@pool_config_keys [:sup, :size, :worker_sup_mod, :worker_spec]
defmodule State do
defstruct [:sup, :size, :worker_sup_mod, :worker_sup, :worker_spec, :monitors, workers: []]
end
def start_link([_sup, _pool_config] = opts) do
IO.puts "in Server start_link"
GenServer.start_link(__MODULE__, opts, name: @name)
end
def init([sup, pool_config]) when is_pid(sup) do
IO.puts "in Server init"
monitors = :ets.new(:monitors, [:private])
state = struct(State, pool_config |> sanitize())
state = %{state | sup: sup, monitors: monitors}
send(self(), :start_worker_supervisor)
{:ok, state}
end
def handle_info(:start_worker_supervisor, %{sup: sup, worker_sup_mod: module, worker_spec: worker_spec, size: size} = state) do
IO.puts "start worker supervisor request"
{:ok, worker_sup} = Supervisor.start_child(sup, {module, [restart: :temporary]})
workers = prepopulate(size, {worker_sup, worker_spec})
{:noreply, %{state | worker_sup: worker_sup, workers: workers}}
end
def handle_info(msg, state) do
IO.puts "Server received: #{msg}"
{:noreply, state}
end
defp sanitize(config) do
config
|> Enum.filter(fn {k, _} -> Enum.member?(@pool_config_keys, k) end)
end
defp prepopulate(size, sup_args), do: prepopulate(size, sup_args, [])
defp prepopulate(size, _sup_args, workers) when size < 1, do: workers
defp prepopulate(size, sup_args, workers) do
prepopulate(size - 1, sup_args, [new_worker(sup_args) | workers])
end
defp new_worker({sup, worker_spec}) do
{:ok, worker} = DynamicSupervisor.start_child(sup, worker_spec)
worker
end
end
# pooly/worker_supervisor.ex
defmodule Pooly.WorkerSupervisor do
use DynamicSupervisor
@name __MODULE__
@opts [
strategy: :one_for_one,
max_restarts: 5,
max_seconds: 5
]
def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: @name)
end
def init(opts) do
DynamicSupervisor.init(@opts ++ opts)
end
end
# pooly/sample_worker.ex
defmodule Pooly.SampleWorker do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, [])
end
def stop(pid) do
GenServer.call(pid, :stop)
end
def init(args) do
{:ok, args}
end
def handle_call(:stop, _from, state) do
{:stop, :normal, :ok, state}
end
end