When calling a function I want to retry the function if I don't find the data I expected. I want to retry after 10 seconds from the time that the function failed.
def check_question do
case question = Repo.get_by(Question, active: true, closed: false) do
question when not(is_nil(question)) ->
case ActiveQuestion.ready_for_answer_status(question) do
n when n in ["complete", "closed"] ->
question
|> Question.changeset(%{ready_for_answer: true, closed: true})
|> Repo.update()
end
_ ->
Process.send_after(Servers.Retry, :update, 10_000)
end
end
defmodule Servers.Retry do
use GenServer
require IEx
def start_link do
GenServer.start_link(__MODULE__, %{})
end
def init(state) do
{:ok, state}
end
def handle_info(:update, state) do
Scheduler.check_question()
{:noreply, state}
end
end
As you can see I'm trying to retry the function if the case statement is not met. But this does not quite work.
#Reference<0.1408720145.4224712705.56756>
It never calls the genserver from within Servers.Retry. I'm a super GenServer noob so forgive the lack of understanding. Thank you!!
So there are a few things to improve here.
First, you are trying to access your GenServer by registered name (Process.send_after(Servers.Retry...
), without actually registering the name.
The basic way register a name is to include the :name
opt in your call to GenServer.start_link
, e.g.:
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
Next, from a design perspective, you've broken encapsulation of the Retry
GenServer. As a quick rule of thumb:
The atoms used within a module shouldn't need to be known by other modules unless they are explicity part of the API (like opts and structs).
How do we fix it? Simple. Put the call to Process.send_after/3
inside the Servers.Retry
module:
defmodule Servers.Retry do
use GenServer
### External API:
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def retry(delay \\ 10_000) do
Process.send_after(__MODULE__, :retry, delay)
end
### GenServer Callbacks
def init(state) do
{:ok, state}
end
def handle_info(:retry, state) do
Scheduler.check_question()
{:noreply, state}
end
end
I found this aspect one of the most confusing parts of learning GenServer: Some of the code defined in this module runs in the GenServer process, and some runs in other processes. Specifically, the two API methods are intended to be called by other processes- start_link
by a Supervisor, and retry
by an actual client.
By placing the Process.send_after
call inside an API method, we've simplified other methods, and decoupled what the Retry
server does (tries again) from how it accomplishes that (with a send_after
).
My last suggestion: Either make the Retry server more generic, or more specific. Right now, it can only help the Scheduler
, because it is too specific - the action to retry is hardcoded in. One idea is to have retry accept an arity-0 function to call when it's time to retry:
def retry(action, delay \\ 10_000) do
Process.send_after(__MODULE__, {:retry, action}, delay)
end
# ...snip
def handle_info({:retry, action}, state) do
action.()
{:noreply, state}
end
Now, it can retry anything- just pass a lambda to it. On the other hand, this seems like a feature that may not rate an abstraction. In this case, just collapse the two modules into one. I can't give you a code sample because I'm not sure what else is in Scheduler, but it shouldn't be too tricking to mix them into one.