Oban Training—Signing Up
Mix.install([:faker, :kino, :oban, :postgrex])
Logger.configure(level: :info)
Application.put_env(:chow_mojo, ChowMojo.Repo,
pool: Ecto.Adapters.SQL.Sandbox,
url: "postgres://localhost:5432/chow_mojo_dev"
)
defmodule ChowMojo.Repo do
use Ecto.Repo, otp_app: :chow_mojo, adapter: Ecto.Adapters.Postgres
end
defmodule CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add(:name, :text, null: false)
add(:email, :text, null: false)
add(:address, :text)
timestamps()
end
create(index(:users, [:email], unique: true))
end
end
defmodule ChowMojo.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field(:email, :string)
field(:name, :string)
timestamps()
end
def insert_changeset(params) do
cast(%__MODULE__{}, params, ~w(email name)a)
end
end
defmodule ChowMojo do
def get_user(id) do
ChowMojo.Repo.get(ChowMojo.User, id)
end
def send_welcome_email(%{email: email}) do
send(self(), {:delivered, email})
{:ok, email}
end
end
ChowMojo.Repo.start_link()
Ecto.Migrator.run(ChowMojo.Repo, [{1, CreateUsers}], :up, all: true)
Ecto.Adapters.SQL.Sandbox.mode(ChowMojo.Repo, :manual)
🏅 Goals
In this exercise you’ll create a job that delivers follow-up email(s) in the background after a new user signs up. Pushing email delivery into the background makes delivery more reliable due to error handling and retries with backoff.
Welcoming Users
We’ll start by creating a ChowMojo.WelcomeEmail worker that delivers a welcome email with ChowMojo.send_welcome_email/1 after users sign up. Pass options to use Oban.Worker that configure the worker to use the :email queue.
Your perform/1 function should accept an args map like %{"id" => id} and use ChowMojo.get_user/1 to fetch the user. Job args are always serialized into a JSON map, and structs such as Ecto schema aren’t serializable.
Configure the worker’s queue with queue: :email. Then, write a perform/1 function that accepts a job struct with string args, fetches the user, then sends a welcome email.
defmodule ChowMojo.WelcomeWorker do
use Oban.Worker, queue: :email
@impl Worker
def perform(%Job{args: %{"id" => id}}) do
id
|> ChowMojo.get_user()
|> ChowMojo.send_welcome_email()
end
end
defmodule ChowMojo.WelcomeWorker do
# Your turn...
end
Now use Oban.insert/1 to enqueue a ChowMojo.WelcomeWorker job for your new worker after a user is created within ChowMojo.create_user/1. Remember, only pass in the user’s id, not the entire User struct.
Take the {:ok, user} returned from Repo.insert/1 and insert a WelcomeWorker job:
def create_user(params) do
changeset = ChowMojo.User.insert_changeset(params)
with {:ok, user} <- ChowMojo.Repo.insert(changeset) do
%{id: user.id}
|> ChowMojo.WelcomeWorker.new()
|> Oban.insert!()
{:ok, user}
end
end
defmodule ChowMojo.Users do
@spec create_user(map) :: {:ok, ChowMojo.User.t()} | {:error, Ecto.Changeset.t()}
def create_user(params) do
changeset = ChowMojo.User.insert_changeset(params)
{:ok, user} = ChowMojo.Repo.insert(changeset)
end
end
Testing
To verify the job is enqueued with the necessary arguments in the correct queue we’ll write a test. But first, we need to configure Oban for testing.
Oban offers two testing modes: :inline and :manual. The :inline mode executes jobs directly in the test process and avoids touching the database at all, whereas :manual mode uses Ecto’s sandbox to insert jobs in an isolated transaction. In either case, Oban doesn’t run any queue processes to simplify testing.
We’ll use both modes to test our aspects of the WelcomeWorker, starting with :inline. Modify setup/0 to start Oban in :inline mode. (Note: Typically this configuration goes in test.exs and you run tests with mix test)
Add testing: :inline to where Oban is started:
start_supervised!({Oban, repo: ChowMojo.Repo, testing: :inline})
ExUnit.start(auto_run: false)
defmodule ChowMojo.InlineUsersTest do
use ExUnit.Case
setup do
# Your turn...
start_supervised!({Oban, repo: ChowMojo.Repo})
:ok = Ecto.Adapters.SQL.Sandbox.checkout(ChowMojo.Repo)
end
test "creating a user delivers a welcome email" do
params = %{email: "shannon@sorentwo.com", name: "Shannon"}
{:ok, user} = ChowMojo.Users.create_user(params)
assert_email_delivered(user.email)
end
defp assert_email_delivered(email) do
assert_receive {:delivered, ^email}, 250, "expected an email delivered to #{email}"
end
end
ExUnit.run()
Once the WelcomeWorker job executes successfully you’ll see one test pass with zero failures. You’ve exercised the full welcome flow within the test process, without touching the database! Now we’ll write another test using the helpers provided by Oban.Testing to assert that the job is enqueued in the database properly.
In this test the testing mode is already set to :manual. Setup the testing helpers, then use assert_enqueued/1 to verify the job is enqueued.
Assert the job is enqueued by checking the :worker, :queue, and :args:
assert_enqueued worker: ChowMojo.WelcomeWorker, queue: :email, args: %{id: user.id}
ExUnit.start(auto_run: false)
defmodule ChowMojo.ManualUsersTest do
use ExUnit.Case
# Your turn...
setup do
start_supervised!({Oban, repo: ChowMojo.Repo, testing: :manual})
:ok = Ecto.Adapters.SQL.Sandbox.checkout(ChowMojo.Repo)
end
test "creating a user enqueues a welcome email" do
params = %{email: "shannon@sorentwo.com", name: "Shannon"}
{:ok, user} = ChowMojo.Users.create_user(params)
# Your turn...
end
end
ExUnit.run()
☠️ Extra Challenges
Schedule a follow-up job
Create another worker and deliver a follow-up job one day in the future after user creation. The follow-up should call ChowMojo.second_day_email/1 to deliver the email.
-
Use the
schedule_inoption to delay job execution until a later time -
Try using the
{:days, N}shorthand rather than calculating using raw seconds - Write a test to verify the email is scheduled for delivery in the future
Enqueue in a transaction
Because Oban jobs are database records they can be inserted alongside your domain objects within a transaction. Use Oban.insert/3 to insert everything together.
-
Use an
Ecto.Multito create theUserand insert both jobs in a single transaction - Use a function to create each job so you can reference the Multi’s changes