# `PhoenixKit.ObanQueues`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.32.1/lib/phoenix_kit/oban_queues.ex#L1)

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.

# `conflict`

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

Two declarations of one queue that disagree on the limit.

# `spec`

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

One declared queue.

# `core_queues`

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

Core's own queues.

# `declared`

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

The resolved queues only — see `resolve/1`.

# `describe_conflict`

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

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

# `legacy_module_queues`

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

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

# `missing`

```elixir
@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`

```elixir
@spec required([spec()], (atom() -&gt; 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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
