Tutorial
How to setup recurring jobs with Oban in Elixir
Oban has proven itself to be the most versatile job processing library in Elixir and Phoenix. Coming from Sidekiq, it feels very familiar and supports scenarios with async jobs, jobs to be performed in a certain time in the future, and recurring jobs based on a cron schedule.
The latter is especially great if you want to setup a daily digest email to yourself or your users.
In this tutorial will go through how to add Oban and create a daily digest email that I send out with Bamboo.
Step 1 - Download a premade boilerplate
As many of the tutorials, I need some prerequisits. In this case, I want a premade Phoenix app with Pow authenication and Bamboo email.
After the boilerplate is downloaded, I need to run
mix deps.get
mix ecto.setup
yarn install --cwd assets
Step 2 - Install Oban
Oban is a well documented library an the installation instructions are pretty straight forward. First add it to the mix file.
# mix.exs
def deps do
[
{:oban, "~> 2.6.1"}
]
end
Oban is dependent on storing the job queues in in postgres database so after the reps are fetched, I ned to generate the migration file for it
mix deps.get
mix ecto.gen.migration add_oban_jobs_table
And paste in this code that comes directly from the installation instructions:
defmodule Tutorial.Repo.Migrations.AddObanJobsTable do
use Ecto.Migration def up do
Oban.Migrations.up()
end # We specify <code class="inline-code">version: 1</code> in <code class="inline-code">down</code>, ensuring that we'll roll all the way back down if
# necessary, regardless of which version we've migrated <code class="inline-code">up</code> to.
def down do
Oban.Migrations.down(version: 1)
end
end
Run the migrations by
mix ecto.migrate
These are the basic configuration recommended in the get started guide. You could tweak the queue priorizations so it fits your needs.
Most likely, you don't want to start Oban to work off queues in tests so turn it off in the test config by adding:
# config/test.exs
config :tutorial, Oban, queues: false, plugins: false
With the configurations in place, add the Oban inside the clirdrens list so its started when the application starts.
# lib/tutorial/application.ex
defmodule Tutorial.Application do
use Application def start(<i>type, </i>args) do
children = [
Tutorial.Repo,
TutorialWeb.Telemetry,
{Phoenix.PubSub, name: Tutorial.PubSub},
TutorialWeb.Endpoint,
{Oban, oban_config()} # Add this line
] opts = [strategy: :one<i>for</i>one, name: Tutorial.Supervisor]
Supervisor.start_link(children, opts)
end defp oban_config do # Add this line
Application.fetch_env!(:tutorial, Oban)
end
end
That should conclude the basic installation and setup of Oban in a Elixir application.
Step 3 - Add the daily digest mailer
Next step is to setup the Daily digest email. This will be a very simple daily digest. I just want to receive an email on all users that have signed up in the last 24 hours.
# lib/tutorial/users.ex
defmodule Tutorial.Users do
@moduledoc """
The Users context.
""" import Ecto.Query, warn: false
alias Tutorial.Repo
alias Tutorial.Users.User def list_users_from_last_day do
from(
u in User,
where: u.inserted_at > ago(1, "day")
)
|> Repo.all()
end
end
Since I want the email to be sent at the same time every day, I dont need to specify the time in the query.
Since I picked a boilerplate with Bamboo installed, I already have the mailer and some emails setup. But I need to add my digest_email that takes the users:
# lib/tutorial/emails.ex
defmodule Tutorial.Email do
import Bamboo.Email def digest_email(users) do
base_email()
|> subject("Daily Digest")
|> to("myself@example.com")
|> render("daily_digest.html", title: "Daily Digest Email", users: users)
end defp base_email do
new_email()
|> from(@from)
|> put_html_layout({TutorialWeb.LayoutView, "email.html"}) # Set default layout
|> put_text_layout({TutorialWeb.LayoutView, "email.text"}) # Set default text layout
end
end
I want to content of the email in a template file:
<!-- lib/tutorial_web/templates/email/daily_digest.html.eex -->
<%= if Enum.count(@users) == 0 do %>
<h1>No users signed up since yesterday</h1>
<% else %>
<h1>These users have joined since yesterday</h1> <%= for user <- @users do %>
<%= user.email %><br>
<% end %>
<% end %>
I can now test this in IEX
Tutorial.Users.list_users_from_last_day() |> Tutorial.Emails.digest_email() |> Tutorial.Mailer.deliver_now()
And I should get get the variant that not user have signed up since I havent added any users.
However, If I start the app and register a user:
Tutorial.Users.list_users_from_last_day() |> Tutorial.Emails.digest_email() |> Tutorial.Mailer.deliver_now()
If I run the code again, then I can see that I have a new user registered since yesterday. And with that part working, I will need to move that code from IEX to a worker and have that triggered by Oban.
Step 4 - Configure Oban to run daily
First I want to store my workers in a separate folder. So I create a workers folder and add the daily digest worker in there. I will do the exact same thing as the command I ran in the IEX console.
# lib/tutorial/workers/daily_digest_worker.ex
defmodule Tutorial.Workers.DailyDigestWorker do
use Oban.Worker alias Tutorial.{Users, Emails, Mailer} @impl Oban.Worker
def perform(_job) do
Users.list<i>users</i>from<i>last</i>day()
|> Emails.digest_email()
|> Mailer.deliver_now() :ok
end
end
The last piece of the puzzle is to configure Oban to run the worker daily at a specific time. Oban supports the normal cron syntax for this. In this case, I want to run this at 08:00 (server time) every day.
# config/config.exs
config :tutorial, Oban,
repo: Tutorial.Repo,
queues: [default: 10, mailers: 20, events: 50, low: 5],
plugins: [
Oban.Plugins.Pruner,
{Oban.Plugins.Cron,
crontab: [
{"0 8 * * *", Tutorial.Workers.DailyDigestWorker},
]}
]