lib/doumi/port/pool.ex

defmodule Doumi.Port.Pool do
  @behaviour NimblePool

  defmacro __using__(opts) do
    quote do
      def child_spec(opts) do
        opts =
          unquote(opts)
          |> Keyword.merge(name: __MODULE__)
          |> Keyword.merge(opts)

        Doumi.Port.Pool.child_spec(opts)
      end

      def command(module, fun, args, opts \\ []) do
        Doumi.Port.Pool.command(__MODULE__, module, fun, args, opts)
      end
    end
  end

  @default_pool_timeout :timer.seconds(5)

  def child_spec(opts) do
    {port_opts, opts} = opts |> Keyword.pop!(:port)

    {port_module, _} =
      port_opts =
      case port_opts do
        {port_module, port_opts} -> {port_module, port_opts}
        port_module -> {port_module, []}
      end

    {restart, opts} = opts |> Keyword.pop(:restart, :permanent)
    {shutdown, opts} = opts |> Keyword.pop(:shutdown, 5_000)

    opts = opts |> Keyword.put(:worker, {__MODULE__, port_opts})
    opts = opts |> Keyword.put_new(:name, port_module)

    %{
      id: port_module,
      start: {NimblePool, :start_link, [opts]},
      shutdown: shutdown,
      restart: restart
    }
  end

  def command(pool_name, module, fun, args, opts \\ [])
      when is_atom(module) and is_atom(fun) and is_list(args) and is_list(opts) do
    {pool_timeout, opts} = opts |> Keyword.pop(:pool_timeout, @default_pool_timeout)

    NimblePool.checkout!(
      pool_name,
      :checkout,
      fn _from, %{port_module: port_module, port: port} ->
        result = port_module.call(port, module, fun, args, opts)

        {result, :ok}
      end,
      pool_timeout
    )
  end

  @impl NimblePool
  def init_worker({port_module, port_opts} = pool_state) do
    {:ok, port} = port_module.start(port_opts)

    {:ok, %{port_module: port_module, port: port}, pool_state}
  end

  @impl NimblePool
  def handle_checkout(:checkout, _from, worker_state, pool_state) do
    {:ok, worker_state, worker_state, pool_state}
  end

  @impl NimblePool
  def handle_checkin(:ok, _from, worker_state, pool_state) do
    {:ok, worker_state, pool_state}
  end

  @impl NimblePool
  def terminate_worker(_reason, %{port_module: port_module, port: port}, pool_state) do
    port_module.stop(port)

    {:ok, pool_state}
  end
end