Oban. Pro. Plugins. DynamicPartitioner
(Oban Pro v1.8.0-rc.1)
The DynamicPartitioner plugin manages a partitioned oban_jobs table for optimized query
performance, minimal database bloat, and efficiently pruned historic jobs.
Migrating Off
New applications shouldn't use partitioned tables. The complexity and edge cases introduced by
dynamic partitioning outweigh the benefits for most applications. To move an existing
application onto a standard oban_jobs table, see Migrating Off Partitioned Tables.
Migrating Off Partitioned Tables
A partitioned table can't be converted back in place, so moving off requires a transition stage. Both strategies are listed below in order of complexity, beginning with the least disruptive.
Strategy 1: Drain Into a Standard Table
The safest path runs two Oban instances side by side while the partitioned table drains.
Start by adding a standard oban_jobs table in a new prefix:
defmodule MyApp.Repo.Migrations.AddStandardObanJobs do
use Ecto.Migration
def up do
Oban.Migration.up(prefix: "standard")
Oban.Pro.Migration.up(prefix: "standard")
end
def down do
Oban.Pro.Migration.down(prefix: "standard")
Oban.Migration.down(prefix: "standard")
end
endThen point the primary instance at the new prefix and add a second instance that finishes processing jobs in the partitioned table, without any plugins, so it won't insert new jobs:
queues = [
default: 10,
other_queue: 10,
and_another: 10
]
config :my_app, Oban.Partitioned, queues: queues
config :my_app, Oban,
prefix: "standard",
queues: queues,
plugins: [
...Now, start both Oban instances within your application's supervisor:
children = [
MyApp.Repo,
{Oban, Application.fetch_env!(:my_app, Oban)},
+ {Oban, Application.fetch_env!(:my_app, Oban.Partitioned)},
...
]New jobs are inserted into the standard table while existing jobs keep processing through the
Oban.Partitioned instance. Once all of the partitioned jobs have executed you're free to
remove the extra instance, remove the plugin, and drop the partitioned table. Dropping the
parent cascades to every sub-partition:
defmodule MyApp.Repo.Migrations.DropPartitionedObanJobs do
use Ecto.Migration
def change do
drop_if_exists table(:oban_jobs)
drop_if_exists table(:oban_jobs_old)
end
endBe very careful to ensure you're dropping the partitioned table and not the standard one. Pass
a prefix if the partitioned table doesn't live in public.
Strategy 2: Revert the Migration
If the original table is still around as oban_jobs_old, reverting the partitioning migration
restores it in place:
defmodule MyApp.Repo.Migrations.RevertPartitionedObanJobs do
use Ecto.Migration
def change do
Oban.Pro.Migrations.DynamicPartitioner.down()
end
endThis drops the partitioned table along with every job in it, so it's only appropriate when those jobs are expendable and the original table is still current enough to use.
Configuration
Until you've migrated off, the plugin manages sub-partitions for the partitioned table:
config :my_app, Oban,
plugins: [Oban.Pro.Plugins.DynamicPartitioner]
...The plugin preemptively creates sub-partitions for finished job states (completed,
cancelled, discarded) as well as prunes partitions older than the retention period. By
default, older jobs are retained for 3 days.
You can override the retention period for states individually. For example, to retain completed
jobs for 2 days, cancelled for 7, and discarded for 30:
plugins: [{
Oban.Pro.Plugins.DynamicPartitioner,
retention: [completed: 2, cancelled: 7, discarded: 30]
}]Pruning sub-partitions is an extremely fast operation akin to dropping a table. As a result,
there is zero lingering bloat and a separate pruner isn't advised, unless you're pruning a subset
of jobs aggressively after a few minutes, hours, etc. DynamicPartitioner will warn you if the
standard Pruner is enabled at the same time.
The partitioner attempts once an hour to pre-create partitions two days in advance. That schedule and buffer should be suitable for most applications. However, you can increase the buffer period and set an alternate schedule if necessary.
For example, to increase the buffer to 3 days and run at 05:00 in the Europe/Paris timezone:
plugins: [{
Oban.Pro.Plugins.DynamicPartitioner,
buffer: 3,
schedule: "0 5 * * *",
timezone: "Europe/Paris"
}]Instrumenting with Telemetry
The DynamicPartitioner plugin adds the following metadata to the [:oban, :plugin, :stop] event:
:created_count— the number of partitions created:deleted_count- the number of partitions deleted
Summary
Functions
Backfill jobs from a standard table into a newly partitioned table.
Types
@type option() :: {:conf, Oban.Config.t()} | {:name, GenServer.name()} | {:retention, retention()} | {:schedule, String.t()} | {:timeout, timeout()} | {:timezone, String.t()}
@type retention() :: [ completed: pos_integer(), cancelled: pos_integer(), discarded: pos_integer() ]
Functions
@spec backfill_jobs( name_or_conf :: Oban.name() | Oban.Config.t(), opts :: Keyword.t() ) :: :ok
Backfill jobs from a standard table into a newly partitioned table.
Backfilling is flexible enough to run against one or more job states, with arbitrary batch sizes, and without transactional blocks. That allows repeated backfill runs in the face of restarts or database errors.
Sub-partitions by date are created for final states (completed, cancelled, discarded)
automatically before jobs are moved.
Options
:new_prefix— The prefix where the new partitionedoban_jobstable resides. Defaults topublic.:old_prefix— The prefix where the standardoban_jobs_oldtable resides. Defaults topublic.:batch_size— The number of jobs to move (delete/insert) in a single query. Defaults to a conservative 5,000 jobs per batch.:batch_sleep— The amount of time to sleep between backfill batches in order to minimize load on the database. Defaults to 0, no downtime between batches.:states— A list of job states to backfill jobs from. Defaults to all states.
Examples
Backfill old jobs across all states in the default public prefix:
DynamicPartitioner.backfill_jobs()Restrict backfilling to incomplete job states:
DynamicPartitioner.backfill_jobs(states: ~w(executing available scheduled retryable))Backfill to and from an alternate prefix:
DynamicPartitioner.backfill_jobs(old_prefix: "private", new_prefix: "private")Backfill using larger batches with half a second between queries:
DynamicPartitioner.backfill_jobs(batch_size: 20_000, batch_sleep: 500)