GenServer continuous background job and ability to retrieve its state

Hi,
Long time reader, first time poster here :slight_smile:
I originally posted this question at stack overflow so I am reposting it here with the hope of receiving suggestions ( and after getting some ideas from this ExForum thread). This is a use-case that I’d find very handy to master. Re-post below:

I’m trying to model a simple oscillator that is continuously running in the background (integrating a sine function). However at some point I want to be able to request its value (voltage and time), which is kept in its internal state. That is because at a latter point I will want a pool of oscillators supervised, and their Supervisor will average the voltage/values, and other handful operations.

I reached this approach, which I’m not 100% happy with, since it’s a bit of a pain to have to run run() before exiting the get_state server implementation, ie. handle_call({:get_state, pid}.....).

Is there any other approach I could give a try to?

defmodule World.Cell do
  use GenServer
  @timedelay  2000
  # API #
  #######
  def start_link do
    GenServer.start_link(__MODULE__, [], [name: {:global, __MODULE__}])
  end
  def run do
    GenServer.cast({:global, __MODULE__}, :run)
  end
  def get_state(pid) do
    GenServer.call(pid, {:get_state, pid})
  end

  # Callbacks #
  #############
  def init([]) do
    :random.seed(:os.timestamp)
    time = :random.uniform
    voltage = :math.sin(2 * :math.pi + time)
    state = %{time: time, voltage: voltage }
    {:ok, state, @timedelay}
  end
  def handle_cast(:run, state) do
    new_time = state.time + :random.uniform/12
    new_voltage = :math.sin(2 * :math.pi + new_time)
    new_state = %{ time: new_time, voltage: new_voltage }
    IO.puts "VALUES #{inspect self()} t/v #{new_time}/#{new_voltage}"
    {:noreply, new_state, @timedelay}
  end
  def handle_info(:timeout, state) do
    run()  # <------------ ALWAYS HAS TO RUN IT
    {:noreply, state, @timedelay}
  end
  def handle_call({:get_state, pid}, _from, state) do
    IO.puts "getting state"
    run() # <------- RUN UNLESS IT STOPS after response
    {:reply, state, state}
  end
end

I was going to ask:

I don’t understand why you have to run() when getting the state.

But you’re using the @timedelay as a ticking clock.

Just use a timer instead.

def init([]) do
    :random.seed(:os.timestamp)
    time = :random.uniform
    voltage = :math.sin(2 * :math.pi + time)
    timer_ref = Process.send_after(self(), :tick, @timedelay)
    state = %{time: time, voltage: voltage, timer: timer_ref}
    {:ok, state}
  end

  def handle_cast(:run, state) do
    # clear timer or if sent let :tick be handled by handle_info
    if Process.clear_timer(state.timer) do
      new_state = run(state)
      timer_ref = Process.send_after(self(), :tick, @timedelay)
      {:noreply, %{new_state | timer: timer_ref}}
    else
      {:noreply, state}
    end
  end

  def handle_info(:tick, state) do
    new_state = run(state)  # <------------ ALWAYS HAS TO RUN IT
    timer_ref = Process.send_after(self(), :tick, @timedelay)
    {:noreply, %{new_state | timer: timer_ref}}
  end

  def handle_call({:get_state, pid}, _from, state) do
    IO.puts "getting state"
    return = Map.take(state, [:time, :voltage])
    {:reply, return, state}
  end

  defp run(state) do
    new_time = state.time + :random.uniform/12
    new_voltage = :math.sin(2 * :math.pi + new_time)
    new_state = %{state | time: new_time, voltage: new_voltage}
    IO.puts "VALUES #{inspect self()} t/v #{new_time}/#{new_voltage}"
    new_state
  end
end

At least, if I correctly understand what you are trying to do. If not, tell me what I missed.

1 Like

Thanks! I really like your suggestion to go the Process.send_after way. Based on your approach, I’m liking this implementation(basically removing run()/handle_cast(:run)):

