lllllll
lllllll

Reputation: 4835

Elixir/OTP continuous background job and state lookup

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 HAVING 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

Update 1

Approach delegating the "ticking" to an underlying Process, thanks to the reply I received at ElixirForum.

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

Upvotes: 1

Views: 397

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 120990

To make things easier it’s always good to use as few abstraction levels as possible. You basically need two different processes: one to tick and one to consume. That way the consumer will only be responsible to handle a state, and “the ticker” will just ping it with intervals specified:

defmodule World.Cell do
  @interval 500
  def start_link do
    {:ok, pid} = Task.start_link(fn ->
      loop(%{time: :random.uniform, voltage: 42})
    end)
    Task.start_link(fn -> tick([interval: @interval, pid: pid]) end)
    {:ok, pid}
  end

  # consumer’s loop
  defp loop(map) do
    receive do
      {:state, caller} -> # state requested
        send caller, {:voltage, Map.get(map, :voltage)}
        loop(map)
      {:ping} ->          # tick 
        loop(map
             |> Map.put(:voltage, map.voltage + 1)
             |> Map.put(:time, map.time + :random.uniform/12))
    end
  end

  # ticker loop
  defp tick(init) do
    IO.inspect init, label: "Tick"
    send init[:pid], {:ping}
    Process.sleep(init[:interval])
    tick(init)
  end
end

{:ok, pid} = World.Cell.start_link

(1..3) |> Enum.each(fn _ ->
  {:state, _result} = send pid, {:state, self()}
  receive do
    {:voltage, value} -> IO.inspect value, label: "Voltage"
  end
  Process.sleep 1000
end)

The output would be:

Voltage: 42
Tick: [interval: 500, pid: #PID<0.80.0>]
Tick: [interval: 500, pid: #PID<0.80.0>]
Voltage: 44
Tick: [interval: 500, pid: #PID<0.80.0>]
Tick: [interval: 500, pid: #PID<0.80.0>]
Voltage: 46
Tick: [interval: 500, pid: #PID<0.80.0>]
Tick: [interval: 500, pid: #PID<0.80.0>]

The implementation with GenServers should be now pretty straightforward.

Upvotes: 1

Related Questions