1

In my app I have a GenServer. It backs up data needed to start again in an Agent. I want to test if my GenServer backs up and restores correctly, so I wanted to start backup agent, then restart GenServer and see if it works (remembers config from before restart).

Right now I have GenServer configured and started (with start_supervised!) in test setup. I need to somehow restart that GenServer.

Is there a good way to do it? Should I be doing it completely differently? Is there a different, correct way of testing restart behavior?

VOID404
  • 25
  • 4

1 Answers1

1

A Supervisor decides when to restart a process under its supervision through the child_spec of that child process. By default, when you define your GenServer module, and use use GenServer on the module, the default (restart values) will be :permanent, which means, always restart this process if it exits.

Given this, it should be enough to send it an exit signal, with Process.exit(your_gen_server_pid, :kill) (:kill will ensure that even if the process is trapping exits it will be killed), and the supervisor should then start the process again and you can then do your assertions.

You'll need a way to address the "new" genserver process, since it will be killed, when restarted its pid won't be the same as was originally, usually you do that by providing a name when starting it.

If your genserver loads the state as part of its init you don't necessarily need to supervise it to test the backup behaviour, you could just start it individually, kill it, and then start it again.

There might be edge-cases depending on how you establish the backup, etc, but normally that would be enough.

UPDATE:

To address both the process exiting and being up again, you could write 2 helper functions to deal specifically with that.

def ensure_exited(pid, timeout \\ 1_000) do
  true = Process.alive?(pid)
  ref = Process.monitor(pid)
  Process.exit(pid, :kill)
  receive do
     {:DOWN, ^ref, :process, ^pid, _reason} -> :ok
  after
    timeout -> :timeout
  end
end

You could make it take instead a name and do GenServer.whereis to retrieve the pid, but the idea is the same.

To make sure it's alive:

def is_back_up?(name, max \\ 200, tries \\ 0) when tries <= max do
    case GenServer.whereis(name) do
       nil ->
         Process.sleep(5)
         is_back_up?(name, max, tries + 1)
       pid -> true
    end
end

def is_back_up?(_, _, _), do: false

The basic idea is that. Not sure if there's already some helpers to do this sort of thing.

Then you just use that (you could write a 3rd helper that takes the live pid, the name, and does it all in one "step"), or write:

:ok = ensure_exited(pid)
true = is_back_up?(name)
m3characters
  • 2,240
  • 2
  • 14
  • 18
  • That would solve my problem, except neither killing, nor restarting is instantaneous, so I'd need a way to wait for restart completion. On the bright side, PIDs aren't a problem because everything starts with modulename. There is no moment in my app's lifetime where actor would have 2 instances. Also, I have actors starting in `setup` (before each test), with `start_supervised`, so is there a way to omit setup for one test, so I could do everything manually? – VOID404 Jul 27 '19 at 14:47
  • @VOID404 yeap, I'll add some more details on your first questions. I haven't used ExUnit extensively so there might be some helpers already for this, but it's easy to write 2 to do exactly this sort of thing. Regarding the `setup` part I'm not understanding what you mean though. – m3characters Jul 27 '19 at 15:07
  • I start everything supervised in setup callback of ExUnit, so controlling stuff manually (as you suggested - without supervision) would mean I have to skip setup callback for one test. – VOID404 Jul 27 '19 at 15:19
  • You can also just keep it with the setup I imagine - that was just a side note regarding the testing of the backup, theoretically it doesn't need to be supervised, but if it is it should work the same. – m3characters Jul 27 '19 at 15:24
  • Also, notice that the problem you mentioned shows also a "problem" the test won't cover, that is, if when running the actual program the server exits in between "interactions" with it, it will fail, so you need to somehow account for that possibility as well. – m3characters Jul 27 '19 at 15:31
  • For some reason my process doesn't get restarted... I start like this: `start_supervised!(Bot.Heart, restart: :permanent)`, and stop like this: `Process.exit(heart, :kill)`. I have a function that wait's for death, and function that waits until it can find PID for given name, and `Process.alive?(pid)`. The latter runs forever. – VOID404 Jul 27 '19 at 15:33
  • Oh. You are right. Am I doing this whole "actor thing" incorrectly? Could you recommend some resources to educate myself then? – VOID404 Jul 27 '19 at 15:36
  • Particular to elixir I haven't read any. Documentation for Elixir & Erlang and lots of topics around the web. For a book I would suggest Programming Erlang by Joe Armstrong, it's in erlang but should give you a good grasp of the semantics. Sasã Juric has also Elixir in Action which is explicitly in Elixir (I have it, but haven't read it yet) and I heard good things about it. You might also check elixirforum threads, there are a few about books and reviews there. – m3characters Jul 27 '19 at 19:55