How to create periodical timer with Elixir?

Viewed 496

I'm trying to create a periodical timer with Elixir which would take the lenght of period (milliseconds) as parameter and a function to be executed after time has elapsed. User should also have option to cancel timer. If passed function returns :cancel value, then it should also cancel timer. Here is my code:

def start_timer(period, callback_function) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
    timer = Process.send_after(self(), :work, period)
    {:ok, %{timer: timer}}
  end
def handle_info(:work, period) do
    IO.puts("This should printed after every period")
    # Start the timer again
    timer = Process.send_after(self(), :work, period)

    {:noreply, %{timer: timer}}
  end

I have not yet implemented cancel-feature or the execution of parameter function as the above doesn't work yet. How I should do this? Any advise is very highly appreciated.

1 Answers

It doesn't look like you are using your callback_function anywhere. If you want that to be available throughout the running of your timer, you will need to pass it along in the state in the 2nd argument to GenServer.start_link/3, e.g.

GenServer.start_link(__MODULE__, %{callback_function: callback_function}, name: __MODULE__)

Here is some code that periodically executes a function:

defmodule Cronlike do
  use GenServer

  @allowed_units [:second, :minute, :hour, :day]

  def start_link(state) do
    GenServer.start_link(__MODULE__, state)
  end

  @impl true
  def init(%{interval: interval, unit: unit} = state) when is_integer(interval) and interval > 0 and unit in  @allowed_units do
    # wait, bang:
    Process.send_after(self(), :tick, to_ms(interval, unit))
    # bang, wait:
    # send(self(), :tick, to_ms(interval, unit))

    {:ok, state}
  end

  @impl true
  def handle_info(:tick, %{interval: interval, unit: unit, 
    callback_function: callback_function} = state) do

    case callback_function.() do
      :cancel -> send(self(), :quit)
      _ -> nil
    end

    Process.send_after(self(), :tick, to_ms(interval, unit))
    {:noreply, state}
  end

  def handle_info(:quit, state) do
    {:stop, :normal, state}
  end

  defp to_ms(interval, :second), do: interval * 1000
  defp to_ms(interval, :minute), do: to_ms(interval, :second) * 60
  defp to_ms(interval, :hour), do: to_ms(interval, :minute) * 60
  defp to_ms(interval, :day), do: to_ms(interval, :hour) * 24
end

This was adapted from a related article I wrote Wait, Bang vs. Bang, Wait: Subtleties in Elixir Cron Scripts

Related