View Source Instance and Database Isolation
This guide will walk you through options for isolating Oban instances as well as Oban database tables.
Running Multiple Oban Instances
You can run multiple Oban instances with different prefixes on the same system and have them entirely isolated, provided you give each Oban supervisor a distinct name. You can do this in one of two ways: explicit names or facades.
Facades
You can create an Oban facade by defining a module that calls use Oban
:
defmodule MyApp.ObanA do
use Oban, otp_app: :my_app
end
defmodule MyApp.ObanB do
use Oban, otp_app: :my_app
end
Configure facades through the application environment, for example in config/config.exs
:
config :my_app, MyApp.ObanA, repo: MyAppo.Repo, prefix: "special"
config :my_app, MyApp.ObanB, repo: MyAppo.Repo, prefix: "private"
You can then start these facades in your application's supervision tree:
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
MyApp.ObanA,
MyApp.ObanB
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
Oban facades define all the functions that the Oban
module defines, so use the facade in place
of Oban
:
MyApp.ObanA.insert(MyApp.Worker.new(%{}))
Isolated Instances Via Names
Here we configure our application to start three Oban supervisors using the "public"
(default),
"special"
, and "private"
prefixes, respectively:
def start(_type, _args) do
children = [
MyApp.Repo,
{Oban, name: ObanA, repo: MyApp.Repo},
{Oban, name: ObanB, repo: MyApp.Repo, prefix: "special"},
{Oban, name: ObanC, repo: MyApp.Repo, prefix: "private"}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
When you do this, you'll have to use the correct Oban supervisor name when performing Oban-related
operations. You'll see that most functions in the Oban
module, for example, take an optional
first argument which represents the name of the Oban supervisor. By default, that's Oban
, which
is why this works if you don't explicitly start an Oban supervisor in your application:
Oban.insert(MyApp.Worker.new(%{}))
In the example above, with ObanA
/ObanB
/ObanC
, you can specify which Oban instance you want
to use for scheduling by passing its name in:
Oban.insert(ObanB, MyApp.Worker.new(%{}))
Umbrella Apps
If you need to run Oban from an umbrella application where more than one of the child apps need to
interact with Oban, you may need to set the :name
for each child application that configures
Oban.
For example, your umbrella contains two apps: MyAppA
and MyAppB
. MyAppA
is responsible for
inserting jobs, while only MyAppB
actually runs any queues.
Configure Oban with a custom name for MyAppA
:
config :my_app_a, Oban,
name: MyAppA.Oban,
repo: MyApp.Repo
Then configure Oban for MyAppB
with a different name and different options:
config :my_app_b, Oban,
name: MyAppB.Oban,
repo: MyApp.Repo,
queues: [default: 10]
Now, use the configured name when calling functions like Oban.insert/2
, Oban.insert_all/2
,
Oban.drain_queue/2
, and so on, to reference the correct Oban process for the current
application.
Oban.insert(MyAppA.Oban, MyWorker.new(%{}))
Oban.insert_all(MyAppB.Oban, multi, :multiname, [MyWorker.new(%{})])
Oban.drain_queue(MyAppB.Oban, queue: :default)
Database Isolation
Let's look at a few options for isolating or scoping Oban database queries.
Database Prefixes
Oban supports namespacing through PostgreSQL schemas, also called "prefixes" in Ecto. With
prefixes, your job table can reside outside of your primary schema (usually public
) and you can
have multiple separate job tables.
To use a prefix you first have to specify it within your migration:
defmodule MyApp.Repo.Migrations.AddPrefixedObanJobsTable do
use Ecto.Migration
def up do
Oban.Migrations.up(prefix: "private")
end
def down do
Oban.Migrations.down(prefix: "private")
end
end
The migration will create the private
schema and all Oban-related tables within that schema.
With the database migrated, you'll then specify the prefix in your configuration:
config :my_app, Oban,
prefix: "private",
repo: MyApp.Repo,
queues: [default: 10]
Now all jobs are inserted and executed using the private.oban_jobs
table. Note that while
Oban.insert/2,4
will write jobs in the private.oban_jobs
table automatically, you'll need to
specify a prefix manually if you insert jobs directly through a repo.
Not only is the oban_jobs
table isolated within the schema, but all notification events are also
isolated. That means that insert/update events will only dispatch new jobs for their prefix.
Dynamic Repositories
Oban supports Ecto dynamic repositories through the :get_dynamic_repo
option. To make
this work, you need to run a separate Oban instance for each dynamic repo instance. Most often
it's worth bundling each Oban and repo instance under the same supervisor:
def start_repo_and_oban(instance_id) do
children = [
{MyDynamicRepo, name: nil, url: repo_url(instance_id)},
{Oban, name: instance_id, get_dynamic_repo: fn -> repo_pid(instance_id) end}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
The function repo_pid/1
in this example must return the PID of the repo for the given instance.
You can use Registry
to register the repo (for example in the repo's init/2
callback) and
discover it.
If your application exclusively uses dynamic repositories and doesn't specify all credentials
upfront, you must implement a init/1
callback in your Ecto repo. Doing so provides the Postgres
notifier with the correct credentials on initialization, allowing jobs to process as expected.
Ecto Multi-tenancy
If you followed the Ecto guide on setting up multi-tenancy with foreign keys, you need to add an
exception for queries originating from Oban. All of Oban's queries have the custom option oban: true
to help you identify them in prepare_query/3
or other instrumentation:
# Sample code, only relevant if you followed the Ecto guide on multi tenancy with foreign keys.
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
require Ecto.Query
@impl true
def prepare_query(_operation, query, opts) do
cond do
opts[:skip_org_id] || opts[:schema_migration] || opts[:oban] ->
{query, opts}
org_id = opts[:org_id] ->
{Ecto.Query.where(query, org_id: ^org_id), opts}
true ->
raise "expected org_id or skip_org_id to be set"
end
end
end