NoDisplayName
NoDisplayName

Reputation: 15736

How can I schedule code to run every few hours in Elixir or Phoenix framework?

So let's say I want to send a bunch of emails or recreate sitemap or whatever every 4 hours, how would I do that in Phoenix or just with Elixir?

Upvotes: 240

Views: 40006

Answers (10)

K. Anye
K. Anye

Reputation: 198

I built a little macro upon the accepted answer, maybe it'll be helpful for someone. (It could use some improvements, but works fine - I use it with @loop_time quite often)

defmodule Listener do
  @callback perform_work() :: any()

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour Listener
      @at_minute Module.get_attribute(__MODULE__, :at_minute, nil)
      @at_hour Module.get_attribute(__MODULE__, :at_hour, nil)
      @loop_time Module.get_attribute(__MODULE__, :loop_time, nil)

      require Logger
      use GenServer

      # ************************************************************
      # API
      # ************************************************************

      def start_link(_) do
        Logger.info("Starting #{__MODULE__} ...")
        GenServer.start_link(__MODULE__, nil, name: __MODULE__)
      end

      def stop(_) do
        Logger.info("Stopping #{__MODULE__} ... ")
        GenServer.stop(nil)
      end

      # ************************************************************
      # Callbacks
      # ************************************************************

      def init(_) do
        schedule_work()
        {:ok, nil}
      end

      def handle_info(:work, _) do
        perform_work()
        schedule_work()
        {:noreply, nil}
      end

      # ************************************************************
      # PRIVATE
      # ************************************************************

      defp schedule_work() do
        calc_loop_time = calculate_loop_time()
        Process.send_after(self(), :work, calc_loop_time)
      end

      defp calculate_loop_time() do
        if @loop_time != nil do
          @loop_time
        else
          datetime_now = DateTime.utc_now()
          time_now = Time.utc_now()

          date_today = Date.utc_today()
          date_next_day = Date.add(date_today, 1)

          {:ok, time_to_run} = Time.new(@at_hour, @at_minute, 0)

          case Time.compare(time_to_run, time_now) do
            :gt ->
              {:ok, datetime_to_run} = DateTime.new(date_today, time_to_run)
              DateTime.diff(datetime_to_run, datetime_now, :millisecond)

            :lt ->
              {:ok, datetime_to_run} = DateTime.new(date_next_day, time_to_run)
              DateTime.diff(datetime_to_run, datetime_now, :millisecond)

            :eq ->
              0
          end
        end
      end
    end
  end
end

Usage:

defmodule MacroTest.TestListener do
  @at_hour 13
  @at_minute 10

  use Listener

  def perform_work() do
    IO.puts("Hello from TestListener")
  end
end

Upvotes: 0

Zain
Zain

Reputation: 41

Normally we use Oban for this but it depends on the priority of the tasks. If you just want to run a job that should be running after a specific period of time. then you can also use Genserver.

Genservers start as our application is started. you can use periodic processes Process.send_after(self(), :work, time) and add handle_info to handle the work you want to do. I used this when i needed to add long polling to my project.

Upvotes: 1

Apoorv-2204
Apoorv-2204

Reputation: 11

Crontab lib & :timer, send_after , GenState machine or GenServer.

Generally we define cron expression in elixir module, and later parsed in that module during init. https://hexdocs.pm/crontab/readme.html

we schedule a timer using this. Process.send_after(self(), :message, time) or :timer.send_interval/2 It returns timer ref, which can be stored in state, which can also be cancelled by the ref.

Upvotes: 0

José Valim
José Valim

Reputation: 51339

There is a simple alternative that does not require any external dependencies:

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{})
  end

  def init(state) do
    schedule_work() # Schedule work to be performed at some point
    {:ok, state}
  end

  def handle_info(:work, state) do
    # Do the work you desire here
    schedule_work() # Reschedule once more
    {:noreply, state}
  end

  defp schedule_work() do
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000) # In 2 hours
  end
end

Now in your supervision tree:

children = [
  MyApp.Periodically
]

Supervisor.start_link(children, strategy: :one_for_one)

Upvotes: 499

Svilen
Svilen

Reputation: 2648

Quantum lets you create, find and delete jobs at runtime.

Furthermore, you can pass arguments to the task function when creating a cronjob, and even modify the timezone if you're not happy with UTC.

If your app is running as multiple isolated instances (e.g. Heroku), there are job processors backed by PostgreSQL or Redis, that also support task scheduling:

Oban: https://github.com/sorentwo/oban

Exq: https://github.com/akira/exq

Toniq: https://github.com/joakimk/toniq

Verk: https://github.com/edgurgel/verk

Upvotes: 45

s3cur3
s3cur3

Reputation: 3025

I find :timer.send_interval/2 slightly more ergonomic to use with a GenServer than Process.send_after/4 (used in the accepted answer).

Instead of having to reschedule your notification each time you handle it, :timer.send_interval/2 sets up an interval on which you receive a message endlessly—no need to keep calling schedule_work() like the accepted answer uses.

defmodule CountingServer do
  use GenServer

  def init(_) do
    :timer.send_interval(1000, :update)
    {:ok, 1}
  end

  def handle_info(:update, count) do
    IO.puts(count)
    {:noreply, count + 1}
  end
end

Every 1000 ms (i.e., once a second), IntervalServer.handle_info/2 will be called, print the current count, and update the GenServer's state (count + 1), giving you output like:

1
2
3
4
[etc.]

Upvotes: 6

thanos
thanos

Reputation: 732

Quantum is great, we use it at work as a cron replacement with a phoenix front-end and we also add jobs in real-time which is very neat.

Upvotes: 0

chris
chris

Reputation: 2863

Besides to use Process.send_after, you can also use :timer.apply_interval.

Upvotes: 1

Shashidhar Mayannavar
Shashidhar Mayannavar

Reputation: 560

I used Quantum library Quantum- Elixir.
Follow below instructions.

#your_app/mix.exs
defp deps do
  [{:quantum, ">= 1.9.1"},  
  #rest code
end



#your_app/mix.exs
def application do
  [mod: {AppName, []},
   applications: [:quantum,
   #rest code         
 ]]
end

#your_app/config/dev.exs
config :quantum, :your_app, cron: [
  # Every minute
  "* * * * *": fn -> IO.puts("Hello QUANTUM!") end
]

All set. Start the server by running below command.

iex -S mix phoenix.server 

Upvotes: 8

Gjaldon
Gjaldon

Reputation: 5644

You can use erlcron for that. You use it like

job = {{:weekly, :thu, {2, :am}},
  {:io, :fwrite, ["It's 2 Thursday morning~n"]}}

:erlcron.cron(job)

A job is a 2-element tuple. The first element is a tuple that represents the schedule for the job and the second element is the function or an MFA(Module, Function, Arity). In the above example, we run :io.fwrite("It's 2 Thursday morning") every 2am of Thursday.

Hope that helps!

Upvotes: 25

Related Questions