233

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?

ryanwinchester
  • 11,737
  • 5
  • 27
  • 45
NoDisplayName
  • 15,246
  • 12
  • 62
  • 98

9 Answers9

483

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)
José Valim
  • 50,409
  • 12
  • 130
  • 115
  • 210
    It's impossible not to love this language :) – NoDisplayName Aug 19 '15 at 15:46
  • 3
    Where should I put this file? Under lib/ directory of Phoenix project? Where do the test go, to test/periodically/*? – EugZol Nov 21 '15 at 18:59
  • 11
    In lib because it is a long running process. You can put the test whatever makes sense, maybe "test/my_app/periodically_test.exs". – José Valim Nov 28 '15 at 11:14
  • 2
    Any particular reason to not move `Process.send_after` into its own function so that the function can be called from both `init` and `handle_info`? – Ryan Bigg Jan 11 '16 at 20:11
  • 1
    Or not to do `:timer.send_interval`? – Cody Poll Apr 22 '16 at 23:26
  • 28
    @CodyPoll `:timer.send_interval` is fine but keep in mind that the intervals will constant. So imagine you want to do something every minute and, in the future, the work itself takes more than a minute. In such cases, you'd be working all the time and your message queue would grow unbounded. The solution above will always wait the given period *after* the work is done. – José Valim Jul 01 '16 at 09:26
  • 1
    @JoséValim Just found this question. Is there any explanation, why the message from Process.send_after should be handled by handle_info? Didn't find any mentions in official doc. – Stanislav Mekhonoshin Aug 22 '16 at 16:00
  • 1
    @StanislavMekhonoshin it is how GenServers work behind the scenes. When it receives messages that were not sent with call nor cast, they are handled by `handle_info/2`. – José Valim Aug 31 '16 at 22:25
  • @JoséValim thanks! we already discussed it at elixir issues on github :) – Stanislav Mekhonoshin Sep 02 '16 at 07:31
  • 2
    The drawback to using GenServer alone is that all of your scheduled jobs will be lost if your application crashes. That's why there are so many libraries dealing with this problem - most of the time you need resiliency against losing scheduled tasks. – sheldonkreger Sep 03 '16 at 05:05
  • 1
    Applications don't crash, processes crash. You shouldn't have the state critical to the work in the process where the scheduling is done. – Michael Terry Sep 16 '16 at 03:42
  • 2
    In case anybody gets stuck, like me, don't use a float in your interval math. I was using `0.25`. This produces an argument error from `:erlang.send_after`. The error messages shows the arguments to `:erlang.send_after` in a different order than given to `Process.send_after`, which threw me off for a while. – Brad Johnson Oct 31 '17 at 03:41
  • 1
    addressing the crash possibility raised by @sheldonkreger: persist the time of your last execution in ets or a disk persistence layer, and query that during `init`, computing your first delay. – Chris Meyer Jan 21 '18 at 02:22
  • 2
    It is also highly dependent on the task. Most tasks won't cause an issue if they end-up running twice in a row. – José Valim Jan 21 '18 at 09:26
  • 1
    If this task is going to be run every 2 hours would hibernating the genserver between tasks help? – heartmo Nov 03 '19 at 21:52
  • @JoséValim I've written a lib to extend your code with cron-like syntax and support for multiple nodes running distributed: https://github.com/loopsocial/ex_scheduler. – alexandrecosta Jan 22 '20 at 18:15
  • 5
    @JoséValim — Is the `{MyApp.Periodically, []}` construct a preferred equivalent to `worker(MyApp.Periodically, [])` in Elixir these days? Trying to find docs to confirm this but having trouble this morning. Absolutely love this example btw — very instructive. Playing around with it and having fun with using `:continue` etc. – Darragh Enright May 02 '20 at 13:03
43

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

Svilen
  • 2,608
  • 24
  • 26
25

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!

Gjaldon
  • 5,534
  • 24
  • 32
  • Yeah it's better than nothing, thank you. I will leave the question unanswered for a while, maybe there will be other suggestions – NoDisplayName Aug 19 '15 at 10:27
  • 4
    You're welcome! There's also https://github.com/c-rack/quantum-elixir which is an elixir lib, if you prefer – Gjaldon Aug 19 '15 at 10:47
8

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 
ryanwinchester
  • 11,737
  • 5
  • 27
  • 45
5

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.]
s3cur3
  • 2,749
  • 2
  • 27
  • 42
  • 3
    Please keep in mind that this solution can lead to an overflowing queue if your interval is smaller than the time it takes to finish the task. – Joe Eifert Nov 09 '20 at 12:23
1

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

chris
  • 2,761
  • 17
  • 24
1

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.

Zain
  • 41
  • 4
0

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.

thanos
  • 723
  • 7
  • 9
0

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.