PhoenixKit.Install.ObanConfig (phoenix_kit v2.60.2)

Copy Markdown View Source

Handles Oban configuration for PhoenixKit installation.

This module provides functionality to:

  • Configure Oban for background job processing
  • Set up required queues (default, file_processing)
  • Add Oban.Plugins.Pruner for job cleanup
  • Add Oban to application supervisor tree
  • Ensure configuration exists during updates

Summary

Functions

Adds or verifies Oban configuration.

Adds Oban to the parent application's supervision tree.

Ensures the crontab schedules ProcessScheduledJobsWorker.

Adds every declared queue (PhoenixKit.ObanQueues.declared/1) that a host's existing Oban block is missing.

Ensures every notification digest cadence has a crontab entry.

Ensure the Lifeline plugin exists in an existing config :app, Oban block's plugins: list, adding it if missing.

Ensures queue: limit exists in a host's existing Oban queues: list.

Ensures the scheduled_jobs queue exists in a host's existing Oban config.

Ensures the plain worker cron entries shipped since a host's install exist.

How many manual steps have been recorded so far, without taking them — for a message that must not claim a clean update while some are pending.

The closing block for take_manual_steps/0's result — printed LAST by the update, after the migration and asset output that would otherwise bury the one-line warnings. "" when there is nothing to do by hand.

Whether the given config content has a config :app, Oban block that lacks a prefix: key.

Checks if Oban configuration exists in config.exs.

Checks if Oban supervisor is configured in application.ex.

The entries this run left out because the host commented them out, in the order met — and forgets them. Shown by manual_steps_summary/2.

The manual steps the config editing of this run could not do itself, oldest first, as {headline, lines} — and forgets them. The update task prints them again as one closing block.

The whole backfill of an existing Oban block, on content: queues, cron plugin and entries, Pruner retention, Lifeline. Public so it can be tested against content strings.

Functions

add_oban_configuration(igniter, prefix \\ nil)

Adds or verifies Oban configuration.

This function ensures that Oban is properly configured for PhoenixKit's background job processing, including:

  1. Repo configuration (auto-detected from PhoenixKit config)
  2. Required queues for file processing
  3. Pruner plugin for automatic job cleanup

Parameters

  • igniter - The igniter context

Returns

Updated igniter with Oban configuration and notices.

add_oban_supervisor(igniter)

Adds Oban to the parent application's supervision tree.

This function ensures that Oban starts automatically when the application starts, with correct positioning in the supervisor tree:

  • AFTER PhoenixKit.Supervisor (PhoenixKit services available)
  • BEFORE Endpoint (Oban ready before HTTP requests)

Important

Oban MUST start AFTER PhoenixKit.Supervisor because PhoenixKit.Supervisor depends on Repo, and Oban also depends on Repo. The correct order is:

  1. Repo (database connection)
  2. PhoenixKit.Supervisor (uses Repo for Settings)
  3. Oban (uses Repo for job persistence)

Parameters

  • igniter - The igniter context

Returns

Updated igniter with Oban added to application supervisor.

ensure_cron_plugin(content, app_name)

@spec ensure_cron_plugin(String.t(), atom() | String.t()) :: String.t()

Ensures the crontab schedules ProcessScheduledJobsWorker.

Public for the same reason as ensure_worker_cron_entries/2: so it can be unit-tested directly against content strings.

ensure_declared_queues(content, app_name, declared \\ nil)

@spec ensure_declared_queues(String.t(), atom() | String.t(), [map()] | nil) ::
  String.t()

Adds every declared queue (PhoenixKit.ObanQueues.declared/1) that a host's existing Oban block is missing.

A block that runs no queues — queues: false or queues: [], a web-only node — is left exactly as it is, with one line saying so, rather than a "please add manually" error per queue. Conflicting declarations are reported, and the first one wins (PhoenixKit's own, then modules in a stable order).

Public so it can be unit-tested against content strings; declared is injectable for the same reason.

ensure_digest_cron_entries(content, app_name)

@spec ensure_digest_cron_entries(String.t(), atom() | String.t()) :: String.t()

Ensures every notification digest cadence has a crontab entry.

Runs AFTER ensure_cron_plugin/2 (which guarantees a crontab: block exists) and is needed because that function short-circuits as soon as ProcessScheduledJobsWorker is present — so a host installed before the digest workers existed would keep a crontab without them forever, and mix phoenix_kit.update would never notice. Each cadence is checked independently, so a partially-updated crontab converges.

Public (not defp, unlike the sibling ensure_*_queue/2 helpers) specifically so this can be unit-tested directly against plain content strings, the same way ensure_lifeline_plugin/2 is.

ensure_lifeline_plugin(content, app_name)

@spec ensure_lifeline_plugin(String.t(), atom() | String.t()) :: String.t()

Ensure the Lifeline plugin exists in an existing config :app, Oban block's plugins: list, adding it if missing.

Rescues a job orphaned in :executing by a hard crash (BEAM kill -9, OOM, node failure) back to :available so it runs again — without it, an orphaned job sits stuck in :executing forever. That's more than a stalled retry: for a unique worker whose unique states: includes :executing (a self-scheduling chain deduping against its own in-flight run is a common pattern — see phoenix_kit_emails' pollers), an orphan permanently blocks every future insert for that worker too, not just the one crashed job.

