Is it possible for start_supervised to work when start_link has no parameters?

Background

I have a small server that takes requests and processes some information. To start this GenServer, I don’t need anything at all. I just need to invoke start_link:

defmodule AuctionHouse.Runtime.Server do
  use GenServer

  @spec start_link :: :ignore | {:error, any} | {:ok, pid}
  def start_link, do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)

  @impl GenServer
  @spec init(nil) :: {:ok, map, {:continue, :setup_queue}}
  def init(nil) do
    
    {:ok, %{hello: "world"}}
  end
end

Problem

This sample server works fine, if I manually invoke it via iex -S mix. However, if in my tests, I need to start it, make a few calls, and then kill it before the next test.

This is where start_supervised comes in. I have a a test with the following:

defmodule AuctionHouseTest do
  @moduledoc false

  use ExUnit.Case
  
  setup do
    {:ok, pid} = start_supervised(AuctionHouse.Runtime.Server)
    %{server: pid}
  end
end

Unfortunately, when I run the test file, it immediately blows up, as it fails to launch the server:

** (MatchError) no match of right hand side value: {:error, {{:EXIT, {:undef, [{AuctionHouse.Runtime.Server, :start_link, [nil], }, {:supervisor, :do_start_child_i, 3, [file: ‘supervisor.erl’, line: 414]}, {:supervisor, :do_start_child, 2, [file: ‘supervisor.erl’, line: 400]}, {:supervisor, :handle_start_child, 2, [file: ‘supervisor.erl’, line: 706]}, {:supervisor, :handle_call, 3, [file: ‘supervisor.erl’, line: 455]}, {:gen_server, :try_handle_call, 4, [file: ‘gen_server.erl’, line: 1149]}, {:gen_server, :handle_msg, 6, [file: ‘gen_server.erl’, line: 1178]}, {:proc_lib, :init_p_do_apply, 3, [file: ‘proc_lib.erl’, line: 240]}]}}, {:child, :undefined, AuctionHouse.Runtime.Server, {AuctionHouse.Runtime.Server, :start_link, [nil]}, :permanent, false, 5000, :worker, [AuctionHouse.Runtime.Server]}}}

This is rather confusing to me. So I went back and changed the API of the server to the following:

@spec start_link(any) :: :ignore | {:error, any} | {:ok, pid}
  def start_link(_i_dont_care), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)

which resulted in start_supervised working.

Questions

I checked the documentation, but I was unable to find anything stating that start_link must take a parameter, no matter what.

I also would like to avoid having to change the public API of my server, just because of a technicality.

  • Have I missed something in the docs?
  • Is it possible to keep my original API for the server, and have start_supservised work?

Yes it can work, but you need to specify your own child_spec in that case:

  def child_spec(_) do
    %{id: __MODULE__, start: {__MODULE__, :start_link, []}}
  end
1 Like

This explains why and how this works.

2 Likes