Genserver: using Process.send_after() with handle_continue

Hello I am referencing this post where the author tries to refactor a GenServer sending messages to itself: https://medium.com/@adammokan/orchestrating-genserver-state-changes-30ab84866678

He goes from using a handle_info and Process.send_after to just a handle_continue.

The problem is that I need to delay the message sent. How would I incorporate Process.send_after?

A few bits of understanding. Typically you want your gen_server bootstrap to occur in three phases:

  1. things that need to happen in the parent process (goes in start_link)
  2. things that need to happen in the child process (goes in init)
  3. things that need to happen in the child process but after reporting successful launch to the parent process (less common, goes in handle_continue)

I believe if you need a delay you can use the timeout strategy:

def start_link...
def init(setup) do
... # initialization code
{:ok, initial_state, 1000}  #timeout for 1000 ms.
end

def handle_info(:timeout, state) do
  ... #continued bootstrap code
  {:noreply, new_state}
end

May I ask why you need a delay after starting up? Usually a fixed delay is not what you want.

I was using handle_info before and had a call to an API. I want to use handle_continue so that if the API is delayed, it doesn’t delay everything else.

edit: i forgot about the timeout strategy, I’ve edited the above.

Note that if you do a timeout and another message somehow comes in before your timeout, it will trigger before your initialization is complete.

So it seems like using handle_continue is correct (see example below), but I’m still not clear why you have wait after initialization to do the API call. Are you waiting for something in general?

def start_link...
def init(setup) do
... # initialization code
{:ok, initial_state, {:continue, :bootstrap}}  #immediate continuation
end

def handle_continue(:bootstrap, state) do
  # this happens immediately after the above
  perform_api_call()
  {:noreply, new_state}
end

After I get the result for the API call, I’m sending that state to the user. I only want to do this every 5 minutes.

got it, so you might want something like this. Note that this one does initial call after 5 minutes (the first api call happens after 5 minutes) but I think you should be able to string together an “immediate first initial call” from my code snippets! Hope this helps!

@polling_frequency 5 * 60 * 1000 # 5 minutes, in milliseconds

def start_link...
def init(setup) do
... # initialization code
schedule_api_call()
{:ok, initial_state}
end

def handle_info(:do_api_call, state) do
  perform_api_call()
  schedule_api_call()
  {:noreply, new_state}
end

defp schedule_api_call(delay \\ @polling_frequency), do: Process.send_after(self(), :do_api_call, delay)

This is what I had initally but don’t want to block other calls e.g. trying to using handle_continue

Unless there’s no reason to use handle_continue

this code shouldn’t block anything (scheduling a message is basically instantaneous), except during the api call itself! Do you expect the api call to be really slow and impinge on other activities of the gen_server? You can set up an async handler for the api call (but this will incur complexity in your code, so I would be careful.)

The idea is that on the chance that the API call is slow; other activities should continue instead of wait for it. I’m not an expert at async inside of Elixir so any advice would be very welcome .

ok. My suggestion would be to try it out first. I would bet that it doesn’t matter. If it becomes a problem, here is how you would do it:

def handle_info(:do_api_call, state) do
  this = self()
  Task.Supervised.start_child(MyTaskSupervisor, fn ->
    report_back_from_api(this, do_api_call())
  end)
  schedule_api_call()
  {:noreply, new_state}
end

defp report_back_from_api(srv, result), do: GenServer.call(srv, {:api_result, result})

You’ll have to set up a Task.Supervisor called MyTaskSupervisor in your MyApp.Application startup, and you’ll also have to implement the handle_call for {:api_result, result}

Like I said, it’s more complicated, but, you know, not terribly hard. I’d rather use this than Python twisted.

Great. I’ll try it out. Many thanks!

1 Like