Trouble Grasping Supervisor Trees

I’m really not sure where the mental block is but I cannot work out Supervisors. I’ve revisited the guides on elixir-lang.org but I can’t wrap my head about it.

Trying to use Ranch 1.8 to add TCP & TLS listeners to a Phoenix project, my progress has been:

  1. Adding a protocol handler:
defmodule MyApp.TCPServer do
  alias MyApp.Message
  @behaviour :ranch_protocol
  
  def start_link(ref, socket, transport, mod: mod) do
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport, mod])
    {:ok, pid}
  end

  def init(ref, _, transport, mod) do
    {:ok, socket} = :ranch.handshake(ref)
    :ok = transport.setopts(socket, [{:active, true}])

    :gen_server.enter_loop(__MODULE__, [], %{
      ref: ref,
      socket: socket,
      transport: transport,
      mod: mod
    })
  end

  def handle_info({:tcp, socket, data}, %{socket: socket, transport: transport, mod: mod} = state) do
    {:ok, %{} = msg} = Message.decode(data)

    case mod.handle_message(msg, state) do
      {:ok, reply, new_state} ->
        {:ok, bin} = Message.encode(reply)
        transport.send(socket, bin)
        {:noreply, new_state}
      :noreply ->
        {:noreply, state}
      {:error, e} ->
        transport.close(socket)
        {:stop, :normal, state}
    end
  end

  def handle_info({:tcp_closed, socket}, %{socket: socket, transport: transport} = state) do
    transport.close(socket)
    {:stop, :normal, state}
  end

  def handle_info({:tcp_error, socket}, %{socket: socket} = state) do
    {:stop, :normal, state}
  end
end
  1. A TCP Listener:
defmodule MyApp.TCPListener do
  def start_link(), do: :ranch.start_listener(make_ref(), :ranch_tcp, [port: 4040], MyApp.TCPServer, [])
    
  def start(_, _) do
    {:ok, _} = :ranch.start_listener(MyApp.TCP, 5, :ranch_tcp, [port: 4040], MyApp.TCPServer, [])
  end
end
  1. And a Supervisor for the Ranch listener:
defmodule MyApp.SocketSupervisor do
  use Supervisor

  def start_link(_args) do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init([]) do
    children = [
      worker(MyApp.TCPListener, [])
    ]

    opts = [strategy: :one_for_one, max_restarts: 3]
    supervise(children, opts)
  end
end
  1. Include MyApp.SocketSupervisor in the main Supervisor defined by Phoenix generators:
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    [
      MyApp.Repo,
      MyAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Endpoint,
      MyApp.SocketSupervisor # <- This line.
    ]
    |> Supervisor.start_link(opts)
  end

  @impl true
  def config_change(changed, _new, removed) do
    MyAppWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

Originally I had MyApp.TCPListener.start_link/1 defined following examples instead of start_link/0 & start/2 but that complained quite a bit. But even after correcting that I’m getting this error on start:

