Hi,
[Question summary: is it ok to use Process.sleep/1
within a GenServer’s handle_continue
callaback during the initialization process?]
I’m starting up a (named) GenServer to process items made available by an external API. The items are then processed under a TaskSupervisor that is NOT started by the GenServer (i.e. they are siblings in the supervision tree).
When the tasks in the TaskSupervisor terminate (whether successfully or not), they will notify the GenServer (even across restarts, thank to this bit of magic: https://github.com/elixir-lang/elixir/blob/v1.8.1/lib/elixir/lib/task/supervisor.ex#L425).
If the GenServer crashes, there could be tasks still being processed by the TaskSupervisor. Naturally, the GenServer won’t have any tracking state related to those tasks. However, as they terminate and send their result to the GenServer, the GenServer will be able to rebuild its state and catch back up. What I’d like to do when initializing the GenServer is to idle if there are ongoing tasks and wait until the TaskSupervisor has no children before starting to accept messages and trigger new work.
My idea was basically to check every 5 seconds on the count of TaskSupervisor.children/1
: if it’s 0 finalize initialization, otherwise loop and wait longer. (Are there smarter ways to do this?) Do achieve this polling, I see two approaches:
-
after
init
, go into aninitializing
status. When in this status, all handlers would return{:error, :not_ready}
, and the handler would triggerProcess.send_after
to call itself again in 5 seconds if tasks are still ongoing. If no tasks are ongoing, the status is changed to:active
and a message is sent toself()
to start triggering work. -
use the recently (OTP 21) introduced
handle_continue
mechanism to achieve a similar goal, but in a cleaner fashion.init
would return{:ok, state, {:continue, :initialize}}
and indef handle_continue(:initialize, state)
the function body would either return{:ok, state}
if no tasks are ongoing, or if tasks are still running it wouldProcess.sleep(5_000)
and then return{:ok, state, {:continue, :initialize}}
.
The advantage of the 2nd approach is that other GenServer callbacks (handle_call
, etc.) wouldn’t have to treat the case where the status is :initializing
which isn’t relevant to their logic.
GenServers shouldn’t typically make use of Process.sleep
as that prevents them from being responsive, but it seems like doing so in handle_continue
would be acceptable as being “unresponsive” is kind of the goal?