My GenServer should process data in database where it’s inserted by external events via REST API. Thus, GenServer will periodically, around every 5 minutes, fetch data from db which “status = unprocessed”, make some checkes and update it with “result=N, status=processed”.
Since there’s no need to call it because it’ll work on its own, I’ve come up with this simplified version of my code:
defmodule MyWorker do
use GenServer
@impl true
def init(stack) do
reschedule_work()
{:ok, stack}
end
@impl true
def handle_cast({:do_work, item}, state) do
do_work()
{:noreply, [item | state]}
end
defp reschedule_work do
Process.send_after(self(), :do_work, 5 * 60 * 1000)
end
# todo
def do_work do
# ..........
reschedule_work()
end
end
Is this implementation correct for my task? Can it be improved?
Why not call “do_work()” function from init directly?
Because “best practice” suggests to delay time consuming initialization until afterGenServer.init/1 has completed - because a supervisor cannot start the next child process until init returns.
And you copied it there, didn’t you?
One thing - in my case there’s no state because a state is fetched from my db each time, there’s nothing to pass around. What is “state” for in your example?
One thing - in my case there’s no state because a state is fetched from my db each time, there’s nothing to pass around. What is “state” for in your example?
Any GenServer has state (that‘s one reason they exist). You can choose to not put any meaningful values in it, but there‘s always state to pass around.
Returning {:ok, state} will cause start_link/3 to return {:ok, pid} and the process to enter its loop.
so looking at:
def init(_args) do
send(self(), :do_work)
{:ok, []}
end
it should the clear that state is [] - i.e. an empty list. Given that state isn’t used {:ok, :ok} or {:ok, nil} would also work. What is important is that state is some kind of value - regardless of whether you intend to use it or not (yes, nil is a value - in fact it simply is syntactic sugar for the :nil atom). Of course if you intend to use it, state needs to be something meaningful.
defmodule MyWorker do
use GenServer
@impl true
def init(args) do
{interval, _args} = Keyword.pop(args, :interval, 5000)
send(self(), :do_work)
{:ok, init_state(interval)}
end
@impl true
def handle_info(:do_work, state) do
new_state = do_work(state)
{:noreply, new_state}
end
# ---
defp init_state(interval),
do: {interval, 0}
defp interval({interval, _}),
do: interval
defp count({_, count}),
do: count
defp next_state({interval, count}),
do: {interval, count + 1}
defp schedule_work(interval),
do: Process.send_after(self(), :do_work, interval)
defp do_work(state) do
IO.puts("doing work: #{count(state)}")
state
|> interval()
|> schedule_work()
next_state(state)
end
end
defmodule Demo do
def run() do
Process.flag(:trap_exit, true)
interval = 2 * 1000
{:ok, pid} = GenServer.start_link(MyWorker,[interval: interval])
IO.puts("Started #{inspect pid}")
Process.sleep(5000)
Process.exit(pid, :shutdown)
IO.puts("Terminating #{inspect pid}")
receive do
msg ->
IO.puts("Received: #{inspect msg}")
end
end
end
Demo.run()