Releases: oban-bg/oban
v2.20.0
This release brings a fantastic new helper function, an optional migration to aid pruning, some stability improvements, and a bevy of documentation updates.
🦋 Update Job
This introduces the Oban.update_job/2,3 function to simplify updating existing jobs while ensuring data consistency and safety. Previously, updating jobs required manually constructing change operations or complex queries that could lead to race conditions or invalid state changes.
Only a curated subset of job fields, e.g. :args, :max_attempts, :meta, etc. may be updated and they use the same validation rules as insertion to prevent invalid data. Updates are also wrapped in a transaction with locking clauses to prevent concurrent modifications.
The function supports direct map changes:
Oban.update_job(job, %{priority: 0, tags: ["urgent"]})It also has a convenient function-based mode for dynamic changes:
Oban.update_job(job, fn job ->
%{meta: Map.put(job.meta, "processed_by", current_node())}
end)❄️ Unique State Groups
There are now named unique state groups to replace custom state lists for unique jobs, promoting better uniqueness design and reducing configuration errors.
Previously, developers had to manually specify lists of job states for uniqueness, which was error-prone and could lead to subtle bugs when states were omitted or incorrectly combined. The new predefined groups ensure correctness and consistency across applications.
The new state groups are:
:all- All job states:incomplete- Jobs that haven't finished (~w(available scheduled executing retryable)a):scheduled- Only scheduled jobs ([:scheduled]):successful- Jobs that completed successfully (~w(available scheduled executing retryable completed)a)
These groups eliminate the risk of accidentally creating incomplete or incorrect state lists that could allow duplicate jobs to be created when they shouldn't be, or prevent valid job creation when duplicates should be allowed.
🪺 Nested Plugin Supervision
Plugins and the internal Stager are now nested within a secondary supervision tree to improve system resilience and stability.
Previously, plugins were supervised directly under the main Oban supervisor alongside core process. This meant that plugin failures could potentially impact the entire Oban system, and frequent plugin restarts could trigger cascading failures in the primary supervision tree.
The new supervisor has more lenient restart limits to allow for more plugin restart attempts before giving up. This change makes Oban more robust in production environments where plugins may experience transient failures due to database or connectivity issues.
v2.20.0 — 2025-08-13
Enhancements
-
MigrationAdd V13 migration for indexing cancelled and discarded states.A new V13 migration adds compound indexes to significantly improve
Oban.Plugins.Prunerperformance when cleaning updiscardedandcancelledjobs. This is especially beneficial for applications that process large volumes of jobs and retain them for extended periods. -
RepoExpose dynamic repo switching aswith_dynamic_repo/2The function was previously internal, which made impossible to use in external modules or extend upon. Now custom plugins and extensions can use
Repo.with_dynamic_repo/2to use the configured dynamic repo options.
Bug Fixes
-
[Oban] Allow
insert_all/1,3via Oban facadeThe
insert_all/1andinsert_all/3function variants were missing from the generated Oban facade functions when using a named instance. -
[Testing] Generate correct
perform_job/1,2,3clauses.The
perform_job/2,3clauses generated byuse Oban.Testingdidn't handle theperform_job/2variant designed to run jobs created withbuild_job/3. This caused test failures when trying to execute jobs built using thebuild_job/3helper function.The fix generates the missing
perform_job/2clause along with a convenientperform_job/1variant, ensuring all testing scenarios work seamlessly regardless of how jobs are constructed. -
[Testing] Restrict inline execution to
availableandscheduledstates.Jobs in the
completedstate or other non-runnable states were incorrectly attempted by the inline engine, potentially causing errors or unexpected behavior during testing. -
[Worker] Disallow
:keyswhen:fieldsdoesn't contain:argsor:metaUnique job configurations using
:keyswere allowed even when:fieldsdidn't include:argsor:meta, which would result in runtime errors since keys can only extract values from these keyable fields. -
[Cron] Fix error message when the crontab has an invalid range.
Cron validation errors for invalid ranges were returning exception structs instead of readable error messages, making it difficult to understand and fix crontab configuration issues.
v2.19.2
Enhancements
-
[Oban] Allow setting a MFA in
:get_dynamic_repoAnonymous functions don't work with OTP releases, as anonymous functions cannot be used in configuration. Now a MFA tuple can be passed instead of a fun, and the scaling guide recommends a function instead.
-
[Cron] Include configured timezone in cron job metadata
Along with the cron expression, stored as
cron_expr, the configured timezone is also recorded ascron_tzin cron job metadata. -
[Cron] Add
next_at/2andlast_at/2for cron time calculationsThis implements jumping functions for cron expressions. Rather than naively iterating through minutes, it uses the expression values to efficiently jump to the next or last cron run time.
-
[Executor] Always convert
queue_timeto native time unitThe telemetry docs state that measurements are recorded in
nativetime units. However, that hasn't been the case forqueue_timefor a while now. It usually worked anyway native and nanosecond is of the same resolution, but now it is guaranteed.
Bug Fixes
-
[Peer] Correct leadership elections for the
DolphinengineMySQL always returns the number of entries attempted, even when nothing was added. The previous match caused all nodes to believe they were the leader. This uses a secondary query within the same transaction to detect if the current instance is the leader.
-
[Reindexer] Drop invalid indexes concurrently when reindexing.
The
DROP INDEXquery would lock the whole table with anACCESS EXCLUSIVElock and could cause queries to fail unexpectedly. -
[Testing] Use
Ecto.Type.cast/2for backward compatibilityThe
cast!/2function wasn't added until Ecto 3.12. This reverts time casting to usecast/2for compatibility with earlier Ecto versions. -
[Worker] Validate that the
uniqueoption isn't an empty list.An empty list was accepted at compile time, but wouldn't be valid later at runtime. Now the two validations match for greater parity.
v2.19.1
Bug Fixes
-
[Mix] Improve igniter installer idempotency and compatibility.
The installer now uses
on_exists: :skipwhen generating a migration, so it composes safely with other igniter installers. It also removes unnecessaryadd_depcalls that would overwrite a previously specified Oban version with~> 2.18.
v2.19
The minimum Elixir version is now v1.15. The official policy is to only support the three latest versions of Elixir.
🐬 MySQL Support
Oban officially supports MySQL with the new Dolphin engine. Oban supports modern (read "with full JSON support") MySQL versions from 8.4 on, and has been tested on the highly scalable Plantescale database.
Running on MySQL is as simple as specifying the Dolphin engine in your configuration:
config :my_app, Oban,
engine: Oban.Engines.Dolphin,
queues: [default: 10],
repo: MyApp.RepoWith this addition, Oban can run in estimated 10% more Elixir applications!
⚗️ Automated Installer
Installing Oban into a new application is simplified with a new igniter powered mix task. The new oban.install task handles installing and configuring a standard Oban installation, and it will deduce the correct engine and notifier automatically based on the database adapter.
mix igniter.install obanThis oban.install task is currently the recommended way to install Oban. As a bonus, the task composes together with other igniter installers, making it possible to install phoenix, ash, oban, and other packages with a single command:
mix igniter.install phoenix ash_phoenix ash_postgres ash_obanLook at the Mix.Oban.Install docs for full usage and options.
📔 Logging Enhancements
Logging in a busy system may be noisy due to job events, but there are other events that are particularly useful for diagnosing issues. A new events option for attach_default_logger/1 allows selective event logging, so it's possible to receive important notices such as notifier connectivity issues, without logging all job activity:
Oban.Telemetry.attach_default_logger(events: ~w(notifier peer stager)a)Along with filtering, there are new events to make diagnosing operational problems easier.
A peer:election events logs leadership changes to indicate when nodes gain or lose leadership. Leadership issues are rare, but insidious, and make diagnosing production problems especially tricky.
[
message: "peer became leader",
source: "oban",
event: "peer:election",
node: "worker.1",
leader: true,
was_leader: false
]Helpfully, plugin:stop events are now logged for all core plugins via an optional callback, and plugin:exception events are logged for all plugins regardless of whether they implement the callback. Runtime information is logged for Cron, Lifeline, Pruner, Stager, and Reindexer plugins.
For example, every time Cron runs successfully it will output details about the execution time and all of the inserted job ids:
[
source: "oban",
duration: 103,
event: "plugin:stop",
plugin: "Oban.Plugins.Cron",
jobs: [1, 2, 3]
]⛵️ Official JSON
Oban will default to using the official JSON module built into Elixir v1.18+ when available.
A new Oban.JSON module detects whether the official Elixir JSON module is available at compile time. If it isn't available, then it falls back to Jason, and if Jason isn't available (which is extremely rare) then it warns about a missing module.
This approach was chosen over a config option for backward compatibility because Oban will only support the JSON module once the minimum supported Elixir version is v1.18.
v2.19.0 — 2025-01-16
Enhancements
-
[Oban] Start all queues in parallel on initialization.
The midwife now starts queues using an async stream to parallelize startup and minimize boot time for applications with many queues. Previously,
-
[Oban] Safely return
nilfromcheck_queue/2when checking queues that aren't running.Checking on a queue that wasn't currently running on the local node now returns
nilrather than causing a crash. This makes it safer to check the whether a queue is running at all without atry/catchclause. -
[Oban] Add
check_all_queues/1to gather all queue status in a single function.This new helper gathers the "check" details from all running queues on the local node. While it was previously possible to pull the queues list from config and call
check_queue/2on each entry, this more accurately pulls from the registry and checks each producer concurrently. -
[Oban] Add
delete_job/2anddelete_all_jobs/2operations.This adds
Oban.delete_job/2,Oban.delete_all_jobs/2, Engine callbacks, and associated operations for all native engines. Deleting jobs is now easier and safer, due to automatic state protections. -
[Engine] Record when a queue starts shutting down
Queue producer metadata now includes a
shutdown_started_atfield to indicate that a queue isn't just paused, but is actually shutting down as well. -
[Engine] Add
rescue_jobs/3callback for all engines.The
Lifelineplugin formerly used two queries to rescue jobs—one to mark jobs with remaining attempts asavailableand another thatdiscardedthe remaining stuck jobs. Those are now combined into a single callback, with the base definition in theBasicengine.MySQL won't accept a select in an update statement. The Dolphin implementation of
rescue_jobs/3uses multiple queries to return the relevant telemetry data and make multiple updates. -
[Cron] Introduce
Oban.Cronwithschedule_interval/4The new
Cronmodule allows processes, namely plugins, to get cron-like scheduled functionality with a single function call. This will allow plugins to removes boilerplate around parsing, scheduling, and evaluating for cron behavior. -
[Registry] Add
select/1to simplify querying for registered modules. -
[Testing] Add
build_job/3helper for easier testing.Extract the mechanism for verifying and building jobs out of
perform_job/3so that it's usable in isolation. This also introducesperform_job/2for executing built jobs. -
[Telemetry] Add information on leadership changes to
oban.peer.electionevent.An additional
was_leader?field is included in[:oban, :peer, :election | _]event metadata to make hooking into leadership change events simpler. -
[Telemetry] Add callback powered logging for plugin events.
Events are now logged for plugins that implement the a new optional callback, and exceptions are logged for all plugins regardless of whether they implement the callback.
This adds logging for
Cron,Lifeline,Pruner,Stager, andReindexer. -
[Telemetry] Add peer election logging to default logger.
The default logger now includes leadership events to make identifying the leader, and leadership changes between nodes, easier.
-
[Telemetry] Add option to restrict logging to certain events.
Logging in a busy system may be noisy due to job events, but there are other events that are particularly useful for diagnosing issues. This adds an
eventsoption toattach_default_logger/1to allow selective event logging. -
[Telemetry] Expose
default_handler_id/0for telemetry testing.Simplifies testing whether the default logger is attached or detached in application code.
Chores
- [Peer] The default database-backed peer was renamed from
PostgrestoDatabasebecause it is also used for MySQL databases.
Bug Fixes
-
[Oban] Allow overwriting all
insert/*functions arities afteruse Oban. -
[Node] Correctly handle
:nodeoption forscale_queue/2Scoping
scale_queue/2calls to a single node didn't work as advertised due to some extra validation for producer meta compatibility. -
[Migration] Fix version query for databases with non-unique
oidUse
pg_catalog.obj_description(object_oid, catalog_name), introduced in PostgreSQL 7.2, to specify thepg_classcatalog so only theoban_jobsdescription is returned. -
[Pruner] Use state specific fields when querying for prunable jobs.
Using
scheduled_atis not correct in all situations. Depending on job state, one ofcancelled_at,discarded_at, orscheduled_atshould be used. -
[Peer] Conditionally return the current node as leader for isolated peers.
Prevents returning the current node name when leadership is disabled.
-
[Testing] Retain time as microseconds for
scheduled_attests.Include microseconds in the
beginanduntiltimes used for scheduled_at tests with a delta. The prior version would truncate, which rounded theuntildown and broke microsecond level checks. -
[Telemetry] Correct spelling of "elapsed" in
oban.queue.shutdownmetadata.
v2.18.3
Enhancements
-
[Basic] Use the shared concat operator when appending errors.
The standard
pushoperation for updates is designed for arrays and usesarray_appendinternally. This replaces all use ofpushwith a fragment that uses the||operator instead, which works for both arrays and jsonb.CockroachDB doesn't support arrays of jsonb, but they do support simple jsonb columns. Now we can append to the errors column in either format for CRDB compatibility.
Bug Fixes
-
[Queue] Link the dynamic queue supervisor and
Midwifefor automatic restarts.When a producer crashes it brings the queue's supervisor down with it. With enough database errors, the producer may crash repeatedly enough to exhaust restarts and bring down the DynamicSupervisor in charge of all queues.
Now the supervisor is linked to the midwife to ensure that the midwife restarts as well, and it restarts all of the queues.
-
[Testing] Handle
insert_all/3with streams for the:inlinetesting engine.The inline engine's
insert_all_jobscallback incorrectly expected changesets to always be a list rather and couldn't handle streams.
v2.18.2
Bug Fixes
-
[Repo] Prevent debug noise by ensuring default opts for standard transactions.
Without default opts each transaction is logged. Many standard operations execute each second, which makes for noisy logs. Now transaction opts are passed as a third argument to ensure defaults are applied.
-
[Repo] Increase transaction retry delay and increase with each attempt.
Bump the base transaction retry from 100ms to 500ms, and increase linearly between each successive attempt to provide deeper backoff. This alleviates pressure on smaller connection pools and gives more time to recover from contentions failures.
v2.18.1
Enhancements
-
[Repo] Automatically retry all transactions with backoff.
Avoid both expected an unexpected database errors by automatically retrying transactions. Some operations, such as serialization and lock not available errors, are likely to occur during standard use depending on how a database is configured. Other errors happen infrequently due to pool contention or flickering connections, and those should also be retried for increased safety.
This change is applied to
Oban.Repo.transaction/3itself, so it will apply to every location that uses transactions. -
[Migration] Declare
tagsas an array oftextrather thanvarchar.We don't provide a limit on the size of tags and they could conceivably be larger than 256 characters. Externally the types are interchangeable, but internally there are minor advantages to using the text type.
There isn't a new migration; this change is only for new tables.
Bug Fixes
- [Repo] Correctly dispatch
query!/4toquery!rather thanquerywithout a bang.
v2.18.0
🔭 Queue Shutdown Telemetry
A new queue shutdown event, [:oban, :queue, :shutdown], is emitted by each queue when it terminates. The event originates from the watchman process, which tracks the total ellapsed time from when termination starts to when all jobs complete or the allotted period is exhausted.
Any jobs that take longer than the :shutdown_grace_period (by default 15 seconds) are brutally killed and left as orphans. The ids of jobs left in an executing state are listed in the event's orphaned meta.
This also adds queue:shutdown logging to the default logger. Only queues that shutdown with orphaned jobs are logged, which makes it easier to detect orphaned jobs and which jobs were affected:
[
message: "jobs were orphaned because they didn't finish executing in the allotted time",
queue: "alpha",
source: "oban",
event: "queue:shutdown",
ellapsed: 500,
orphaned: [101, 102, 103]
]
🚚 Distributed PostgreSQL Support
It's now possible to run Oban in distributed PostgreSQL databases such as Yugabyte. This is made possible by a few simple changes to the Basic engine, and a new unlogged migration option.
Some PostgreSQL compatible databases don't support unlogged tables. Making oban_peers unlogged isn't a requirement for Oban to operate, so it can be disabled with a migration flag:
defmodule MyApp.Repo.Migrations.AddObanTables do
use Ecto.Migration
def up do
Oban.Migration.up(version: 12, unlogged: false)
end
end🧠 Job Observability
Job stop and exception telemetry now includes the reported memory and total reductions from the job's process. Values are pulled with Process.info/2 after the job executes and safely fall back to 0 in the event the process has crashed. Reductions are a rough proxy for CPU load, and the new measurements will make it easier to identify computationally expensive or memory hungry jobs.
In addition, thanks to the addition of Process.set_label in recent Elixir versions, the worker name is set as the job's process label. That makes it possible to identify which job is running in a pid via observer or live dashboard.
v2.18.0 — 2024-07-26
Enhancements
-
[Job] Support simple
unique: trueandunique: falsedeclarationsUniqueness can now be enabled with
unique: trueand disabled withunique: falsefrom job options or a worker definition. Theunique: trueoption uses all the standard defaults, but sets the period to:infinityfor compatibility with Oban Pro's newsimpleunique mode. -
[Cron] Remove forced uniqueness when inserting scheduled jobs.
Using uniqueness by default prevents being able to use the Cron plugin with databases that don't support uniqueness because of advisory locks. Luckily, uniqueness hasn't been necessary for safe cron insertion since leadership was introduced and scheduling changed to top-of-the-minute many versions ago.
-
[Engine] Introduce
check_available/1engine callbackThe
check_available/1callback allows engines to customize the query used to find jobs in theavailablestate. That makes it possible for alternative engines, such Oban Pro's Smart engine, to check for available jobs in a fraction of the time with large queues. -
[Peer] Add
Oban.Peer.get_leader/2for checking leadershipThe
get_leader/2function makes it possible to check which node is currently the leader regardless of the Peer implementation, and without having to query the database. -
[Producer] Log a warning for unhandled producer messages.
Some messages are falling through to the catch-all
handle_info/2clause. Previously, they were silently ignored and it degraded producer functionality because inactive jobs with dead pids were still tracked asrunningin the producer. -
[Oban] Use structured messages for most logger warnings.
A standard structure for warning logs makes it easier to search for errors or unhandled messages from Oban or a particular module.
Bug Fixes
-
[Job] Include all fields in the unique section of
Job.t/0.The unique spec lacked types for both
keysandtimestampkeys. -
[Basic] Remove
materializedoption fromfetch_jobs/3.The
MATERIALIZEDclause for CTEs didn't make a meaningful difference in job fetching accuracy. In some situations it caused a performance regression (which is why it was removed from Pro's Smart engine a while ago).
v2.17.11
Bug Fixes
-
[Oban] Handle deprecation warnings from Elixir 1.17
-
[Notifier] Prevent noisy logging about switching between modes.
There's an apparent race condition in Sonar between pruning stale nodes on
:pingand updating the status after a notification. This primarily happens in development for two reasons:- Development laptops are most prone to time warp because of system sleep.
- Apps only run a single node in development.
Using
monotonic_time/1instead ofsystem_time/1guards against clock drift/time warp effects. -
[Stager] Prevent notification status timeouts from bubbling into the Stager.
A clogged Ecto pool could cause cascading errors on startup due to a sequence of calls between the
Notifier,Sonar, andStager.Sonarsends a notification inhandle_continueon startup.- The notification is blocked while the
Notifierwaits for a connection from the Ecto pool. Stagerchecks for the connection status on startup, which would eventually time out because theSonarhadn't finished initializing.- The
Stagercrashes from the timeout error.
This makes the following changes to prevent this sequence of events:
- The
Stagerno longer gets the sonar status during startup. - The
Notifiercatches timeout errors fromSonarchecks, warns about it, then returns an:unknownstatus.
-
[Engine] Defensively check the process dictionary during inline testing.
Not all processes are guaranteed to return a value for the process dictionary. Sometimes a value was missing during inline testing, which would crash the test.
-
[Basic] Set
conflict?flag when encountering a unique advisory lock.The
conflict?flag wasn't set when inserting a unique job was blocked by an advisory lock. Now the flag is set on either a fetched duplicate, or when the advisory lock is set. -
[Job] Correct
replace_by_state_optiontype by switching from keyword to tuples. -
[Config] Correctly type
shutdown_grace_periodas anintegerrather than atimeout.
v2.17.10
Enhancements
-
[Oban] Make all generated functions from
use Obanoverridable.Now the functions generated by
use Obanare all marked withdefoverridablefor extensibility.
Bug Fixes
-
[Testing] Use
$callersrather than$ancestorsfor ancestry tree check.We care about Tasks for inline testing checks, not normal supervision tree ancestry. The
$callersentry is the appropriate mechanism to find the trail of calling processes: