PhoenixKit.ObanQueues (phoenix_kit v2.28.1)

Copy Markdown View Source

The Oban queues PhoenixKit and its modules need, declared where the work is defined rather than hand-copied into a host's config.

Why

Oban only runs jobs from queues the node lists. A job inserted into a queue nothing runs sits available forever, and the feature looks fine until someone counts the rows. Until now the list lived in two places in core — the installer's block and the updater's backfill — which had already drifted (the updater wrote shop_imports: 2 while the module's own docs asked for 5), and a module had no way to declare a queue at all, so its install docs told the host to edit config.exs by hand and its workers ended up sharing default.

How it fits together

  • A module declares its queues with the optional oban_queues/0 callback of PhoenixKit.Module. Core declares its own here (core_queues/0), so there is one mechanism, not a module mechanism beside a hardcoded list.
  • mix phoenix_kit.install writes every declared queue into a new Oban block; mix phoenix_kit.update adds the ones an existing block is missing. Neither ever changes a limit the host already has — a number is the module author's suggestion until it lands in config.exs, and the host's from then on. A node that runs no queues (queues: false or []) is left alone.
  • mix phoenix_kit.doctor reports declared queues the running config is missing, and modules that declare one queue with different limits. The application logs the same at boot, once.

Queues are per node: a limit is how many jobs of that queue one node runs at a time. Split work by what it is — interactive work a person is waiting for, batch work nobody watches — rather than one queue per package, so a 400-item import cannot hold up an image someone just asked for.

Declaring

@impl PhoenixKit.Module
def oban_queues do
  [
    image_generation: [limit: 3, kind: :interactive],
    catalogue_import: 2
  ]
end

A bare integer is the limit. :kind (:interactive or :batch) is advisory — it explains the number in the doctor's output and the generated config, and changes nothing at runtime.

Summary

Types

Two declarations of one queue that disagree on the limit.

One declared queue.

Functions

Core's own queues.

The resolved queues only — see resolve/1.

One line describing a conflict, for logs and the doctor.

The fallback entries kept for modules that do not declare their queues yet.

The declared queues a node's Oban config does not list.

The declared queues a node must actually run: declared without the fallback entries whose package is not installed. Warning that no one runs catalogue_pdf on a site without the catalogue would send its owner off to configure a queue nothing can enqueue into.

Every queue to configure: core's, then each module's, then the legacy fallbacks no module has taken over — one entry per name.

Logs, once per boot, any declared queue the running Oban instance does not run. Best-effort by design: it runs after the host's supervision tree has had time to start Oban, reads only what Oban reports, and never raises.

Types

conflict()

@type conflict() :: %{name: atom(), kept: spec(), ignored: spec()}

Two declarations of one queue that disagree on the limit.

spec()

@type spec() :: %{
  name: atom(),
  limit: pos_integer(),
  kind: :interactive | :batch | nil,
  owner: module() | :core | :legacy
}

One declared queue.

Functions

core_queues()

@spec core_queues() :: [spec()]

Core's own queues.

declared(modules \\ nil)

@spec declared([module()] | nil) :: [spec()]

The resolved queues only — see resolve/1.

describe_conflict(map)

@spec describe_conflict(conflict()) :: String.t()

One line describing a conflict, for logs and the doctor.

legacy_module_queues()

@spec legacy_module_queues() :: [spec()]

The fallback entries kept for modules that do not declare their queues yet.

missing(oban_config, declared)

@spec missing(keyword() | nil, [spec()]) :: [spec()]

The declared queues a node's Oban config does not list.

oban_config is the keyword list given to Oban. A node that runs no queues (queues: false or []) is missing nothing — it is a web-only node by choice — and so is one whose config is not a keyword list this function can read (a host building it at runtime from environment variables).

required(declared, installed? \\ &app_installed?/1)

@spec required([spec()], (atom() -> boolean())) :: [spec()]

The declared queues a node must actually run: declared without the fallback entries whose package is not installed. Warning that no one runs catalogue_pdf on a site without the catalogue would send its owner off to configure a queue nothing can enqueue into.

installed? answers for an OTP application; it defaults to whether the application can be loaded — whether it is on the code path, as every dependency of the host is, whether or not it has started yet.

resolve(modules \\ nil)

@spec resolve([module()] | nil) :: {[spec()], [conflict()]}

Every queue to configure: core's, then each module's, then the legacy fallbacks no module has taken over — one entry per name.

modules defaults to the registered modules when the registry is running, and to a .beam scan otherwise (the install and update tasks run before it). Returns the resolved list and any conflicts found on the way.

warn_about_missing_queues(opts \\ [])

@spec warn_about_missing_queues(keyword()) :: :ok

Logs, once per boot, any declared queue the running Oban instance does not run. Best-effort by design: it runs after the host's supervision tree has had time to start Oban, reads only what Oban reports, and never raises.