rescue_after is Oban's default of 60 minutes rather than anything more aggressive, and it must stay above the host's longest-running job. Lifeline rescues purely by elapsed time — it never checks whether the node is still alive — so a job that legitimately runs past rescue_after is flipped back to :available (or :discarded, if its attempts are exhausted) while the original process is still working, and re-executes concurrently. PhoenixKit's longest declared worker timeout is 30 minutes (Storage.Jobs.Reconcile); workers with no timeout/1 callback have no bound at all, which is the case the margin is really protecting.

Public (not defp, unlike the sibling ensure_*_queue/2 helpers) specifically so this can be unit-tested directly against plain content strings, the same way oban_block_missing_prefix?/1 is — no live Igniter/Mix context needed.

ensure_queue(content, app_name, queue, limit)

@spec ensure_queue(
  String.t(),
  atom() | String.t(),
  String.t() | atom(),
  pos_integer()
) :: String.t()

Ensures queue: limit exists in a host's existing Oban queues: list.

The shared implementation behind every ensure_*_queue/2. It was written for scheduled_jobs and generalised afterwards, because the six sibling helpers that predated it had each hand-rolled the same string surgery and reproduced the same six defects — and theirs are worse than the missing queue this repairs: a bad insert corrupts the host's config.exs.

Public so it can be unit-tested directly against content strings.

ensure_scheduled_jobs_queue(content, app_name)

@spec ensure_scheduled_jobs_queue(String.t(), atom() | String.t()) :: String.t()

Ensures the scheduled_jobs queue exists in a host's existing Oban config.

Public for the same reason as ensure_worker_cron_entries/2: so it can be unit-tested directly against content strings.

ensure_worker_cron_entries(content, app_name)

@spec ensure_worker_cron_entries(String.t(), atom() | String.t()) :: String.t()

Ensures the plain worker cron entries shipped since a host's install exist.

Public for the same reason as ensure_digest_cron_entries/2: so it can be unit-tested directly against content strings.

Declining an entry

An entry that shows up in the crontab only inside a comment — # {"30 4 * * *", PhoenixKit.Users.Referrals.PruneWorker} — is declined: the updater leaves it out, says so, and never offers it again. Commenting the line out is what a host does anyway to switch an entry off while keeping a record of it, so the opt-out needs no new setting to learn; and a config key would have to be read from the very file being edited. The same applies to the digest entries. (A plain contains? has always behaved this way; it is now deliberate, announced, and written into the manual-step text.) Deleting the line is not a refusal — the updater cannot tell it from a crontab that predates the entry.

manual_step_count()

@spec manual_step_count() :: non_neg_integer()

How many manual steps have been recorded so far, without taking them — for a message that must not claim a clean update while some are pending.

manual_steps_summary(steps, declined \\ [])

@spec manual_steps_summary([{String.t(), [String.t()]}], [String.t()]) :: String.t()

The closing block for take_manual_steps/0's result — printed LAST by the update, after the migration and asset output that would otherwise bury the one-line warnings. "" when there is nothing to do by hand.

The exit code stays 0 on purpose: the app boots and the schema is current; what is missing is a cron entry (a recovery sweep, a prune), i.e. a feature that is off, not a mismatch that breaks the host — unlike a pending migration, which does fail the task. A non-zero exit after a finished migration also makes a deploy script treat a completed update as failed.

oban_block_missing_prefix?(content)

@spec oban_block_missing_prefix?(String.t()) :: boolean()

Whether the given config content has a config :app, Oban block that lacks a prefix: key.

Scoped to the Oban block on purpose: a whole-file scan is defeated by the config :phoenix_kit, prefix: "..." entry (which matches a naive prefix: grep) and false-positives on unrelated config. Any prefix: inside the block counts — including computed values like System.get_env(...). Returns false when the content has no Oban block at all (nothing to judge).

oban_config_exists?(igniter)

Checks if Oban configuration exists in config.exs.

Parameters

  • igniter - The igniter context for detecting parent app name

Returns

Boolean indicating if configuration exists.

oban_supervisor_exists?(igniter)

Checks if Oban supervisor is configured in application.ex.

Parameters

  • igniter - The igniter context for detecting parent app name

Returns

Boolean indicating if Oban supervisor exists in application.ex.

take_declined()

@spec take_declined() :: [String.t()]

The entries this run left out because the host commented them out, in the order met — and forgets them. Shown by manual_steps_summary/2.

take_manual_steps()

@spec take_manual_steps() :: [{String.t(), [String.t()]}]

The manual steps the config editing of this run could not do itself, oldest first, as {headline, lines} — and forgets them. The update task prints them again as one closing block.

update_content(content, app_name)

@spec update_content(String.t(), atom() | String.t()) :: String.t()

The whole backfill of an existing Oban block, on content: queues, cron plugin and entries, Pruner retention, Lifeline. Public so it can be tested against content strings.

A block the host switched off is not a to-do list: config :app, Oban, false ends the phase with one line, and plugins: false skips every plugin step with one line (queues are still checked).