Oban Configuration
View SourceAshDispatch uses Oban for asynchronous email delivery. This guide explains how to configure Oban for your application.
Requirements
AshDispatch requires Oban 2.0 or later. Add it to your dependencies if not already present:
# mix.exs
defp deps do
[
{:oban, "~> 2.0"}
]
endBasic Setup
1. Configure Oban
Add Oban configuration to your config/config.exs:
config :my_app, Oban,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
repo: MyApp.Repo,
queues: [
# Email queue for AshDispatch
emails: 10 # Process up to 10 email jobs concurrently
]2. Add Oban to Supervision Tree
In your application.ex, add Oban to your supervision tree:
def start(_type, _args) do
children = [
MyApp.Repo,
{Oban, Application.fetch_env!(:my_app, Oban)},
# ... other children
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end3. Run Oban Migrations
Oban requires database tables to track jobs:
mix ecto.gen.migration add_oban
Then add the migration:
defmodule MyApp.Repo.Migrations.AddOban do
use Ecto.Migration
def up do
Oban.Migration.up(version: 12)
end
def down do
Oban.Migration.down(version: 1)
end
endRun migrations:
mix ecto.migrate
Custom Queue Name
If you want to use a different queue name (e.g., mailer instead of emails), configure AshDispatch:
# config/config.exs
config :ash_dispatch,
email_queue: :mailer # Use :mailer queue instead of :emailsThen add the queue to your Oban config:
config :my_app, Oban,
queues: [
mailer: 10 # AshDispatch will use this queue
]Retry Configuration
By default, email jobs retry up to 5 times with exponential backoff. You can customize this:
# config/config.exs
config :ash_dispatch,
max_email_attempts: 3 # Only retry failed emails 3 timesQueue Concurrency
Adjust the concurrency (number of jobs processed in parallel) based on your needs:
config :my_app, Oban,
queues: [
emails: 20 # Higher concurrency for email-heavy apps
]Guidelines:
- Low volume (< 100 emails/day):
emails: 5 - Medium volume (100-1000 emails/day):
emails: 10 - High volume (> 1000 emails/day):
emails: 20+
Scheduled Delivery
AshDispatch supports delayed email delivery via the channel's time option:
event :welcome,
channels: [
[
transport: :email,
audience: :user,
time: {:in, 300} # Send email in 5 minutes (300 seconds)
]
]Oban automatically handles the scheduling - no additional configuration needed.
Production Considerations
1. Use Postgres Notifier
Always use Oban.Notifiers.Postgres in production for reliable job distribution:
# config/prod.exs
config :my_app, Oban,
notifier: Oban.Notifiers.Postgres # Required for multi-node deployments2. Enable Plugins
Add recommended Oban plugins for production:
config :my_app, Oban,
plugins: [
# Prune completed jobs older than 7 days
{Oban.Plugins.Pruner, max_age: 604_800},
# Rescue orphaned jobs
{Oban.Plugins.Lifeline, rescue_after: :timer.minutes(30)},
# Automatic retry of failed deliveries (optional)
{Oban.Plugins.Cron,
crontab: [
# Retry failed email deliveries every 15 minutes
{"*/15 * * * *", AshDispatch.Workers.RetryFailedDeliveries}
]}
]3. Monitor Job Performance
Use ObanWeb for monitoring:
# mix.exs
defp deps do
[
{:oban_web, "~> 2.0"}
]
endAdd to your router:
# lib/my_app_web/router.ex
scope "/" do
pipe_through :browser
forward "/oban", ObanWeb.Router
endTesting
Test Mode Options
Oban provides three testing modes. Choose carefully - this significantly affects how your dispatch tests behave:
| Mode | Behavior | Use When |
|---|---|---|
:inline | Jobs execute synchronously in the same process | Testing full dispatch flow end-to-end |
:manual | Jobs enqueued but not executed | Testing job arguments without side effects |
| (none) | Jobs execute async as normal | Integration tests with real timing |
Recommended: Use :inline for Dispatch Tests
For testing AshDispatch events, :inline mode is recommended because it ensures:
- Events dispatch synchronously during tests
- Delivery receipts are created immediately
- Notifications appear right after the action completes
# config/test.exs
config :my_app, Oban,
testing: :inline # Jobs execute synchronously in testsWhy not :manual? With :manual mode, your tests may see events as "pending" because the Oban job hasn't run. You'd need to manually execute jobs to see the full flow.
Common Testing Pitfall
Problem: Tests pass individually but fail when run together.
Cause: Usually happens when using :manual mode - jobs from previous tests interfere with later tests.
Solution: Use :inline mode for most dispatch tests:
# config/test.exs
config :my_app, Oban,
testing: :inline,
queues: false # Disable queue polling in testsUsing Manual Testing Mode
Use :manual when you want to test job arguments without executing side effects:
config :my_app, Oban,
testing: :manual # Jobs are enqueued but not executedAssert Jobs Enqueued
Test that jobs are enqueued correctly:
use Oban.Testing, repo: MyApp.Repo
test "email job is enqueued" do
# Trigger event that sends email
product
|> Ash.Changeset.for_create(:create, %{name: "Widget"})
|> Ash.create!()
# Assert job was enqueued
assert_enqueued worker: AshDispatch.Workers.SendEmail,
args: %{
"receipt_id" => receipt.id,
"recipient_email" => "user@example.com"
}
endExecute Jobs Manually
In manual testing mode, execute jobs explicitly:
test "email is sent" do
# Trigger event
create_product()
# Find enqueued job
job = Enum.at(all_enqueued(worker: AshDispatch.Workers.SendEmail), 0)
# Execute job
perform_job(AshDispatch.Workers.SendEmail, job.args)
# Assert receipt was marked as sent
receipt = Ash.get!(DeliveryReceipt, job.args["receipt_id"])
assert receipt.status == :sent
endTroubleshooting
Jobs Not Processing
Problem: Jobs enqueued but never execute.
Solutions:
- Check Oban is started:
Oban.config() - Verify queue exists:
config :my_app, Oban, queues: [emails: 10] - Check queue is running: Visit
/obandashboard
High Job Failure Rate
Problem: Many jobs failing with errors.
Solutions:
- Check email backend configuration
- Verify DeliveryReceipt records exist
- Review logs for error patterns
- Increase
max_attemptsif transient errors
Memory Issues
Problem: Oban consuming too much memory.
Solutions:
- Reduce queue concurrency
- Enable pruner plugin to remove old jobs
- Limit job retention time
Next Steps
- Getting Started - Set up your first event
- Configuration - Complete configuration reference