GenServer for processing data in database

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?

2 Likes

To begin with:

    def handle_info(:do_work, state) do
      item = do_work()
      {:noreply, [item | state]}
    end

    defp reschedule_work do
      Process.send_after(self(), :do_work,  5 * 60 * 1000)
    end

See:

3 Likes

How is that better than my version?

  1. Process.send_after(self(), :do_work, 5 * 60 * 1000) will send :do_work, not {:do_work, item}.
  2. The Process.send_after/4 message isn’t processed by GenServer.handle_cast/2 but GenServer.handle_info/2. send_after sends a raw BEAM message which are all processed by handle_info. handle_cast only processes OTP messages cast by OTP.
4 Likes

There’ll be a delay of 2 hours in your version after init(). Why not call “do_work()” function from init directly?

Why did you make a copy-paste of that code from their documentation?

Why not call “do_work()” function from init directly?

Because “best practice” suggests to delay time consuming initialization until after GenServer.init/1 has completed - because a supervisor cannot start the next child process until init returns.

From: https://learnyousomeerlang.com/clients-and-servers#callback-to-the-future

From: https://hexdocs.pm/elixir/GenServer.html#start_link/3

See also: Simple OTP Idioms: using handle_info (Part 1)


Why did you make a copy-paste of that code from their documentation?

I didn’t - I only copied the URL of the gist. In response the forum software (Discourse) creates a onebox preview (First Onebox badge).

1 Like

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?

defmodule MyWorker do
  use GenServer

  @interval 2 * 1000

  @impl true
  def init(_args) do
    send(self(), :do_work)
    {:ok, []}
  end

  @impl true
  def handle_info(:do_work, state) do
    do_work()
    {:noreply, state}
  end

  # ---
  defp reschedule_work,
    do: Process.send_after(self(), :do_work,  @interval)

  def do_work do
    IO.puts("doing work")
    reschedule_work()
  end
end

defmodule Demo do

  def run() do
    Process.flag(:trap_exit, true)
    {:ok, pid} = GenServer.start_link(MyWorker,[])
    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()
$ elixir demo.exs
doing work
Started #PID<0.96.0>
doing work
doing work
Terminating #PID<0.96.0>
Received: {:EXIT, #PID<0.96.0>, :shutdown}
$ 

Which begs the question - why don’t you try to run some of your code to see if it works?

… which strangely reminds me of this.

3 Likes

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.

From GenServer callback init/1:

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()
1 Like