** (Mix) Could not start application my_app: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: MyApp.SocketSupervisor
    ** (EXIT) shutdown: failed to start child: MyApp.TCPListener
        ** (EXIT) {{:shutdown, {:failed_to_start_child, :ranch_acceptors_sup, {:badarg, [{:lists, :keymember, [:backlog, 1, nil], [error_info: %{module: :erl_stdlib_errors}]}, {:ranch, :set_option_default, 3, [file: '/tmp/my_app/deps/ranch/src/ranch.erl', line: 468]}, {:ranch_tcp, :listen, 1, [file: '/tmp/my_app/deps/ranch/src/ranch_tcp.erl', line: 84]}, {:ranch_acceptors_sup, :init, 1, [file: '/tmp/my_app/deps/ranch/src/ranch_acceptors_sup.erl', line: 39]}, {:supervisor, :init, 1, [file: 'supervisor.erl', line: 330]}, {:gen_server, :init_it, 2, [file: 'gen_server.erl', line: 423]}, {:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 390]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]}}}, {:child, :undefined, {:ranch_listener_sup, #Reference<0.3723464176.968359939.121336>}, {:ranch_listener_sup, :start_link, [#Reference<0.3723464176.968359939.121336>, :ranch_tcp, %{socket_opts: nil}, MyApp.TCPServer, []]}, :permanent, false, :infinity, :supervisor, [:ranch_listener_sup]}}

It’s obvious I’ve no idea what I’m doing but would anyone know what it is I’m doing wrong?

Thanks.

Having going through the process of trying to explain this, I think I’ve realised it’s an issue with not using child_specs??

Seems like you’re passing wrong options to the children. I don’t have experience with using Ranch directly, so it’s hard to figure out the solution. But I’d suggest trying out manually starting each “component” (MyApp.TCPListener, etc.) separately in IEx the way the Supervisor starts them and going up the supervision tree.

Have a look at the docs. You’re also using the deprecated worker helper - the docs will help with the upgrade too.

Thanks @stefanchrobot. Stepping back through everything, it’s not Ranch specific. Just pilot error, passing an Application rather than a Supervisor.

For example, if I strip everything back to bare minimum and set up a simple TCP echo server. This works as expected.

defmodule MyApp.MixProject do
  use Mix.Project

  def project() do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.12",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application(), do: [mod: {MyApp.SocketListener, {}}, applications: [:logger, :ranch]]

  defp deps(), do: [{:ranch, "~> 2.0"}]
end

defmodule MyApp.SocketListener do
  use Application
  def start(_type, _args) do
    {:ok, _} = :ranch.start_listener(MyApp.TCP, :ranch_tcp, [port: 5555], MyApp.ProtocolTCP, [])
  end
end

defmodule MyApp.ProtocolTCP do
  use GenServer
  @behaviour :ranch_protocol

  def start_link(ref, socket, transport) do
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport])
    {:ok, pid}
  end

  def init(ref, transport, _opts) do
    {:ok, socket} = :ranch.handshake(ref)
    :ok = transport.setopts(socket, [{:active, true}])
    :gen_server.enter_loop(__MODULE__, [], %{socket: socket, transport: transport})
  end

  def handle_info({:tcp, socket, data}, state = %{socket: socket, transport: transport}) do
    transport.send(socket, data)
    {:noreply, state}
  end

  def handle_info({:tcp_closed, socket}, state = %{socket: socket, transport: transport}) do
    transport.close(socket)
    {:stop, :normal, state}
  end
end

Create a fresh Phoenix project, and adding MyApp.SocketListener into the Phoenix generated Application:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Telemetry supervisor
      MyAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Endpoint,
      MyApp.SocketListener # <-- This line
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @impl true
  def config_change(changed, _new, removed) do
    MyAppWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

I get the complaint about child_spec/1 being missing like you mentioned. There’s no child_spec because MyApp.SocketListener is using Application rather than Agent or GenServer.

** (Mix) Could not start application my_app: exited in: MyApp.Application.start(:normal, [])
  ** (EXIT) an exception was raised:
      ** (ArgumentError) The module MyApp.SocketListener was given as a child to a supervisor
but it does not implement child_spec/1.

If you own the given module, please define a child_spec/1 function
that receives an argument and returns a child specification as a map.
For example:

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      type: :worker,
      restart: :permanent,
      shutdown: 500
    }
  end

Note that "use Agent", "use GenServer" and so on automatically define
this function for you.

However, if you don't own the given module and it doesn't implement
child_spec/1, instead of passing the module name directly as a supervisor
child, you will have to pass a child specification as a map:

  %{
    id: MyApp.SocketListener,
    start: {MyApp.SocketListener, :start_link, [arg1, arg2]}
  }

See the Supervisor documentation for more information.

          (elixir 1.12.2) lib/supervisor.ex:631: Supervisor.init_child/1
          (elixir 1.12.2) lib/enum.ex:1582: Enum."-map/2-lists^map/1-0-"/2
          (elixir 1.12.2) lib/enum.ex:1582: Enum."-map/2-lists^map/1-0-"/2
          (elixir 1.12.2) lib/supervisor.ex:617: Supervisor.init/2
          (elixir 1.12.2) lib/supervisor.ex:556: Supervisor.start_link/2
          (kernel 8.0.1) application_master.erl:293: :application_master.start_it_old/4

So my question becomes, what does MyApp.SocketListener look like to allow it to be passed to MyApp.Application?

1 Like

I don’t believe this is the correct approach but to answer my own question.

Remove MyApp.SocketListener and change MyApp.Application.start to:

  def start(_type, _args) do
    children = [
      MyAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Endpoint,
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]

    {:ok, _} = :ranch.start_listener(MyApp.TCP, :ranch_tcp, [port: 5555], MyApp.ProtocolTCP, [])

    Supervisor.start_link(children, opts)
  end

You should make MyApp.SocketListener a Supervisor.

1 Like