Testing handle_info requires sleeping?

Hey Alchemists,

I am writing a unit test against a GenServer that verifies that the state is maintained when a monitored client crashes. (This is code snippet from the Little Elixir & OTP Guidebook).

In this snippet, I setup the Pooly GenServer by invoking Pooly.start_pool with some configuration. Next, I create two client processes. Each process checks out a worker and waits for a message. The Server.checkout/0 function monitors all clients who ask for a worker. The test asserts that when the client crashes, the state of the server is maintained. This implies that handle_info has been invoked. This code works as expected via iex. However in a test, the code finishes running before the handle_info message can be received.

I have hacked in a :timer.sleep/1 call to wait for this code to work, but I am wondering if there is a more idiomatic way to get the test to wait. Any thoughts?

Thanks
Steve

test "persists pool state when client crashes" do
    pool_config = [
      mfa: {SampleWorker, :start_link, []},
      size: 2
    ]

    Pooly.start_pool(pool_config)

    pid_1 = spawn(&client/0)
    _pid_2 = spawn(&client/0)

    assert {2, 0} == Pooly.status()

    Process.exit(pid_1, :kill)

    assert {1, 1} == Pooly.status()
  end

  def client do
    Pooly.checkout()
    receive do
      :stop ->
        :ok
    end
  end
# Server module
  def handle_call(:checkout, {from_pid, _ref}, %{workers: workers, monitors: monitors} = state) do
    case workers do
      [worker | rest] ->
        ref = Process.monitor(from_pid)
        true = :ets.insert(monitors, {worker, ref})
        {:reply, worker, %{state | workers: rest }}
      [] ->
        {:reply, :noproc, state}
    end
  end

  def handle_info({:DOWN, ref, _, _, _}, state = %{monitors: monitors, workers: workers}) do
    case :ets.match(monitors, {:"$1", ref}) do
      [[pid]] ->
        true = :ets.delete(monitors, pid)
        new_state = %{state | workers: [pid | workers]}
        {:noreply, new_state}
      [[]] ->
        {:noreply, state}
    end
  end
2 Likes

Normally you can handle this by doing _ = :sys.get_state(pid) and then carrying on. The reason this works is that :sys.get_state is a GenServer.call, and so that function blocks until all other messages from your test process have been handled, including the one you sent to the process.

In this case however that probably won’t work since you’re waiting on the genserver to get a DOWN message from some third process. Maybe also monitoring it and checking the genserver after you get a DOWN would work? Perhaps not, there’s no guarantee about the order that each process gets DOWN.

4 Likes

Imho the best way to sanely test async code is finding a way so that your test process is notified once the action you’re waiting on is finished. E.g. in your case you could look at e.g. subscribing to changes on workers, so that you genserver sends e.g. {:crash, pid} to your test process as part of the handle_info.

As a sidenote: I’m wondering about the monitors ets table. Why not keep refs in the genserver’s state?

Hey Ben,

That seems to have done the trick.Thank you for the response!

Steve

Hey LostKobrakai,

I am in a tough place with this code. Since this is an example from a book, I would make my life worse by attempting to restructure its design. But I will look into your recommendation for production code.

aside: this snippet is from a section of the book that is showing how to use :ets.

Thanks
Steve

In app.html.eex I have:

  <body class="has-navbar-fixed-top">
    <%= render @view_module, @view_template, assigns %>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>

Then in each live view I have in the mount function:

      socket
      |> assign(:layout, {MyAppWeb.LayoutView, "body.html"})

AND in def render of my live view I have:

Phoenix.View.render(SomeRegularView, "show.html", assigns)

instead of the usual SomeRegularView.render("show.html", assigns").

Finally inside body.html.leex I have:

<%= render CommonWeb.NavView, "nav.html", assigns %>

<%= for key <- [:info, :error], msg = Phoenix.LiveView.View.get_flash(@socket)[key] do %>
  <div class="notification is-<%= if key == :info, do: "dark", else: "danger" %>">
    <button class="delete"></button>
    <%= {:safe, msg} %>
  </div>
<% end %>
<%= render @view_module, @view_template, assigns %>

Everything in body.html.leex runs off of socket assigns not conn assigns, so the only thing that uses conn assigns is static assets and stuff.

1 Like