defmodule World.Cell do
  use GenServer
  @timedelay  2000

  def start_link do
    GenServer.start_link(__MODULE__, [], [name: {:global, __MODULE__}])
  end
  def get_state(pid) do
    GenServer.call(pid, {:get_state, pid})
  end


  def init([]) do
    :random.seed(:os.timestamp)
    time = :random.uniform
    voltage = :math.sin(2 * :math.pi + time)
    timer_ref = Process.send_after(self(), :tick, @timedelay)
    state = %{time: time, voltage: voltage, timer: timer_ref}
    {:ok, state}
  end

  def handle_info(:tick, state) do
    new_state = run(state) 
    timer_ref = Process.send_after(self(), :tick, @timedelay)
    {:noreply, %{new_state | timer: timer_ref}}
  end

  def handle_call({:get_state, pid}, _from, state) do
    IO.puts "getting state"
    return = Map.take(state, [:time, :voltage])
    {:reply, return, state}
  end

  defp run(state) do
    new_time = state.time + :random.uniform/12
    new_voltage = :math.sin(2 * :math.pi + new_time)
    new_state = %{state | time: new_time, voltage: new_voltage}
    IO.puts "VALUES #{inspect self()} t/v #{new_time}/#{new_voltage}"
    new_state
  end
end

A couple of questions though:

  1. Why would I want to handle_cast(:run,state) anymore? What is the point of keeping the timer to clear it?

  2. Given the fact that I will eventually want a pool of these oscillators running autonomously and being supervised. Is it a better idea to use Task instead of Process? Can an OTP Supervisor monitor and restart a Process(ie. the ticking process)?

I wouldn’t want it but I don’t know your requirements.

No, a Task is meant to be more of a fire and do something else or fire and forget. There’s no receive loop like a GenServer. A Supervisor works great with a GenServer and will restart it whenever it crashes, though when a process crashes it loses its state.

So in this case the Supervisor is monitoring World.Cell which behaves as GenServer, which internally starts an on-going background process (ie. Process.send_after(self(), :tick, @timedelay)) that keeps calling itself.
My question is, if this Process fails (eg. the logic inside run() function might get more complicated later), will the Supervisor also restart it? If not, would there be a “superviseable” alternative?

I want World.Cell to keep running the “ticking” unless it crashes, how could I guarantee that?
Thanks again!

Oh I see, you’re saying if the timer itself fails. Umm, I don’t know. It’s never failed for me.

It calls an Erlang Built-in-Function(BIF) which I think would cause it to be linked to the calling process. I would look into for you, but I don’t know my way around ERTS.

Someone who know Erlang better than myself would have to tell you. Maybe @rvirding would know more?

Simple answer: Yes! The supervisor will restart the process!

More in depth answer:

What happens exactly depends on the settings in your Supervisor:

  1. The Supervision Strategies that determine when a process should be considered for restarting.
  2. The Restart Strategies which determine what to do when a process is considered for restarting.

In most setups, the first will probably be :one_for_one, :one_for_all or :simple_one_for_one. The second will usually :transient (which means that the current process is always restarted, except when it was shut down in a proper manner).

That the process sends messages to itself (using a timer or using anything else) does not alter how a supervisor treats the process.

1 Like

I don’t quite understand how you mean. The Process:send_after/3 calls a BIF which asks the BEAM to send a message to the process after a certain time. There is no linking as such as there is no process which sends the message, it is the BEAM itself.

This is different from the :timer module which does a timer process running which keeps track of the time and sends the messages. The BIF is generally considered more efficient.

Sorry, I phrased my question too closely to how @_toni asked, which also confused me at first. I think his original question is based on a bit of a misunderstanding of how Erlang works, but the underlying worry of failure was still there and not something I can answer.

The question I think @_toni meant to ask was:
What happens when Process.send_after/3 fails to send?
What guarantee is there that the message will be sent?
How can I mitigate the failure of Process.send_after/3 not sending?

I couldn’t find how the timers work in ERTS so I honestly don’t know.

@Azolo @rvirding Thank you for the interesting details on the Erlang underlying operation

Great info! I will change the code a bit and experiment with it, see if I can find a situation ( as I initially thought would be) where the Process is not restarted, and report back regardless :smile:

Process.send_after/3 is a BIF (built-in function) so it it the BEAM which remembers to send a message and does the sending. If a message is not sent then it is a failure in the BEAM itself so it is as reliable as the BEAM.

An aside here is that all the functions in Process are BIFs, or rather just call the Erlang BIFs. This is true of other modules as well like Kernel, Node, Port and System. I think two reasons for this are to better group the functions and to give them more Elixiry arguments. For example Process:register(pid, name) becomes a call to :erlang.register(name, pid).

All Erlang BIFS are implemented in the module :erlang.

4 Likes