How to gracefully kill gen_servers after their job is done?

I have two GenServers, A and B, where A is long-running, spawns a bunch of Bs, which perform a bunch of tasks and then are no longer needed.

So:

  • A spawns B.
  • B calls A.
  • A casts a message back to B, possibly minutes later.
  • B performs some task on the information cast by A, then needs to be killed.

So I want B to be killed either after it gets the response from A, or after a timeout. This leads to two questions:

  1. To kill B after receiving the cast from A, is it simply enough to return {:stop, :normal, state} instead of {:noreply, state}?

  2. For timing out B in case of no reply, I am returning {:ok, state, @timeout} from init (where @timeout is some timespan). I also need to add timeout to all my replies, correct? (e.g: {:noreply, state, @timeout}).

3 Likes

Hi,

I assume B does a cast to A as the 2nd step. Or is there something else
going on that needs that to be synchronous? A return value??

When you want to stop B {:stop, :normal, state} is enough.

Still assuming that B casts to A. After doing that you could do a
{:noreply, state, timeout}. If you don’t receive anything the timeout will
happen and you then need to deal with that in handle_info/2, where you can
kill B. Do note that the timeout is cancelled whenever a message is
received.
If that does not suit you, you have to manage timers yourself. It sounds
like you need that, but it is hard to tell from your description.

Cheers,
Torben

If you want a dead-simple timeout that applies to the whole lifetime of a genserver you can also do something like this:

def init(_) do
  :timer.send_after(600_000, :job_timeout)
end

def handle_info(:job_timeout, state) do
  {:stop, :timeout, state}
end

Basically you send yourself a :job_timeout message 600 seconds after it starts. This timer won’t be reset by incoming messages. The only downside I know of is that :timer has a single process that sends all of the messages so if you get up to millions of jobs it can become a bottleneck.

3 Likes

Process.send_after uses a different mechanism and would be probably the right choice here.

I wish we knew more about the actual underlying problem, it might help with feedback.

5 Likes

You can use :erlang.send_after/3/4 or :erlang.start_timer/3/4 which are handled internally in the machine and are much more efficient.

EDIT: This is probably the same as Process.send_after.

3 Likes

Yeah, Process.send_after is just erlang.send_after but it swaps the arguments around to be more in line with the noun first elixir convention. IE pid |> Process.send_after(:msg, 5_000)

3 Likes