Skip to content

Instantly share code, notes, and snippets.

@milmazz
Created October 14, 2023 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milmazz/94dd111ac040771aa5842bb84b922f85 to your computer and use it in GitHub Desktop.
Save milmazz/94dd111ac040771aa5842bb84b922f85 to your computer and use it in GitHub Desktop.
ExDocJSON

Let's try this formatter for the first time.

  1. Installing the package.
diff --git a/mix.exs b/mix.exs
index 2da2204..c46129a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -165,6 +165,7 @@ defmodule Oban.MixProject do
       {:credo, "~> 1.6", only: [:test, :dev], runtime: false},
       {:dialyxir, "~> 1.0", only: [:test, :dev], runtime: false},
       {:ex_doc, "~> 0.28", only: [:test, :dev], runtime: false},
+      {:ex_doc_json, "~> 0.1.0", only: [:test, :dev], runtime: false},
       {:makeup_diff, "~> 0.1", only: [:test, :dev], runtime: false}
     ]
   end
  1. Update your dependencies (mix deps.get)
  2. generate your docs using:
$ mix docs --formatter json                                                                                                                                     (base)
==> ex_doc_json
Compiling 3 files (.ex)
Generated ex_doc_json app
==> oban
Generated oban app
Generating docs...
View "json" docs at "doc/oban.json"
  1. Result: cat doc/oban.json | python -m json.tool | pbcopy
  2. Note that in future version I could add/remove more fields. Check or contribute at: https://github.com/milmazz/ex_doc_json
{
    "about": "ExDoc/version/1",
    "description": "Robust job processing, backed by modern PostgreSQL.\n",
    "homepage_url": null,
    "icon": "assets/oban-logo.svg",
    "items": {
        "attachments": [],
        "exceptions": [
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.CrashError",
                "nested_title": null,
                "source_doc": {
                    "en": "Wraps unhandled exits and throws that occur during job execution.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L1",
                "title": "Oban.CrashError",
                "type": "exception",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 17,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.PerformError",
                "nested_title": null,
                "source_doc": {
                    "en": "Wraps the reason returned by `{:error, reason}`, `{:cancel, reason}`, or `{:discard, reason}` in\na proper exception.\n\nThe original return tuple is available in the `:reason` key.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L16",
                "title": "Oban.PerformError",
                "type": "exception",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 37,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.TimeoutError",
                "nested_title": null,
                "source_doc": {
                    "en": "Returned when a job is terminated early due to a custom timeout.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L36",
                "title": "Oban.TimeoutError",
                "type": "exception",
                "typespecs": []
            }
        ],
        "modules": [
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 4,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban",
                "nested_title": null,
                "source_doc": {
                    "en": "\n\nOban is a robust job processing library which uses PostgreSQL or SQLite3 for\nstorage and coordination.\n\n### Installation\n\nSee the [installation guide](https://hexdocs.pm/oban/installation.html) for\ndetails on installing and configuring Oban in your application.\n\n### Requirements\n\nOban requires Elixir 1.12+, Erlang 22+, and PostgreSQL 12.0+ or SQLite3 3.37.0+.\n\n### Testing\n\nFind testing setup, helpers, and strategies in the [testing guide](https://hexdocs.pm/oban/testing.html).\n\n## Oban Web+Pro\n\nA web dashboard for managing Oban, along with an official set of extensions,\nplugins, and workers are available as licensed packages:\n\n* [\ud83e\udded Web Overview](https://getoban.pro#oban-web)\n* [\ud83c\udf1f Pro Overview](https://getoban.pro#oban-pro)\n\nLearn more about prices and licensing Oban Web+Pro at [getoban.pro][pro].\n\n[pro]: https://getoban.pro\n\n## Running with SQLite3\n\nOban ships with engines for PostgreSQL and SQLite3. Both engines support the\nsame core functionality for a single node, while the Postgres engine is more\nadvanced and designed to run in a distributed environment.\n\nRunning with SQLite3 requires adding `ecto_sqlite3` to your app's dependencies\nand setting the `Oban.Engines.Lite` engine:\n\n```elixir\nconfig :my_app, Oban,\n  engine: Oban.Engines.Lite,\n  queues: [default: 10],\n  repo: MyApp.Repo\n```\n\n_Please note that SQLite3 may not be suitable for high-concurrency systems or for systems that need\nto handle large amounts of data. If you expect your background jobs to generate high loads, it\nwould be better to use a more robust database solution that supports horizontal scalability, like\nPostgres._\n\n## Configuring Queues\n\nQueues are specified as a keyword list where the key is the name of the queue\nand the value is the maximum number of concurrent jobs. The following\nconfiguration would start four queues with concurrency ranging from 5 to 50:\n\n```elixir\nconfig :my_app, Oban,\n  queues: [default: 10, mailers: 20, events: 50, media: 5],\n  repo: MyApp.Repo\n```\n\nYou may also use an expanded form to configure queues with individual overrides:\n\n```elixir\nqueues: [\n  default: 10,\n  events: [limit: 50, paused: true]\n]\n```\n\nThe `events` queue will now start in a paused state, which means it won't\nprocess anything until `Oban.resume_queue/2` is called to start it.\n\nThere isn't a limit to the number of queues or how many jobs may execute\nconcurrently in each queue. Some additional guidelines:\n\n#### Caveats & Guidelines\n\n* Each queue will run as many jobs as possible concurrently, up to the\n  configured limit. Make sure your system has enough resources (i.e. database\n  connections) to handle the concurrent load.\n\n* Queue limits are local (per-node), not global (per-cluster). For example,\n  running a queue with a local limit of one on three separate nodes is\n  effectively a global limit of three. If you require a global limit you must\n  restrict the number of nodes running a particular queue.\n\n* Only jobs in the configured queues will execute. Jobs in any other queue will\n  stay in the database untouched.\n\n* Be careful how many concurrent jobs make expensive system calls (i.e. FFMpeg,\n  ImageMagick). The BEAM ensures that the system stays responsive under load,\n  but those guarantees don't apply when using ports or shelling out commands.\n\n## Defining Workers\n\nWorker modules do the work of processing a job. At a minimum they must define a\n`perform/1` function, which is called with an `%Oban.Job{}` struct.\n\nNote that the `args` field of the job struct will _always_ have string keys,\nregardless of the key type when the job was enqueued. The `args` are stored as\n`json` and the serialization process automatically stringifies all keys.\n\nDefine a worker to process jobs in the `events` queue:\n\n```elixir\ndefmodule MyApp.Business do\n  use Oban.Worker, queue: :events\n\n  @impl Oban.Worker\n  def perform(%Oban.Job{args: %{\"id\" => id} = args}) do\n    model = MyApp.Repo.get(MyApp.Business.Man, id)\n\n    case args do\n      %{\"in_the\" => \"business\"} ->\n        IO.inspect(model)\n\n      %{\"vote_for\" => vote} ->\n        IO.inspect([vote, model])\n\n      _ ->\n        IO.inspect(model)\n    end\n\n    :ok\n  end\nend\n```\n\nThe `use` macro also accepts options to customize `max_attempts`, `priority`, `tags`, and `unique`\noptions:\n\n```elixir\ndefmodule MyApp.LazyBusiness do\n  use Oban.Worker,\n    queue: :events,\n    priority: 3,\n    max_attempts: 3,\n    tags: [\"business\"],\n    unique: [period: 30]\n\n  @impl Oban.Worker\n  def perform(_job) do\n    # do business slowly\n\n    :ok\n  end\nend\n```\n\nLike all `use` macros, options are defined at compile time. Avoid using `Application.get_env/2` to\ndefine worker options. Instead, pass dynamic options at runtime by passing them to\n`MyWorker.new/2`:\n\n```elixir\nMyApp.MyWorker.new(args, queue: dynamic_queue)\n```\n\nSuccessful jobs should return `:ok` or an `{:ok, value}` tuple. The value returned from\n`perform/1` is used to control whether the job is treated as a success, a failure, cancelled or\ndeferred for retrying later.\n\nSee the `Oban.Worker` docs for more details on failure conditions and `Oban.Telemetry` for details\non job reporting.\n\n## Enqueueing Jobs\n\nJobs are simply Ecto structs and are enqueued by inserting them into the\ndatabase. For convenience and consistency all workers provide a `new/2`\nfunction that converts an args map into a job changeset suitable for insertion:\n\n```elixir\n%{id: 1, in_the: \"business\", of_doing: \"business\"}\n|> MyApp.Business.new()\n|> Oban.insert()\n```\n\nThe worker's defaults may be overridden by passing options:\n\n```elixir\n%{id: 1, vote_for: \"none of the above\"}\n|> MyApp.Business.new(queue: :special, max_attempts: 5)\n|> Oban.insert()\n```\n\nUnique jobs can be configured in the worker, or when the job is built:\n\n```elixir\n%{email: \"brewster@example.com\"}\n|> MyApp.Mailer.new(unique: [period: 300, fields: [:queue, :worker])\n|> Oban.insert()\n```\n\nJob priority can be specified using an integer from 0 to 3, with 0 being the\ndefault and highest priority:\n\n```elixir\n%{id: 1}\n|> MyApp.Backfiller.new(priority: 2)\n|> Oban.insert()\n```\n\nAny number of tags can be added to a job dynamically, at the time it is\ninserted:\n\n```elixir\nid = 1\n\n%{id: id}\n|> MyApp.OnboardMailer.new(tags: [\"mailer\", \"record-#{id}\"])\n|> Oban.insert()\n```\n\nMultiple jobs can be inserted in a single transaction:\n\n```elixir\nEcto.Multi.new()\n|> Oban.insert(:b_job, MyApp.Business.new(%{id: 1}))\n|> Oban.insert(:m_job, MyApp.Mailer.new(%{email: \"brewser@example.com\"}))\n|> Repo.transaction()\n```\n\nOccasionally you may need to insert a job for a worker that exists in another\napplication. In that case you can use `Oban.Job.new/2` to build the changeset\nmanually:\n\n```elixir\n%{id: 1, user_id: 2}\n|> Oban.Job.new(queue: :default, worker: OtherApp.Worker)\n|> Oban.insert()\n```\n\n`Oban.insert/2,4` is the preferred way of inserting jobs as it provides some of\nOban's advanced features (i.e., unique jobs). However, you can use your\napplication's `Repo.insert/2` function if necessary.\n\nSee `Oban.Job.new/2` for a full list of job options.\n\n## Scheduling Jobs\n\nJobs may be scheduled down to the second any time in the future:\n\n```elixir\n%{id: 1}\n|> MyApp.Business.new(schedule_in: 5)\n|> Oban.insert()\n```\n\nJobs may also be scheduled at a specific datetime in the future:\n\n```elixir\n%{id: 1}\n|> MyApp.Business.new(scheduled_at: ~U[2020-12-25 19:00:56.0Z])\n|> Oban.insert()\n```\n\nScheduling is _always_ in UTC. You'll have to shift timestamps in other zones to\nUTC before scheduling:\n\n```elixir\n%{id: 1}\n|> MyApp.Business.new(scheduled_at: DateTime.shift_zone!(datetime, \"Etc/UTC\"))\n|> Oban.insert()\n```\n\n#### Caveats & Guidelines\n\nUsually, scheduled job management operates in `global` mode and notifies queues\nof available jobs via PubSub to minimize database load. However, when PubSub\nisn't available, staging switches to a `local` mode where each queue polls\nindependently.\n\nLocal mode is less efficient and will only happen if you're running in an\nenvironment where neither `Postgres` nor `PG` notifications work. That situation\nshould be rare and limited to the following conditions:\n\n1. Running with a connection pooler, i.e., `pg_bouncer`, in transaction mode.\n2. Running without clustering, i.e., without Distributed Erlang\n\nIf **both** of those criteria apply and PubSub notifications won't work, then\nstaging will switch to polling in `local` mode.\n\n## Prioritizing Jobs\n\nNormally, all available jobs within a queue are executed in the order they were\nscheduled. You can override the normal behavior and prioritize or de-prioritize\na job by assigning a numerical `priority`.\n\n* Priorities from 0-3 are allowed, where 0 is the highest priority and 3 is the\n  lowest.\n\n* The default priority is 0, unless specified all jobs have an equally high\n  priority.\n\n* All jobs with a higher priority will execute before any jobs with a lower\n  priority. Within a particular priority jobs are executed in their scheduled\n  order.\n\n#### Caveats & Guidelines\n\nThe default priority is defined in the jobs table. The least intrusive way to\nchange it for all jobs is to change the column default:\n\n```elixir\nalter table(\"oban_jobs\") do\n  modify :priority, :integer, default: 1, from: {:integer, default: 0}\nend\n```\n\n## Unique Jobs\n\nThe unique jobs feature lets you specify constraints to prevent enqueueing duplicate jobs.\nUniqueness is based on a combination of `args`, `queue`, `worker`, `state` and insertion time. It\nis configured at the worker or job level using the following options:\n\n* `:period` \u2014 The number of seconds until a job is no longer considered duplicate. You should\n  always specify a period, otherwise Oban will default to 60 seconds. `:infinity` can be used to\n  indicate the job be considered a duplicate as long as jobs are retained.\n\n* `:fields` \u2014 The fields to compare when evaluating uniqueness. The available fields are `:args`,\n  `:queue`, `:worker`, and `:meta`. By default, fields is set to `[:worker, :queue, :args]`.\n\n* `:keys` \u2014 A specific subset of the `:args` or `:meta` to consider when comparing against\n  historic jobs. This allows a job with multiple key/value pairs in the args to be compared using\n  only a subset of them.\n\n* `:states` \u2014 The job states that are checked for duplicates. The available states are\n  `:available`, `:scheduled`, `:executing`, `:retryable`, `:completed`, `:cancelled` and\n  `:discarded`. By default all states except for `:discarded` and `:cancelled` are checked, which\n  prevents duplicates even if the previous job has been completed.\n\n* `:timestamp` \u2014 Which timestamp to check the period against. The available timestamps are\n  `:inserted_at` or `:scheduled_at`, and it defaults to `:inserted_at` for legacy reasons.\n\nFor example, configure a worker to be unique across all fields and states for 60\nseconds:\n\n```elixir\nuse Oban.Worker, unique: [period: 60]\n```\n\nConfigure the worker to be unique only by `:worker` and `:queue`:\n\n```elixir\nuse Oban.Worker, unique: [fields: [:queue, :worker], period: 60]\n```\n\nCheck the `:scheduled_at` timestamp instead of `:inserted_at` for uniqueness:\n\n```elixir\nuse Oban.Worker, unique: [period: 120, timestamp: :scheduled_at]\n```\n\nOr, configure a worker to be unique until it has executed:\n\n```elixir\nuse Oban.Worker, unique: [period: 300, states: [:available, :scheduled, :executing]]\n```\n\nOnly consider the `:url` key rather than the entire `args`:\n\n```elixir\nuse Oban.Worker, unique: [fields: [:args, :worker], keys: [:url]]\n```\n\nYou can use `Oban.Job.states/0` to specify uniqueness across _all_ states,\nincluding `:discarded`:\n\n```elixir\nuse Oban.Worker, unique: [period: 300, states: Oban.Job.states()]\n```\n\n#### Detecting Unique Conflicts\n\nWhen unique settings match an existing job, the return value of `Oban.insert/2`\nis still `{:ok, job}`. However, you can detect a unique conflict by checking the\njobs' `:conflict?` field. If there was an existing job, the field is `true`;\notherwise it is `false`.\n\nYou can use the `:conflict?` field to customize responses after insert:\n\n```elixir\nwith {:ok, %Job{conflict?: true}} <- Oban.insert(changeset) do\n  {:error, :job_already_exists}\nend\n```\n\nNote that conflicts are only detected for jobs enqueued through `Oban.insert/2,3`.\nJobs enqueued through `Oban.insert_all/2` _do not_ use per-job unique\nconfiguration.\n\n#### Replacing Values\n\nIn addition to detecting unique conflicts, passing options to `replace` can\nupdate any job field when there is a conflict. Any of the following fields can\nbe replaced per _state_:  `args`, `max_attempts`, `meta`, `priority`, `queue`,\n`scheduled_at`, `tags`, `worker`.\n\nFor example, to change the `priority` and increase `max_attempts` when there is\na conflict with a job in a `scheduled` state:\n\n```elixir\nBusinessWorker.new(\n  args,\n  max_attempts: 5,\n  priority: 0,\n  replace: [scheduled: [:max_attempts, :priority]]\n)\n```\n\nAnother example is bumping the scheduled time on conflict. Either `scheduled_at`\nor `schedule_in` values will work, but the replace option is always\n`scheduled_at`.\n\n```elixir\nUrgentWorker.new(args, schedule_in: 1, replace: [scheduled: [:scheduled_at]])\n```\n\nNOTE: If you use this feature to replace a field (e.g. `args`) in the\n`executing` state by doing something like: `UniqueWorker.new(new_args, replace:\n[executing: [:args]])` Oban will update the `args`, but the job will continue\nexecuting with the original value.\n\n#### Strong Guarantees\n\nUnique jobs are guaranteed through transactional locks and database queries:\nthey _do not_ rely on unique constraints in the database. This makes uniqueness\nentirely configurable by application code, without the need for database\nmigrations.\n\n## Pruning Historic Jobs\n\nJob stats and queue introspection are built on keeping job rows in the database\nafter they have completed. This allows administrators to review completed jobs\nand build informative aggregates, at the expense of storage and an unbounded\ntable size. To prevent the `oban_jobs` table from growing indefinitely, Oban\nprovides active pruning of `completed`, `cancelled` and `discarded` jobs.\n\nBy default, the `Pruner` plugin retains jobs for 60 seconds. You can configure a\nlonger retention period by providing a `max_age` in seconds to the `Pruner`\nplugin.\n\n```elixir\n# Set the max_age for 5 minutes\nconfig :my_app, Oban,\n  plugins: [{Oban.Plugins.Pruner, max_age: 300}]\n  ...\n```\n\n#### Caveats & Guidelines\n\n* Pruning is best-effort and performed out-of-band. This means that all limits\n  are soft; jobs beyond a specified age may not be pruned immediately after jobs\n  complete.\n\n* Pruning is only applied to jobs that are `completed`, `cancelled` or\n  `discarded`. It'll never delete a new job, a scheduled job or a job that\n  failed and will be retried.\n\n## Periodic Jobs\n\nOban's `Cron` plugin registers workers a cron-like schedule and enqueues jobs\nautomatically. Periodic jobs are declared as a list of `{cron, worker}` or\n`{cron, worker, options}` tuples:\n\n```elixir\nconfig :my_app, Oban,\n  repo: MyApp.Repo,\n  plugins: [\n    {Oban.Plugins.Cron,\n     crontab: [\n       {\"* * * * *\", MyApp.MinuteWorker},\n       {\"0 * * * *\", MyApp.HourlyWorker, args: %{custom: \"arg\"}},\n       {\"0 0 * * *\", MyApp.DailyWorker, max_attempts: 1},\n       {\"0 12 * * MON\", MyApp.MondayWorker, queue: :scheduled, tags: [\"mondays\"]},\n       {\"@daily\", MyApp.AnotherDailyWorker}\n     ]}\n  ]\n```\n\nThe crontab would insert jobs as follows:\n\n* `MyApp.MinuteWorker` \u2014 Inserted once every minute\n* `MyApp.HourlyWorker` \u2014 Inserted at the first minute of every hour with custom args\n* `MyApp.DailyWorker` \u2014 Inserted at midnight every day with no retries\n* `MyApp.MondayWorker` \u2014 Inserted at noon every Monday in the \"scheduled\" queue\n* `MyApp.AnotherDailyWorker` \u2014 Inserted at midnight every day with no retries\n\nThe crontab format respects all [standard rules][cron] and has one minute\nresolution. Jobs are considered unique for most of each minute, which prevents\nduplicate jobs with multiple nodes and across node restarts.\n\nLike other jobs, recurring jobs will use the `:queue` specified by the worker\nmodule (or `:default` if one is not specified).\n\n### Cron Expressions\n\nStandard Cron expressions are composed of rules specifying the minutes, hours,\ndays, months and weekdays. Rules for each field are comprised of literal values,\nwildcards, step values or ranges:\n\n* `*` \u2014 Wildcard, matches any value (0, 1, 2, ...)\n* `0` \u2014 Literal, matches only itself (only 0)\n* `*/15` \u2014 Step, matches any value that is a multiple (0, 15, 30, 45)\n* `0-5` \u2014 Range, matches any value within the range (0, 1, 2, 3, 4, 5)\n* `0-9/2` - Step values can be used in conjunction with ranges (0, 2, 4, 6, 8)\n\nEach part may have multiple rules, where rules are separated by a comma. The\nallowed values for each field are as follows:\n\n* `minute` \u2014 0-59\n* `hour` \u2014 0-23\n* `days` \u2014 1-31\n* `month` \u2014 1-12 (or aliases, `JAN`, `FEB`, `MAR`, etc.)\n* `weekdays` \u2014 0-6 (or aliases, `SUN`, `MON`, `TUE`, etc.)\n\nThe following Cron extensions are supported:\n\n* `@hourly` \u2014 `0 * * * *`\n* `@daily` (as well as `@midnight`) \u2014 `0 0 * * *`\n* `@weekly` \u2014 `0 0 * * 0`\n* `@monthly` \u2014 `0 0 1 * *`\n* `@yearly` (as well as `@annually`) \u2014 `0 0 1 1 *`\n* `@reboot` \u2014 Run once at boot across the entire cluster\n\nSome specific examples that demonstrate the full range of expressions:\n\n* `0 * * * *` \u2014 The first minute of every hour\n* `*/15 9-17 * * *` \u2014 Every fifteen minutes during standard business hours\n* `0 0 * DEC *` \u2014 Once a day at midnight during December\n* `0 7-9,4-6 13 * FRI` \u2014 Once an hour during both rush hours on Friday the 13th\n\nFor more in depth information see the man documentation for `cron` and `crontab`\nin your system. Alternatively you can experiment with various expressions\nonline at [Crontab Guru][guru].\n\n#### Caveats & Guidelines\n\n* All schedules are evaluated as UTC unless a different timezone is provided.\n  See `Oban.Plugins.Cron` for information about configuring a timezone.\n\n* Workers can be used for regular _and_ scheduled jobs so long as they accept\n  different arguments.\n\n* Duplicate jobs are prevented through transactional locks and unique\n  constraints. Workers that are used for regular and scheduled jobs _must not_\n  specify `unique` options less than `60s`.\n\n* Long running jobs may execute simultaneously if the scheduling interval is\n  shorter than it takes to execute the job. You can prevent overlap by passing\n  custom `unique` opts in the crontab config:\n\n  ```elixir\n  custom_args = %{scheduled: true}\n\n  unique_opts = [\n    period: 60 * 60 * 24,\n    states: [:available, :scheduled, :executing]\n  ]\n\n  config :my_app, Oban,\n    repo: MyApp.Repo,\n    plugins: [\n      {Oban.Plugins.Cron,\n       crontab: [\n         {\"* * * * *\", MyApp.SlowWorker, args: custom_args, unique: unique_opts}\n       ]}\n    ]\n  ```\n\n[cron]: https://en.wikipedia.org/wiki/Cron#Overview\n[guru]: https://crontab.guru\n\n## Error Handling\n\nWhen a job returns an error value, raises an error, or exits during execution the\ndetails are recorded within the `errors` array on the job. When the number of\nexecution attempts is below the configured `max_attempts` limit, the job will\nautomatically be retried in the future.\n\nThe retry delay has an exponential backoff, meaning the job's second attempt\nwill be after 16s, third after 31s, fourth after 1m 36s, etc.\n\nSee the `Oban.Worker` documentation on \"Customizing Backoff\" for alternative\nbackoff strategies.\n\n### Error Details\n\nExecution errors are stored as a formatted exception along with metadata about\nwhen the failure occurred and which attempt caused it. Each error is stored with\nthe following keys:\n\n* `at` The UTC timestamp when the error occurred at\n* `attempt` The attempt number when the error occurred\n* `error` A formatted error message and stacktrace\n\nSee the [Instrumentation](#instrumentation-error-reporting-and-logging) docs for an example of\nintegrating with external error reporting systems.\n\n### Limiting Retries\n\nBy default, jobs are retried up to 20 times. The number of retries is controlled\nby the `max_attempts` value, which can be set at the Worker or Job level. For\nexample, to instruct a worker to discard jobs after three failures:\n\n```elixir\nuse Oban.Worker, queue: :limited, max_attempts: 3\n```\n\n### Limiting Execution Time\n\nBy default, individual jobs may execute indefinitely. If this is undesirable you\nmay define a timeout in milliseconds with the `timeout/1` callback on your\nworker module.\n\nFor example, to limit a worker's execution time to 30 seconds:\n\n```elixir\ndef MyApp.Worker do\n  use Oban.Worker\n\n  @impl Oban.Worker\n  def perform(_job) do\n    something_that_may_take_a_long_time()\n\n    :ok\n  end\n\n  @impl Oban.Worker\n  def timeout(_job), do: :timer.seconds(30)\nend\n```\n\nThe `timeout/1` function accepts an `Oban.Job` struct, so you can customize the\ntimeout using any job attributes.\n\nDefine the `timeout` value through job args:\n\n```elixir\ndef timeout(%_{args: %{\"timeout\" => timeout}}), do: timeout\n```\n\nDefine the `timeout` based on the number of attempts:\n\n```elixir\ndef timeout(%_{attempt: attempt}), do: attempt * :timer.seconds(5)\n```\n\n## Instrumentation, Error Reporting, and Logging\n\nOban provides integration with [Telemetry][tele], a dispatching library for\nmetrics. It is easy to report Oban metrics to any backend by attaching to\n`:oban` events.\n\nHere is an example of a sample unstructured log handler:\n\n```elixir\ndefmodule MyApp.ObanLogger do\n  require Logger\n\n  def handle_event([:oban, :job, :start], measure, meta, _) do\n    Logger.warning(\"[Oban] :started #{meta.worker} at #{measure.system_time}\")\n  end\n\n  def handle_event([:oban, :job, event], measure, meta, _) do\n    Logger.warning(\"[Oban] #{event} #{meta.worker} ran in #{measure.duration}\")\n  end\nend\n```\n\nAttach the handler to success and failure events in `application.ex`:\n\n```elixir\nevents = [[:oban, :job, :start], [:oban, :job, :stop], [:oban, :job, :exception]]\n\n:telemetry.attach_many(\"oban-logger\", events, &MyApp.ObanLogger.handle_event/4, [])\n```\n\nThe `Oban.Telemetry` module provides a robust structured logger that handles all\nof Oban's telemetry events. As in the example above, attach it within your\n`application.ex` module:\n\n```elixir\n:ok = Oban.Telemetry.attach_default_logger()\n```\n\nFor more details on the default structured logger and information on event\nmetadata see docs for the `Oban.Telemetry` module.\n\n### Reporting Errors\n\nAnother great use of execution data is error reporting. Here is an example of\nintegrating with [Sentry][sentry] to report job failures:\n\n```elixir\ndefmodule ErrorReporter do\n  def handle_event([:oban, :job, :exception], measure, meta, _) do\n    extra =\n      meta.job\n      |> Map.take([:id, :args, :meta, :queue, :worker])\n      |> Map.merge(measure)\n\n    Sentry.capture_exception(meta.reason, stacktrace: meta.stacktrace, extra: extra)\n  end\nend\n\n:telemetry.attach(\n  \"oban-errors\",\n  [:oban, :job, :exception],\n  &ErrorReporter.handle_event/4,\n  []\n)\n```\n\nYou can use exception events to send error reports to Honeybadger, Rollbar,\nAppSignal or any other application monitoring platform.\n\n[tele]: https://hexdocs.pm/telemetry\n[sentry]: https://sentry.io\n\n## Instance and Database Isolation\n\nYou can run multiple Oban instances with different prefixes on the same system\nand have them entirely isolated, provided you give each supervisor a distinct\nid.\n\nHere we configure our application to start three Oban supervisors using the\n\"public\", \"special\" and \"private\" prefixes, respectively:\n\n```elixir\ndef start(_type, _args) do\n  children = [\n    Repo,\n    Endpoint,\n    {Oban, name: ObanA, repo: Repo},\n    {Oban, name: ObanB, repo: Repo, prefix: \"special\"},\n    {Oban, name: ObanC, repo: Repo, prefix: \"private\"}\n  ]\n\n  Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)\nend\n```\n\n### Umbrella Apps\n\nIf you need to run Oban from an umbrella application where more than one of\nthe child apps need to interact with Oban, you may need to set the `:name` for\neach child application that configures Oban.\n\nFor example, your umbrella contains two apps: `MyAppA` and `MyAppB`. `MyAppA` is\nresponsible for inserting jobs, while only `MyAppB` actually runs any queues.\n\nConfigure Oban with a custom name for `MyAppA`:\n\n```elixir\nconfig :my_app_a, Oban,\n  name: MyAppA.Oban,\n  repo: MyApp.Repo\n```\n\nThen configure Oban for `MyAppB` with a different name:\n\n```elixir\nconfig :my_app_b, Oban,\n  name: MyAppB.Oban,\n  repo: MyApp.Repo,\n  queues: [default: 10]\n```\n\nNow, use the configured name when calling functions like `Oban.insert/2`,\n`Oban.insert_all/2`, `Oban.drain_queue/2`, etc., to reference the correct Oban\nprocess for the current application.\n\n```elixir\nOban.insert(MyAppA.Oban, MyWorker.new(%{}))\nOban.insert_all(MyAppB.Oban, multi, :multiname, [MyWorker.new(%{})])\nOban.drain_queue(MyAppB.Oban, queue: :default)\n```\n\n### Database Prefixes\n\nOban supports namespacing through PostgreSQL schemas, also called \"prefixes\" in\nEcto. With prefixes your jobs table can reside outside of your primary schema\n(usually public) and you can have multiple separate job tables.\n\nTo use a prefix you first have to specify it within your migration:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddPrefixedObanJobsTable do\n  use Ecto.Migration\n\n  def up do\n    Oban.Migrations.up(prefix: \"private\")\n  end\n\n  def down do\n    Oban.Migrations.down(prefix: \"private\")\n  end\nend\n```\n\nThe migration will create the \"private\" schema and all tables, functions and\ntriggers within that schema. With the database migrated you'll then specify the\nprefix in your configuration:\n\n```elixir\nconfig :my_app, Oban,\n  prefix: \"private\",\n  repo: MyApp.Repo,\n  queues: [default: 10]\n```\n\nNow all jobs are inserted and executed using the `private.oban_jobs` table. Note\nthat `Oban.insert/2,4` will write jobs in the `private.oban_jobs` table, you'll\nneed to specify a prefix manually if you insert jobs directly through a repo.\n\nNot only is the `oban_jobs` table isolated within the schema, but all\nnotification events are also isolated. That means that insert/update events will\nonly dispatch new jobs for their prefix.\n\n### Dynamic Repositories\n\nOban supports [Ecto dynamic repositories][dynamic] through the\n`:get_dynamic_repo` option. To make this work, you need to run a separate Oban\ninstance per each dynamic repo instance. Most often it's worth bundling each\nOban and repo instance under the same supervisor:\n\n```elixir\ndef start_repo_and_oban(instance_id) do\n  children = [\n    {MyDynamicRepo, name: nil, url: repo_url(instance_id)},\n    {Oban, name: instance_id, get_dynamic_repo: fn -> repo_pid(instance_id) end}\n  ]\n\n  Supervisor.start_link(children, strategy: :one_for_one)\nend\n```\n\nThe function `repo_pid/1` must return the pid of the repo for the given\ninstance. You can use `Registry` to register the repo (for example in the repo's\n`init/2` callback) and discover it.\n\nIf your application exclusively uses dynamic repositories and doesn't specify\nall credentials upfront, you must implement an `init/1` callback in your Ecto\nRepo. Doing so provides the Postgres notifier with the correct credentials on\ninit, allowing jobs to process as expected.\n\n[dynamic]: https://hexdocs.pm/ecto/replicas-and-dynamic-repositories.html#dynamic-repositories\n\n### Ecto Multi-tenancy\n\nIf you followed the Ecto guide on setting up multi-tenancy with foreign keys, you need to add an\nexception for queries originating from Oban. All of Oban's queries have the custom option `oban:\ntrue` to help you identify them in `prepare_query/3` or other instrumentation:\n\n```elixir\n# Sample code, only relevant if you followed the Ecto guide on multi tenancy with foreign keys.\ndefmodule MyApp.Repo do\n  use Ecto.Repo, otp_app: :my_app\n\n  require Ecto.Query\n\n  @impl true\n  def prepare_query(_operation, query, opts) do\n    cond do\n      opts[:skip_org_id] || opts[:schema_migration] || opts[:oban] ->\n        {query, opts}\n\n      org_id = opts[:org_id] ->\n        {Ecto.Query.where(query, org_id: ^org_id), opts}\n\n      true ->\n        raise \"expected org_id or skip_org_id to be set\"\n    end\n  end\nend\n```\n\n"
                },
                "source_path": "lib/oban.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L1",
                "title": "Oban",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 74,
                        "name": "changeset_or_fun",
                        "rendered_doc": null,
                        "signature": "changeset_or_fun()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L74",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 77,
                        "name": "changeset_wrapper",
                        "rendered_doc": null,
                        "signature": "changeset_wrapper()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L77",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 75,
                        "name": "changesets_or_wrapper",
                        "rendered_doc": null,
                        "signature": "changesets_or_wrapper()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L75",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 76,
                        "name": "changesets_or_wrapper_or_fun",
                        "rendered_doc": null,
                        "signature": "changesets_or_wrapper_or_fun()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L76",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 59,
                        "name": "drain_option",
                        "rendered_doc": null,
                        "signature": "drain_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L59",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 66,
                        "name": "drain_result",
                        "rendered_doc": null,
                        "signature": "drain_result()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L66",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 78,
                        "name": "multi",
                        "rendered_doc": null,
                        "signature": "multi()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L78",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 79,
                        "name": "multi_name",
                        "rendered_doc": null,
                        "signature": "multi_name()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L79",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 17,
                        "name": "name",
                        "rendered_doc": null,
                        "signature": "name()",
                        "source_doc": {
                            "en": "The name of an Oban instance. This is used to identify instances in the internal registry for\nconfiguration lookup.\n"
                        },
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L21",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 42,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L42",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 23,
                        "name": "queue_name",
                        "rendered_doc": null,
                        "signature": "queue_name()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L23",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 25,
                        "name": "queue_option",
                        "rendered_doc": null,
                        "signature": "queue_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L25",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 31,
                        "name": "queue_state",
                        "rendered_doc": null,
                        "signature": "queue_state()",
                        "source_doc": "none",
                        "source_path": "lib/oban.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban.ex#L31",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Job",
                "nested_title": null,
                "source_doc": {
                    "en": "A Job is an Ecto schema used for asynchronous execution.\n\nJob changesets are created by your application code and inserted into the database for\nasynchronous execution. Jobs can be inserted along with other application data as part of a\ntransaction, which guarantees that jobs will only be triggered from a successful transaction.\n"
                },
                "source_path": "lib/oban/job.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L1",
                "title": "Oban.Job",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 15,
                        "name": "args",
                        "rendered_doc": null,
                        "signature": "args()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L15",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 122,
                        "name": "changeset",
                        "rendered_doc": null,
                        "signature": "changeset()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L122",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 123,
                        "name": "changeset_fun",
                        "rendered_doc": null,
                        "signature": "changeset_fun()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L123",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 124,
                        "name": "changeset_list",
                        "rendered_doc": null,
                        "signature": "changeset_list()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L124",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 125,
                        "name": "changeset_list_fun",
                        "rendered_doc": null,
                        "signature": "changeset_list_fun()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L125",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 16,
                        "name": "errors",
                        "rendered_doc": null,
                        "signature": "errors()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L16",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 75,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L75",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 51,
                        "name": "replace_by_state_option",
                        "rendered_doc": null,
                        "signature": "replace_by_state_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L51",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 40,
                        "name": "replace_option",
                        "rendered_doc": null,
                        "signature": "replace_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L40",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 61,
                        "name": "schedule_in_option",
                        "rendered_doc": null,
                        "signature": "schedule_in_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L61",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 89,
                        "name": "t",
                        "rendered_doc": null,
                        "signature": "t()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L89",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 17,
                        "name": "tags",
                        "rendered_doc": null,
                        "signature": "tags()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L17",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 19,
                        "name": "unique_field",
                        "rendered_doc": null,
                        "signature": "unique_field()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L19",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 33,
                        "name": "unique_option",
                        "rendered_doc": null,
                        "signature": "unique_option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L33",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 21,
                        "name": "unique_period",
                        "rendered_doc": null,
                        "signature": "unique_period()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L21",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 23,
                        "name": "unique_state",
                        "rendered_doc": null,
                        "signature": "unique_state()",
                        "source_doc": "none",
                        "source_path": "lib/oban/job.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/job.ex#L23",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 9,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Migration",
                "nested_title": null,
                "source_doc": {
                    "en": "Migrations create and modify the database tables Oban needs to function.\n\n## Usage\n\nTo use migrations in your application you'll need to generate an `Ecto.Migration` that wraps\ncalls to `Oban.Migration`:\n\n```bash\nmix ecto.gen.migration add_oban\n```\n\nOpen the generated migration in your editor and call the `up` and `down` functions on\n`Oban.Migration`:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddOban do\n  use Ecto.Migration\n\n  def up, do: Oban.Migrations.up()\n\n  def down, do: Oban.Migrations.down()\nend\n```\n\nThis will run all of Oban's versioned migrations for your database.\n\nNow, run the migration to create the table:\n\n```bash\nmix ecto.migrate\n```\n\nMigrations between versions are idempotent. As new versions are released, you may need to run\nadditional migrations. To do this, generate a new migration:\n\n```bash\nmix ecto.gen.migration upgrade_oban_to_v11\n```\n\nOpen the generated migration in your editor and call the `up` and `down` functions on\n`Oban.Migration`, passing a version number:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.UpgradeObanToV11 do\n  use Ecto.Migration\n\n  def up, do: Oban.Migrations.up(version: 11)\n\n  def down, do: Oban.Migrations.down(version: 11)\nend\n```\n\n## Isolation with Prefixes\n\nOban supports namespacing through PostgreSQL schemas, also called \"prefixes\" in Ecto. With\nprefixes your jobs table can reside outside of your primary schema (usually public) and you can\nhave multiple separate job tables.\n\nTo use a prefix you first have to specify it within your migration:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddPrefixedObanJobsTable do\n  use Ecto.Migration\n\n  def up, do: Oban.Migrations.up(prefix: \"private\")\n\n  def down, do: Oban.Migrations.down(prefix: \"private\")\nend\n```\n\nThe migration will create the \"private\" schema and all tables, functions and triggers within\nthat schema. With the database migrated you'll then specify the prefix in your configuration:\n\n```elixir\nconfig :my_app, Oban,\n  prefix: \"private\",\n  ...\n```\n\nIn some cases, for example if your \"private\" schema already exists and your database user in\nproduction doesn't have permissions to create a new schema, trying to create the schema from the\nmigration will result in an error. In such situations, it may be useful to inhibit the creation\nof the \"private\" schema:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddPrefixedObanJobsTable do\n  use Ecto.Migration\n\n  def up, do: Oban.Migrations.up(prefix: \"private\", create_schema: false)\n\n  def down, do: Oban.Migrations.down(prefix: \"private\")\nend\n```\n\n## Migrating Without Ecto\n\nIf your application uses something other than Ecto for migrations, be it an external system or\nanother ORM, it may be helpful to create plain SQL migrations for Oban database schema changes.\n\nThe simplest mechanism for obtaining the SQL changes is to create the migration locally and run\n`mix ecto.migrate --log-migrations-sql`. That will log all of the generated SQL, which you can\nthen paste into your migration system of choice.\n\nAlternatively, if you'd like a more automated approach, try using the [oban_migations_sql][sql]\nproject to generate `up` and `down` SQL migrations for you.\n\n[sql]: https://github.com/btwb/oban_migrations_sql\n"
                },
                "source_path": "lib/oban/migration.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/migration.ex#L8",
                "title": "Oban.Migration",
                "type": "behaviour",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Telemetry",
                "nested_title": null,
                "source_doc": {
                    "en": "Telemetry integration for event metrics, logging and error reporting.\n\n## Initialization Events\n\nOban emits the following telemetry event when an Oban supervisor is started:\n\n* `[:oban, :supervisor, :init]` - when the Oban supervisor is started this will execute\n\nThe initialization event contains the following measurements:\n\n* `:system_time` - The system's time when Oban was started\n\nThe initialization event contains the following metadata:\n\n* `:conf` - The configuration used for the Oban supervisor instance\n* `:pid` - The PID of the supervisor instance\n\n## Job Events\n\nOban emits the following telemetry events for each job:\n\n* `[:oban, :job, :start]` \u2014 at the point a job is fetched from the database and will execute\n* `[:oban, :job, :stop]` \u2014 after a job succeeds and the success is recorded in the database\n* `[:oban, :job, :exception]` \u2014 after a job fails and the failure is recorded in the database\n\nAll job events share the same details about the job that was executed. In addition, failed jobs\nprovide the error type, the error itself, and the stacktrace. The following chart shows which\nmetadata you can expect for each event:\n\n| event        | measures                   | metadata                                                     |\n| ------------ | -------------------------- | ------------------------------------------------------------ |\n| `:start`     | `:system_time`             | `:conf`, `:job`                                              |\n| `:stop`      | `:duration`, `:queue_time` | `:conf`, `:job`, `:state`, `:result`                         |\n| `:exception` | `:duration`, `:queue_time` | `:conf`, `:job`, `:state`, `:kind`, `:reason`, `:stacktrace` |\n\n### Metadata\n\n* `:conf` \u2014 the executing Oban instance's config\n* `:job` \u2014 the executing `Oban.Job`\n* `:state` \u2014 one of `:success`, `:failure`, `:cancelled`, `:discard` or `:snoozed`\n* `:result` \u2014 the `perform/1` return value, included unless job failed with an exception or\n  crash\n\nFor `:exception` events the metadata also includes details about what caused the failure. The\n`:kind` value is determined by how an error occurred. Here are the possible kinds:\n\n* `:error` \u2014 from an `{:error, error}` return value. Some Erlang functions may also throw an\n  `:error` tuple, which will be reported as `:error`.\n* `:exit` \u2014 from a caught process exit\n* `:throw` \u2014 from a caught value, this doesn't necessarily mean that an error occurred and the\n  error value is unpredictable\n\n## Engine Events\n\nOban emits telemetry span events for the following Engine operations:\n\n* `[:oban, :engine, :init, :start | :stop | :exception]`\n* `[:oban, :engine, :refresh, :start | :stop | :exception]`\n* `[:oban, :engine, :put_meta, :start | :stop | :exception]`\n\n| event        | measures       | metadata                                              |\n| ------------ | -------------- | ----------------------------------------------------- |\n| `:start`     | `:system_time` | `:conf`, `:engine`                                    |\n| `:stop`      | `:duration`    | `:conf`, `:engine`                                    |\n| `:exception` | `:duration`    | `:conf`, `:engine`, `:kind`, `:reason`, `:stacktrace` |\n\nEvents for bulk operations also include `:jobs` for the `:stop` event:\n\n* `[:oban, :engine, :cancel_all_jobs, :start | :stop | :exception]`\n* `[:oban, :engine, :fetch_jobs, :start | :stop | :exception]`\n* `[:oban, :engine, :insert_all_jobs, :start | :stop | :exception]`\n* `[:oban, :engine, :prune_jobs, :start | :stop | :exception]`\n* `[:oban, :engine, :retry_all_jobs, :start | :stop | :exception]`\n* `[:oban, :engine, :stage_jobs, :start | :stop | :exception]`\n\n| event        | measures       | metadata                                              |\n| ------------ | -------------- | ----------------------------------------------------- |\n| `:start`     | `:system_time` | `:conf`, `:engine`                                    |\n| `:stop`      | `:duration`    | `:conf`, `:engine`, `:jobs`                                    |\n| `:exception` | `:duration`    | `:conf`, `:engine`, `:kind`, `:reason`, `:stacktrace` |\n\nEvents for job-level Engine operations also include the `job`, with the exception of\n`:insert_job, :start`, because the `job` isn't available yet.\n\n* `[:oban, :engine, :cancel_job, :start | :stop | :exception]`\n* `[:oban, :engine, :complete_job, :start | :stop | :exception]`\n* `[:oban, :engine, :discard_job, :start | :stop | :exception]`\n* `[:oban, :engine, :error_job, :start | :stop | :exception]`\n* `[:oban, :engine, :insert_job, :start | :stop | :exception]`\n* `[:oban, :engine, :retry_job, :start | :stop | :exception]`\n* `[:oban, :engine, :snooze_job, :start | :stop | :exception]`\n\n| event        | measures       | metadata                                                      |\n| ------------ | -------------- | ------------------------------------------------------------- |\n| `:start`     | `:system_time` | `:conf`, `:engine`, `:job`                                    |\n| `:stop`      | `:duration`    | `:conf`, `:engine`, `:job`                                    |\n| `:exception` | `:duration`    | `:conf`, `:engine`, `:job`, `:kind`, `:reason`, `:stacktrace` |\n\n### Metadata\n\n* `:conf` \u2014 the Oban supervisor's config\n* `:engine` \u2014 the module of the engine used\n* `:job` - the `Oban.Job` in question\n* `:jobs` \u2014 zero or more maps with the `queue`, `state` for each modified job\n* `:kind`, `:reason`, `:stacktrace` \u2014 see the explanation in job metadata above\n\n## Notifier Events\n\nOban emits a telemetry span event each time the Notifier is triggered:\n\n* `[:oban, :notifier, :notify, :start | :stop | :exception]`\n\n| event        | measures       | metadata                                                            |\n| ------------ | -------------- | ------------------------------------------------------------------- |\n| `:start`     | `:system_time` | `:conf`, `:channel`, `:payload`                                     |\n| `:stop`      | `:duration`    | `:conf`, `:channel`, `:payload`                                     |\n| `:exception` | `:duration`    | `:conf`, `:channel`, `:payload`, `:kind`, `:reason`, `:stacktrace`  |\n\n### Metadata\n\n* `:conf` \u2014 the Oban supervisor's config\n* `:channel` \u2014 the channel on which the notification was sent\n* `:payload` - the decoded payload that was sent\n* `:kind`, `:reason`, `:stacktrace` \u2014 see the explanation in job metadata above\n\n## Plugin Events\n\nAll the Oban plugins emit telemetry events under the `[:oban, :plugin, *]` pattern (where `*` is\neither `:init`, `:start`, `:stop`, or `:exception`). You can filter out for plugin events by\nlooking into the metadata of the event and checking the value of `:plugin`. The `:plugin` field\nis the plugin module that emitted the event. For example, to get `Oban.Plugins.Cron` specific\nevents, you can filter for telemetry events with a metadata key/value of `plugin:\nOban.Plugins.Cron`.\n\nOban emits the following telemetry event whenever a plugin executes (be sure to check the\ndocumentation for each plugin as each plugin can also add additional metadata specific to\nthe plugin):\n\n* `[:oban, :plugin, :init]` \u2014 when the plugin first initializes\n* `[:oban, :plugin, :start]` \u2014 when the plugin beings performing its work\n* `[:oban, :plugin, :stop]` \u2014  after the plugin completes its work\n* `[:oban, :plugin, :exception]` \u2014 when the plugin encounters an error\n\nThe following chart shows which metadata you can expect for each event:\n\n| event        | measures        | metadata                                              |\n| ------------ | --------------- | ----------------------------------------------------- |\n| `:init`      |                 | `:conf`, `:plugin`                                    |\n| `:start`     | `:system_time`  | `:conf`, `:plugin`                                    |\n| `:stop`      | `:duration`     | `:conf`, `:plugin`                                    |\n| `:exception` | `:duration`     | `:conf`, `:plugin`, `:kind`, `:reason`, `:stacktrace` |\n\n## Peer Events\n\nOban emits a telemetry span event each time an Oban Peer election occurs:\n\n* `[:oban, :peer, :election, :start | :stop | :exception]`\n\n| event        | measures       | metadata                                                       |\n| ------------ | -------------- | -------------------------------------------------------------- |\n| `:start`     | `:system_time` | `:conf`, `:leader`, `:peer`,                                   |\n| `:stop`      | `:duration`    | `:conf`, `:leader`, `:peer`,                                   |\n| `:exception` | `:duration`    | `:conf`, `:leader`, `:peer`, `:kind`, `:reason`, `:stacktrace` |\n\n### Metadata\n\n* `:conf`, `:kind`, `:reason`, `:stacktrace` \u2014 see the explanation in notifier metadata above\n* `:leader` \u2014 whether the peer is the current leader\n* `:peer` \u2014 the module used for peering\n\n## Stager Events\n\nOban emits an event any time the Stager switches between `local` and `global` modes:\n\n* `[:oban, :stager, :switch]`\n\n| event        | measures  | metadata         |\n| ------------ | --------- | ---------------- |\n| `:switch`    |           | `:conf`, `:mode` |\n\n### Metadata\n\n* `:conf` \u2014 see the explanation in metadata above\n* `:mode` \u2014 either `local` for polling mode or `global` in the more efficient pub-sub mode\n\n## Default Logger\n\nA default log handler that emits structured JSON is provided, see `attach_default_logger/0` for\nusage. Otherwise, if you would prefer more control over logging or would like to instrument\nevents you can write your own handler.\n\nHere is an example of the JSON output for the `job:stop` event:\n\n```json\n{\n  \"args\":{\"action\":\"OK\",\"ref\":1},\n  \"attempt\":1,\n  \"duration\":4327295,\n  \"event\":\"job:stop\",\n  \"id\":123,\n  \"max_attempts\":20,\n  \"meta\":{},\n  \"queue\":\"alpha\",\n  \"queue_time\":3127905,\n  \"source\":\"oban\",\n  \"state\":\"success\",\n  \"tags\":[],\n  \"worker\":\"Oban.Integration.Worker\"\n}\n```\n\nAll timing measurements are recorded as native time units but logged in microseconds.\n\n## Examples\n\nA handler that only logs a few details about failed jobs:\n\n```elixir\ndefmodule MicroLogger do\n  require Logger\n\n  def handle_event([:oban, :job, :exception], %{duration: duration}, meta, nil) do\n    Logger.warning(\"[#{meta.queue}] #{meta.worker} failed in #{duration}\")\n  end\nend\n\n:telemetry.attach(\"oban-logger\", [:oban, :job, :exception], &MicroLogger.handle_event/4, nil)\n```\n\nAnother great use of execution data is error reporting. Here is an example of integrating with\n[Honeybadger][honey], but only reporting jobs that have failed 3 times or more:\n\n```elixir\ndefmodule ErrorReporter do\n  def handle_event([:oban, :job, :exception], _, %{attempt: attempt} = meta, _) do\n    if attempt >= 3 do\n      context = Map.take(meta, [:id, :args, :queue, :worker])\n\n      Honeybadger.notify(meta.reason, metadata: context, stacktrace: meta.stacktrace)\n    end\n  end\nend\n\n:telemetry.attach(\"oban-errors\", [:oban, :job, :exception], &ErrorReporter.handle_event/4, [])\n```\n\n[honey]: https://honeybadger.io\n"
                },
                "source_path": "lib/oban/telemetry.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/telemetry.ex#L1",
                "title": "Oban.Telemetry",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Testing",
                "nested_title": null,
                "source_doc": {
                    "en": "This module simplifies testing workers and making assertions about enqueued jobs when testing in\n`:manual` mode.\n\nAssertions may be made on any property of a job, but you'll typically want to check by `args`,\n`queue` or `worker`.\n\n## Usage\n\nThe most convenient way to use `Oban.Testing` is to `use` the module:\n\n    use Oban.Testing, repo: MyApp.Repo\n\nThat will define the helper functions you'll use to make assertions on the jobs that should (or\nshould not) be inserted in the database while testing.\n\nIf you're using namespacing through Postgres schemas, also called \"prefixes\" in Ecto, you\nshould set the `prefix` option:\n\n    use Oban.Testing, repo: MyApp.Repo, prefix: \"business\"\n\nUnless overridden, the default `prefix` is `public`.\n\n### Adding to Case Templates\n\nTo include helpers in all of your tests you can add it to your case template:\n\n```elixir\ndefmodule MyApp.DataCase do\n  use ExUnit.CaseTemplate\n\n  using do\n    quote do\n      use Oban.Testing, repo: MyApp.Repo\n\n      import Ecto\n      import Ecto.Changeset\n      import Ecto.Query\n      import MyApp.DataCase\n\n      alias MyApp.Repo\n    end\n  end\nend\n```\n\n## Examples\n\nAfter the test helpers are imported, you can make assertions about enqueued (available or\nscheduled) jobs in your tests.\n\nHere are a few examples that demonstrate what's possible:\n\n```elixir\n# Assert that a job was already enqueued\nassert_enqueued worker: MyWorker, args: %{id: 1}\n\n# Assert that a job was enqueued or will be enqueued in the next 100ms\nassert_enqueued [worker: MyWorker, args: %{id: 1}], 100\n\n# Refute that a job was already enqueued\nrefute_enqueued queue: \"special\", args: %{id: 2}\n\n# Refute that a job was already enqueued or would be enqueued in the next 100ms\nrefute_enqueued queue: \"special\", args: %{id: 2}, 100\n\n# Make assertions on a list of all jobs matching some options\nassert [%{args: %{\"id\" => 1}}] = all_enqueued(worker: MyWorker)\n\n# Assert that no jobs are enqueued in any queues\nassert [] = all_enqueued()\n```\n\nNote that the final example, using `all_enqueued/1`, returns a raw list of matching jobs and\ndoes not make an assertion by itself. This makes it possible to test using pattern matching at\nthe expense of being more verbose.\n\nSee the docs for `assert_enqueued/1,2`, `refute_enqueued/1,2`, and `all_enqueued/1` for more\nexamples.\n\n## Matching Timestamps\n\nIn order to assert a job has been scheduled at a certain time, you will need to match against\nthe `scheduled_at` attribute of the enqueued job.\n\n    in_an_hour = DateTime.add(DateTime.utc_now(), 3600, :second)\n    assert_enqueued worker: MyApp.Worker, scheduled_at: in_an_hour\n\nBy default, Oban will apply a 1 second delta to all timestamp fields of jobs, so that small\ndeviations between the actual value and the expected one are ignored. You may configure this\ndelta by passing a tuple of value and a `delta` option (in seconds) to corresponding keyword:\n\n    assert_enqueued worker: MyApp.Worker, scheduled_at: {in_an_hour, delta: 10}\n"
                },
                "source_path": "lib/oban/testing.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/testing.ex#L1",
                "title": "Oban.Testing",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [
                            "since 0.3.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 106,
                        "name": "perform_opts",
                        "rendered_doc": null,
                        "signature": "perform_opts()",
                        "source_doc": "none",
                        "source_path": "lib/oban/testing.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/testing.ex#L106",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": null,
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Worker",
                "nested_title": null,
                "source_doc": {
                    "en": "Defines a behavior and macro to guide the creation of worker modules.\n\nWorker modules do the work of processing a job. At a minimum they must define a `c:perform/1`\nfunction, which is called with the full `Oban.Job` struct.\n\n## Defining Workers\n\nWorker modules are defined by using `Oban.Worker`. A bare `use Oban.Worker` invocation sets a\nworker with these defaults:\n\n* `:max_attempts` \u2014 20\n* `:priority` \u2014 0\n* `:queue` \u2014 `:default`\n* `:unique` \u2014 no uniqueness set\n\nTo create a minimum worker using the defaults, including the `default` queue:\n\n    defmodule MyApp.Workers.Basic do\n      use Oban.Worker\n\n      @impl Oban.Worker\n      def perform(%Oban.Job{args: args}) do\n        IO.inspect(args)\n        :ok\n      end\n    end\n\nThe following example defines a complex worker module to process jobs in the `events` queue.\nIt then dials down the priority from 0 to 1, limits retrying on failures to 10 attempts, adds\na \"business\" tag, and ensures that duplicate jobs aren't enqueued within a 30 second period:\n\n    defmodule MyApp.Workers.Business do\n      use Oban.Worker,\n        queue: :events,\n        priority: 1,\n        max_attempts: 10,\n        tags: [\"business\"],\n        unique: [period: 30]\n\n      @impl Oban.Worker\n      def perform(%Oban.Job{attempt: attempt}) when attempt > 3 do\n        IO.inspect(attempt)\n      end\n\n      def perform(job) do\n        IO.inspect(job.args)\n      end\n    end\n\nThe `c:perform/1` function receives an `Oban.Job` struct as an argument. This allows workers to\nchange the behavior of `c:perform/1` based on attributes of the job, e.g. the args, number of\nexecution attempts, or when it was inserted.\n\nThe value returned from `c:perform/1` can control whether the job is a success or a failure:\n\n* `:ok` or `{:ok, value}` \u2014 the job is successful and marked as `completed`.  The `value` from\n  success tuples is ignored.\n\n* `{:cancel, reason}` \u2014 cancel executing the job and stop retrying it. An error is recorded\n  using the provided `reason`. The job is marked as `cancelled`.\n\n* `{:error, error}` \u2014 the job failed, record the error. If `max_attempts` has not been reached\n  already, the job is marked as `retryable` and scheduled to run again. Otherwise, the job is\n  marked as `discarded` and won't be retried.\n\n* `{:snooze, seconds}` \u2014 mark the job as `snoozed` and schedule it to run again `seconds` in the\n  future. See [Snoozing](#module-snoozing-jobs) for more details.\n\nIn addition to explicit return values, any _unhandled exception_, _exit_ or _throw_ will fail\nthe job and schedule a retry if possible.\n\nAs an example of error tuple handling, this worker will return an error tuple when the `value`\nis less than one:\n\n    defmodule MyApp.Workers.ErrorExample do\n      use Oban.Worker\n\n      @impl Worker\n      def perform(%{args: %{\"value\" => value}}) do\n        if value > 1 do\n          :ok\n        else\n          {:error, \"invalid value given: \" <> inspect(value)}\n        end\n      end\n    end\n\nThe error tuple is wrapped in an `Oban.PerformError` with a formatted message. The error tuple\nitself is available through the exception's `:reason` field.\n\n## Enqueuing Jobs\n\nAll workers implement a `c:new/2` function that converts an args map into a job changeset\nsuitable for inserting into the database for later execution:\n\n    %{in_the: \"business\", of_doing: \"business\"}\n    |> MyApp.Workers.Business.new()\n    |> Oban.insert()\n\nThe worker's defaults may be overridden by passing options:\n\n    %{vote_for: \"none of the above\"}\n    |> MyApp.Workers.Business.new(queue: \"special\", max_attempts: 5)\n    |> Oban.insert()\n\nUniqueness options may also be overridden by passing options:\n\n    %{expensive: \"business\"}\n    |> MyApp.Workers.Business.new(unique: [period: 120, fields: [:worker]])\n    |> Oban.insert()\n\nNote that `unique` options aren't merged, they are overridden entirely.\n\nSee `Oban.Job` for all available options.\n\n## Customizing Backoff\n\nWhen jobs fail they may be retried again in the future using a backoff algorithm. By default the\nbackoff is exponential with a fixed padding of 15 seconds and a small amount of jitter. The\njitter helps to prevent jobs that fail simultaneously from consistently retrying at the same\ntime. With the default backoff behavior, the 20th attempt will occur around 12 days after the\nfirst attempt.\n\nIf the default strategy is too aggressive or otherwise unsuited to your app's workload you can\ndefine a custom backoff function using the `c:backoff/1` callback.\n\nThe following worker defines a `c:backoff/1` function that delays retries using a variant of the\nhistoric Resque/Sidekiq algorithm:\n\n    defmodule MyApp.SidekiqBackoffWorker do\n      use Oban.Worker\n\n      @impl Worker\n      def backoff(%Job{attempt: attempt}) do\n        trunc(:math.pow(attempt, 4) + 15 + :rand.uniform(30) * attempt)\n      end\n\n      @impl Worker\n      def perform(_job) do\n        :do_business\n      end\n    end\n\nHere are some alternative backoff strategies to consider:\n\n* **constant** \u2014 delay by a fixed number of seconds, e.g. 1\u219215, 2\u219215, 3\u219215\n* **linear** \u2014 delay for the same number of seconds as the current attempt, e.g. 1\u21921, 2\u21922, 3\u21923\n* **squared** \u2014 delay by attempt number squared, e.g. 1\u21921, 2\u21924, 3\u21929\n* **sidekiq** \u2014 delay by a base amount plus some jitter, e.g. 1\u219232, 2\u219261, 3\u2192135\n\n### Contextual Backoff\n\nAny error, catch or throw is temporarily recorded in the job's `unsaved_error` map. The unsaved\nerror map can be used by `c:backoff/1` to calculate a custom backoff based on the exact error\nthat failed the job. In this example the `c:backoff/1` callback checks to see if the error was\ndue to rate limiting and adjusts the backoff accordingly:\n\n    defmodule MyApp.ApiWorker do\n      use Oban.Worker\n\n      @five_minutes 5 * 60\n\n      @impl Worker\n      def perform(%{args: args}) do\n        MyApp.make_external_api_call(args)\n      end\n\n      @impl Worker\n      def backoff(%Job{attempt: attempt, unsaved_error: unsaved_error}) do\n        %{kind: _, reason: reason, stacktrace: _} = unsaved_error\n\n        case reason do\n          %MyApp.ApiError{status: 429} -> @five_minutes\n          _ -> trunc(:math.pow(attempt, 4))\n        end\n      end\n    end\n\n## Execution Timeout\n\nBy default, individual jobs may execute indefinitely. If this is undesirable you may define a\ntimeout in milliseconds with the `c:timeout/1` callback on your worker module.\n\nFor example, to limit a worker's execution time to 30 seconds:\n\n    def MyApp.Worker do\n      use Oban.Worker\n\n      @impl Oban.Worker\n      def perform(_job) do\n        something_that_may_take_a_long_time()\n\n        :ok\n      end\n\n      @impl Oban.Worker\n      def timeout(_job), do: :timer.seconds(30)\n    end\n\nThe `c:timeout/1` function accepts an `Oban.Job` struct, so you can customize the timeout using\nany job attributes.\n\nDefine the `timeout` value through job args:\n\n    def timeout(%_{args: %{\"timeout\" => timeout}}), do: timeout\n\nDefine the `timeout` based on the number of attempts:\n\n    def timeout(%_{attempt: attempt}), do: attempt * :timer.seconds(5)\n\n## Snoozing Jobs\n\nWhen returning `{:snooze, snooze_time}` in `c:perform/1`, the job is postponed for at least\n`snooze_time` seconds. Snoozing is done by incrementing the job's `max_attempts` field and\nscheduling execution for `snooze_time` seconds in the future.\n\nExecuting bumps a job's `attempt` count. Despite snooze incrementing the `max_attempts` to\npreserve total retries, the change to `attempt` will affect the default backoff retry\nalgorithm.\n\n> #### \ud83c\udf1f Snoozes and Attempts {: .info}\n>\n> Oban Pro's [Smart Engine](https://getoban.pro/docs/pro/Oban.Pro.Engines.Smart.html) rolls back\n> the `attempt` and preserves the original `max_attempts` in order to differentiate between\n> \"real\" attempts and snoozes, which keeps backoff calculation accurate.\n>\n> Without attempt correction you may need a solution that compensates for snoozing, such as the\n> example below:\n\n    defmodule MyApp.SnoozingWorker do\n      @max_attempts 20\n\n      use Oban.Worker, max_attempts: @max_attempts\n\n      @impl Worker\n      def backoff(%Job{} = job) do\n        corrected_attempt = @max_attempts - (job.max_attempts - job.attempt)\n\n        Worker.backoff(%{job | attempt: corrected_attempt})\n      end\n\n      @impl Worker\n      def perform(job) do\n        if MyApp.something?(job), do: :ok, else: {:snooze, 60}\n      end\n    end\n"
                },
                "source_path": "lib/oban/worker.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/worker.ex#L1",
                "title": "Oban.Worker",
                "type": "behaviour",
                "typespecs": [
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 257,
                        "name": "result",
                        "rendered_doc": null,
                        "signature": "result()",
                        "source_doc": "none",
                        "source_path": "lib/oban/worker.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/worker.ex#L257",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "since 0.1.0"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 256,
                        "name": "t",
                        "rendered_doc": null,
                        "signature": "t()",
                        "source_doc": "none",
                        "source_path": "lib/oban/worker.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/worker.ex#L256",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Cron",
                "nested_title": null,
                "source_doc": {
                    "en": "Periodically enqueue jobs through CRON based scheduling.\n\nThis plugin registers workers a cron-like schedule and enqueues jobs automatically. Periodic\njobs are declared as a list of `{cron, worker}` or `{cron, worker, options}` tuples.\n\n> #### \ud83c\udf1f DynamicCron {: .info}\n>\n> This plugin only loads the crontab statically, at boot time. To configure cron schedules\n> dynamically at runtime, across your entire cluster, see the `DynamicCron` plugin in [Oban\n> Pro](https://getoban.pro/docs/pro/Oban.Pro.Plugins.DynamicCron.html).\n\n## Using the Plugin\n\nSchedule various jobs using `{expr, worker}` and `{expr, worker, opts}` syntaxes:\n\n    config :my_app, Oban,\n      plugins: [\n        {Oban.Plugins.Cron,\n         crontab: [\n           {\"* * * * *\", MyApp.MinuteWorker},\n           {\"0 * * * *\", MyApp.HourlyWorker, args: %{custom: \"arg\"}},\n           {\"0 0 * * *\", MyApp.DailyWorker, max_attempts: 1},\n           {\"0 12 * * MON\", MyApp.MondayWorker, queue: :scheduled, tags: [\"mondays\"]},\n           {\"@daily\", MyApp.AnotherDailyWorker}\n         ]}\n      ]\n\n## Options\n\n* `:crontab` \u2014 a list of cron expressions that enqueue jobs on a periodic basis. See [Periodic\n  Jobs][perjob] in the Oban module docs for syntax and details.\n\n* `:timezone` \u2014 which timezone to use when scheduling cron jobs. To use a timezone other than\n  the default of \"Etc/UTC\" you *must* have a timezone database like [tz][tz] installed and\n  configured.\n\n[tz]: https://hexdocs.pm/tz\n[perjob]: Oban.html#module-periodic-jobs\n\n## Instrumenting with Telemetry\n\nThe `Oban.Plugins.Cron` plugin adds the following metadata to the `[:oban, :plugin, :stop]` event:\n\n* :jobs - a list of jobs that were inserted into the database\n"
                },
                "source_path": "lib/oban/plugins/cron.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/cron.ex#L1",
                "title": "Oban.Plugins.Cron",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 58,
                        "name": "cron_input",
                        "rendered_doc": null,
                        "signature": "cron_input()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/cron.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/cron.ex#L58",
                        "type": "type"
                    },
                    {
                        "annotations": [
                            "opaque"
                        ],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 56,
                        "name": "expression",
                        "rendered_doc": null,
                        "signature": "expression()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/cron.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/cron.ex#L56",
                        "type": "opaque"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 60,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/cron.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/cron.ex#L60",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Gossip",
                "nested_title": null,
                "source_doc": {
                    "en": "The Gossip plugin uses PubSub to periodically exchange queue state information between all\ninterested nodes. This allows Oban instances to broadcast state information regardless of which\nengine they are using, and without storing anything in the database.\n\nGossip enables real-time updates across an entire cluster, and is essential to the operation of\nUIs like Oban Web.\n\nThe Gossip plugin entirely replaced heartbeats and the legacy `oban_beats` table.\n\n## Using the Plugin\n\nThe following example demonstrates using the plugin without any configuration, which will broadcast\nthe state of each local queue every 1 second:\n\n    config :my_app, Oban,\n      plugins: [Oban.Plugins.Gossip],\n      ...\n\nOverride the default options to broadcast every 5 seconds:\n\n    config :my_app, Oban,\n      plugins: [{Oban.Plugins.Gossip, interval: :timer.seconds(5)}],\n      ...\n\n## Options\n\n* `:interval` \u2014 the number of milliseconds between gossip broadcasts\n\n## Instrumenting with Telemetry\n\nThe `Oban.Plugins.Gossip` plugin adds the following metadata to the `[:oban, :plugin, :stop]` event:\n\n* `:gossip_count` - the number of queues that had activity broadcasted\n"
                },
                "source_path": "lib/oban/plugins/gossip.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/gossip.ex#L1",
                "title": "Oban.Plugins.Gossip",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 44,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/gossip.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/gossip.ex#L44",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Lifeline",
                "nested_title": null,
                "source_doc": {
                    "en": "Naively transition jobs stuck `executing` back to `available`.\n\nThe `Lifeline` plugin periodically rescues orphaned jobs, i.e. jobs that are stuck in the\n`executing` state because the node was shut down before the job could finish. Rescuing is\npurely based on time, rather than any heuristic about the job's expected execution time or\nwhether the node is still alive.\n\nIf an executing job has exhausted all attempts, the Lifeline plugin will mark it `discarded`\nrather than `available`.\n\n> #### \ud83c\udf1f DynamicLifeline {: .info}\n>\n> This plugin may transition jobs that are genuinely `executing` and cause duplicate execution.\n> For more accurate rescuing or to rescue jobs that have exhausted retry attempts see the\n> `DynamicLifeline` plugin in [Oban Pro](https://getoban.pro/docs/pro/Oban.Pro.Plugins.DynamicLifeline.html).\n\n## Using the Plugin\n\nRescue orphaned jobs that are still `executing` after the default of 60 minutes:\n\n    config :my_app, Oban,\n      plugins: [Oban.Plugins.Lifeline],\n      ...\n\nOverride the default period to rescue orphans after a more aggressive period of 5 minutes:\n\n    config :my_app, Oban,\n      plugins: [{Oban.Plugins.Lifeline, rescue_after: :timer.minutes(5)}],\n      ...\n\n## Options\n\n* `:interval` \u2014 the number of milliseconds between rescue attempts. The default is `60_000ms`.\n\n* `:rescue_after` \u2014 the maximum amount of time, in milliseconds, that a job may execute before\nbeing rescued. 60 minutes by default, and rescuing is performed once a minute.\n\n## Instrumenting with Telemetry\n\nThe `Oban.Plugins.Lifeline` plugin adds the following metadata to the `[:oban, :plugin, :stop]`\nevent:\n\n* `:rescued_jobs` \u2014 a list of jobs transitioned back to `available`\n\n* `:discarded_jobs` \u2014 a list of jobs transitioned to `discarded`\n\n_Note: jobs only include `id`, `queue`, `state` fields._\n"
                },
                "source_path": "lib/oban/plugins/lifeline.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/lifeline.ex#L1",
                "title": "Oban.Plugins.Lifeline",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 60,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/lifeline.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/lifeline.ex#L60",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Pruner",
                "nested_title": null,
                "source_doc": {
                    "en": "Periodically delete completed, cancelled and discarded jobs based on age.\n\n## Using the Plugin\n\nThe following example demonstrates using the plugin without any configuration, which will prune\njobs older than the default of 60 seconds:\n\n    config :my_app, Oban,\n      plugins: [Oban.Plugins.Pruner],\n      ...\n\nOverride the default options to prune jobs after 5 minutes:\n\n    config :my_app, Oban,\n      plugins: [{Oban.Plugins.Pruner, max_age: 300}],\n      ...\n\n> #### \ud83c\udf1f DynamicPruner {: .info}\n>\n> To prune on a cron-style schedule, retain jobs by a limit, or provide overrides for specific\n> queues, workers, and job states; see Oban Pro's [DynamicPruner](https://getoban.pro/docs/pro/Oban.Pro.Plugins.DynamicPruner.html).\n\n## Options\n\n* `:interval` \u2014 the number of milliseconds between pruning attempts. The default is `30_000ms`.\n\n* `:limit` \u2014 the maximum number of jobs to prune at one time. The default is 10,000 to prevent\n  request timeouts. Applications that steadily generate more than 10k jobs a minute should\n  increase this value.\n\n* `:max_age` \u2014 the number of seconds after which a job may be pruned. Defaults to 60s.\n\n## Instrumenting with Telemetry\n\nThe `Oban.Plugins.Pruner` plugin adds the following metadata to the `[:oban, :plugin, :stop]` event:\n\n* `:pruned_jobs` - the jobs that were deleted from the database\n\n_Note: jobs only include `id`, `queue`, `state` fields._\n"
                },
                "source_path": "lib/oban/plugins/pruner.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/pruner.ex#L1",
                "title": "Oban.Plugins.Pruner",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 50,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/pruner.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/pruner.ex#L50",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Reindexer",
                "nested_title": null,
                "source_doc": {
                    "en": "Periodically rebuild indexes to minimize database bloat.\n\nOver time various Oban indexes may grow without `VACUUM` cleaning them up properly. When this\nhappens, rebuilding the indexes will release bloat.\n\nThe plugin uses `REINDEX` with the `CONCURRENTLY` option to rebuild without taking any locks\nthat prevent concurrent inserts, updates, or deletes on the table.\n\nNote: This plugin requires the `CONCURRENT` option, which is only available in Postgres 12 and\nabove.\n\n## Using the Plugin\n\nBy default, the plugin will reindex once a day, at midnight UTC:\n\n    config :my_app, Oban,\n      plugins: [Oban.Plugins.Reindexer],\n      ...\n\nTo run on a different schedule you can provide a cron expression. For example, you could use the\n`\"@weekly\"` shorthand to run once a week on Sunday:\n\n    config :my_app, Oban,\n      plugins: [{Oban.Plugins.Reindexer, schedule: \"@weekly\"}],\n      ...\n\n## Options\n\n  * `:indexes` \u2014 a list of indexes to reindex on the `oban_jobs` table. Defaults to only the\n    `oban_jobs_args_index` and `oban_jobs_meta_index`.\n\n  * `:schedule` \u2014 a cron expression that controls when to reindex. Defaults to `\"@midnight\"`.\n\n  * `:timeout` - time in milliseconds to wait for each query call to finish. Defaults to 15 seconds.\n\n  * `:timezone` \u2014 which timezone to use when evaluating the schedule. To use a timezone other than\n    the default of \"Etc/UTC\" you *must* have a timezone database like [tz][tz] installed and\n    configured.\n\n[tz]: https://hexdocs.pm/tz\n"
                },
                "source_path": "lib/oban/plugins/reindexer.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/reindexer.ex#L1",
                "title": "Oban.Plugins.Reindexer",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 53,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugins/reindexer.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/reindexer.ex#L53",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": "Superseded by hybrid staging",
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Plugins",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugins.Repeater",
                "nested_title": null,
                "source_doc": {
                    "en": "Forced polling mode for local queues.\n\nThis plugin is superseded by the new hybrid staging.\n"
                },
                "source_path": "lib/oban/plugins/repeater.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugins/repeater.ex#L1",
                "title": "Oban.Plugins.Repeater",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Config",
                "nested_title": null,
                "source_doc": {
                    "en": "The Config struct validates and encapsulates Oban instance state.\n\nTypically, you won't use the Config module directly. Oban automatically creates a Config struct\non initialization and passes it through to all supervised children with the `:conf` key.\n\nTo fetch a running Oban supervisor's config, see `Oban.config/1`.\n"
                },
                "source_path": "lib/oban/config.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/config.ex#L1",
                "title": "Oban.Config",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 13,
                        "name": "t",
                        "rendered_doc": null,
                        "signature": "t()",
                        "source_doc": "none",
                        "source_path": "lib/oban/config.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/config.ex#L13",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Engine",
                "nested_title": null,
                "source_doc": {
                    "en": "Defines an Engine for job orchestration.\n\nEngines are responsible for all non-plugin database interaction, from inserting through\nexecuting jobs.\n\nOban ships with three Engine implementations:\n\n1. `Basic` \u2014 The default engine for development, production, and manual testing mode.\n2. `Inline` \u2014 Designed specifically for testing, it executes jobs immediately, in-memory, as\n   they are inserted.\n3. `Lite` - The engine for running Oban using SQLite3.\n\n> #### \ud83c\udf1f SmartEngine {: .info}\n>\n> The Basic engine lacks advanced functionality such as global limits, rate limits, and\n> unique bulk insert. For those features and more, see the [`Smart` engine in Oban\n> Pro](https://getoban.pro/docs/pro/Oban.Pro.Engines.Smart.html).\n"
                },
                "source_path": "lib/oban/engine.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L1",
                "title": "Oban.Engine",
                "type": "behaviour",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 25,
                        "name": "conf",
                        "rendered_doc": null,
                        "signature": "conf()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L25",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 26,
                        "name": "job",
                        "rendered_doc": null,
                        "signature": "job()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L26",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 27,
                        "name": "meta",
                        "rendered_doc": null,
                        "signature": "meta()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L27",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 28,
                        "name": "opts",
                        "rendered_doc": null,
                        "signature": "opts()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L28",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 29,
                        "name": "queryable",
                        "rendered_doc": null,
                        "signature": "queryable()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L29",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 30,
                        "name": "running",
                        "rendered_doc": null,
                        "signature": "running()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L30",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 31,
                        "name": "seconds",
                        "rendered_doc": null,
                        "signature": "seconds()",
                        "source_doc": "none",
                        "source_path": "lib/oban/engine.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engine.ex#L31",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Notifier",
                "nested_title": null,
                "source_doc": {
                    "en": "The `Notifier` coordinates listening for and publishing notifications for events in predefined\nchannels.\n\nEvery Oban supervision tree contains a notifier process, registered as `Oban.Notifier`, which\nmust be an implementation of the `Oban.Notifier` behaviour. The default implementation uses\nthe `LISTEN/NOTIFY` operations built into Postgres.\n\nAll incoming notifications are relayed through the notifier to other processes.\n\n## Channels\n\nInternally, Oban uses a variety of predefined channels with distinct responsibilities:\n\n* `insert` \u2014 as jobs are inserted into the database an event is published on the `insert`\n  channel. Processes such as queue producers use this as a signal to dispatch new jobs.\n\n* `leader` \u2014 messages regarding node leadership exchanged between peers\n\n* `signal` \u2014 instructions to take action, such as scale a queue or kill a running job, are sent\n  through the `signal` channel\n\n* `gossip` \u2014 arbitrary communication for coordination between nodes\n\n* `stager` \u2014 messages regarding job staging, e.g. notifying queues that jobs are ready for execution\n\n## Examples\n\nBroadcasting after a job is completed:\n\n    defmodule MyApp.Worker do\n      use Oban.Worker\n\n      @impl Oban.Worker\n      def perform(job) do\n        :ok = MyApp.do_work(job.args)\n\n        Oban.Notifier.notify(Oban, :my_app_jobs, %{complete: job.id})\n\n        :ok\n      end\n    end\n\nListening for job complete events from another process:\n\n    def insert_and_listen(args) do\n      :ok = Oban.Notifier.listen([:my_app_jobs])\n\n      {:ok, %{id: job_id} = job} =\n        args\n        |> MyApp.Worker.new()\n        |> Oban.insert()\n\n      receive do\n        {:notification, :my_app_jobs, %{\"complete\" => ^job_id}} ->\n          IO.puts(\"Other job complete!\")\n      after\n        30_000 ->\n          IO.puts(\"Other job didn't finish in 30 seconds!\")\n      end\n    end\n"
                },
                "source_path": "lib/oban/notifier.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifier.ex#L1",
                "title": "Oban.Notifier",
                "type": "behaviour",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 69,
                        "name": "channel",
                        "rendered_doc": null,
                        "signature": "channel()",
                        "source_doc": "none",
                        "source_path": "lib/oban/notifier.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifier.ex#L69",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 68,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/notifier.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifier.ex#L68",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 67,
                        "name": "server",
                        "rendered_doc": null,
                        "signature": "server()",
                        "source_doc": "none",
                        "source_path": "lib/oban/notifier.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifier.ex#L67",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Peer",
                "nested_title": null,
                "source_doc": {
                    "en": "The `Peer` module maintains leadership for a particular Oban instance within a cluster.\n\nLeadership is used by plugins, primarily, to prevent duplicate work accross nodes. For example,\nonly the leader's `Cron` plugin will try inserting new jobs. You can use peer leadership to\nextend Oban with custom plugins, or even within your own application.\n\nNote a few important details about how peer leadership operates:\n\n* Each peer checks for leadership at a 30 second interval. When the leader exits it broadcasts a\n  message to all other peers to encourage another one to assume leadership.\n\n* Each Oban instance supervises a distinct `Oban.Peer` instance. That means that with multiple\n  Oban instances on the same node one instance may be the leader, while the others aren't.\n\n* Without leadership, global plugins (Cron, Lifeline, Stager, etc.), will not run on any node.\n\n## Available Peer Implementations\n\nThere are two built-in peering modules:\n\n* `Oban.Peers.Postgres` \u2014 uses table-based leadership through the `oban_peers` table and works\n  in any environment, with or without clustering. Only one node (per instance name) will have a\n  row in the peers table, that node is the leader. This is the default.\n\n* `Oban.Peers.Global` \u2014 coordinates global locks through distributed Erlang, requires\n  distributed Erlang.\n\nYou can specify the peering module to use in your Oban configuration:\n\n    config :my_app, Oban,\n      peer: Oban.Peers.Postgres, # default value\n      ...\n\nIf in doubt, you can call `Oban.config()` to see which module is being used.\n\n## Examples\n\nCheck leadership for the default Oban instance:\n\n    Oban.Peer.leader?()\n    # => true\n\nThat is identical to using the name `Oban`:\n\n    Oban.Peer.leader?(Oban)\n    # => true\n\nCheck leadership for a couple of instances:\n\n    Oban.Peer.leader?(Oban.A)\n    # => true\n\n    Oban.Peer.leader?(Oban.B)\n    # => false\n"
                },
                "source_path": "lib/oban/peer.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/peer.ex#L1",
                "title": "Oban.Peer",
                "type": "behaviour",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 63,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/peer.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/peer.ex#L63",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Plugin",
                "nested_title": null,
                "source_doc": {
                    "en": "Defines a shared behaviour for Oban plugins.\n\nIn addition to implementing the Plugin behaviour, all plugins **must** be a `GenServer`, `Agent`, or\nanother OTP compliant module.\n\n## Example\n\nDefining a basic plugin that satisfies the minimum behaviour:\n\n    defmodule MyPlugin do\n      @behaviour Oban.Plugin\n\n      use GenServer\n\n      @impl Oban.Plugin\n      def start_link(opts) do\n        GenServer.start_link(__MODULE__, opts, name: opts[:name])\n      end\n\n      @impl Oban.Plugin\n      def validate(opts) do\n        if is_atom(opts[:mode])\n          :ok\n        else\n          {:error, \"expected opts to have a :mode key\"}\n        end\n      end\n\n      @impl GenServer\n      def init(opts) do\n        case validate(opts) do\n          :ok -> {:ok, opts}\n          {:error, reason} -> {:stop, reason}\n        end\n      end\n    end\n"
                },
                "source_path": "lib/oban/plugin.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugin.ex#L1",
                "title": "Oban.Plugin",
                "type": "behaviour",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 43,
                        "name": "option",
                        "rendered_doc": null,
                        "signature": "option()",
                        "source_doc": "none",
                        "source_path": "lib/oban/plugin.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/plugin.ex#L43",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Registry",
                "nested_title": null,
                "source_doc": {
                    "en": "Local process storage for Oban instances.\n"
                },
                "source_path": "lib/oban/registry.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/registry.ex#L1",
                "title": "Oban.Registry",
                "type": "module",
                "typespecs": [
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 7,
                        "name": "key",
                        "rendered_doc": null,
                        "signature": "key()",
                        "source_doc": "none",
                        "source_path": "lib/oban/registry.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/registry.ex#L7",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 6,
                        "name": "role",
                        "rendered_doc": null,
                        "signature": "role()",
                        "source_doc": "none",
                        "source_path": "lib/oban/registry.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/registry.ex#L6",
                        "type": "type"
                    },
                    {
                        "annotations": [],
                        "arity": 0,
                        "deprecated": null,
                        "doc_line": 8,
                        "name": "value",
                        "rendered_doc": null,
                        "signature": "value()",
                        "source_doc": "none",
                        "source_path": "lib/oban/registry.ex",
                        "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/registry.ex#L8",
                        "type": "type"
                    }
                ]
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Extending",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Repo",
                "nested_title": null,
                "source_doc": {
                    "en": "Wrappers around `Ecto.Repo` and `Ecto.Adapters.SQL` callbacks.\n\nEach function resolves the correct repo instance and sets options such as `prefix` and `log`\naccording to `Oban.Config`.\n\n> #### Meant for Extending Oban {: .warning}\n>\n> These functions should only be used when working with a repo inside engines, plugins, or other\n> extensions for Oban. Favor using your application's repo directly when querying `Oban.Job`\n> from your workers.\n\n## Examples\n\nThe first argument for every function must be an `Oban.Config` struct. Many functions pass\nconfiguration around as a `conf` key, and it can always be fetched with `Oban.config/1`. This\ndemonstrates fetching the default instance config and querying all jobs:\n\n    Oban\n    |> Oban.config()\n    |> Oban.Repo.all(Oban.Job)\n"
                },
                "source_path": "lib/oban/repo.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/repo.ex#L1",
                "title": "Oban.Repo",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Notifiers",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Notifiers.PG",
                "nested_title": null,
                "source_doc": {
                    "en": "A PG/PG2 based notifier implementation that runs with Distributed Erlang.\n\nOut of the box, Oban uses PostgreSQL's `LISTEN/NOTIFY` for PubSub. For most applications, that\nis fine, but Postgres-based PubSub isn't sufficient in some circumstances. In particular,\nPostgres notifications won't work when your application connects through PGbouncer in\n_transaction_ or _statement_ mode.\n\n_Note: You must be using [Distributed Erlang][de] to use the PG notifier._\n\n## Usage\n\nSpecify the `PG` notifier in your Oban configuration:\n\n```elixir\nconfig :my_app, Oban,\n  notifier: Oban.Notifiers.PG,\n  ...\n```\n\n## Implementation Notes\n\n* The notifier will use `pg` if available (OTP 23+) or fall back to `pg2` for\n  older OTP releases.\n\n* Like the Postgres implementation, notifications are namespaced by `prefix`.\n\n* For compatibility, message payloads are always serialized to JSON before\n  broadcast and deserialized before relay to local processes.\n\n## Migrating from `Oban.Notifiers.Postgres`\n\nAfter switching from `Oban.Notifiers.Postgres`, you may remove the unused `oban_notify` trigger.\nUse the following migration to drop the trigger while retaining the `oban_jobs_notify` function:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.DropObanJobsNotifyTrigger do\n  use Ecto.Migration\n\n  def change do\n    execute(\n      \"DROP TRIGGER IF EXISTS oban_notify ON public.oban_jobs\",\n      \"CREATE TRIGGER oban_notify AFTER INSERT ON public.oban_jobs FOR EACH ROW EXECUTE PROCEDURE public.oban_jobs_notify()\"\n    )\n  end\nend\n```\n\n[de]: https://elixir-lang.org/getting-started/mix-otp/distributed-tasks.html#our-first-distributed-code\n"
                },
                "source_path": "lib/oban/notifiers/pg.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifiers/pg.ex#L1",
                "title": "Oban.Notifiers.PG",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 3,
                "group": "Notifiers",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Notifiers.Postgres",
                "nested_title": null,
                "source_doc": {
                    "en": "A Postgres Listen/Notify based Notifier.\n\n## Usage\n\nSpecify the `Postgres` notifier in your Oban configuration:\n\n    config :my_app, Oban,\n      notifier: Oban.Notifiers.Postgres\n\n### Transactions and Testing\n\nThe notifications system is built on PostgreSQL's `LISTEN/NOTIFY` functionality. Notifications\nare only delivered **after a transaction completes** and are de-duplicated before publishing.\nTypically, applications run Ecto in sandbox mode while testing, but sandbox mode wraps each test\nin a separate transaction that's rolled back after the test completes. That means the\ntransaction is never committed, which prevents delivering any notifications.\n\nTo test using notifications you must run Ecto without sandbox mode enabled, or use\n`Oban.Notifiers.PG` instead.\n\n### Triggers and Scaling\n\nA database trigger is used to dispatch notifications to relevant queues as jobs are inserted\ninto the database. The trigger uses `pg_notify` to send a notification for each distinct\nqueue, and `pg_notify` relies on a de-duplication mechanism with `O(N^2)` complexity. At high\nload, e.g. thousands or more job inserts per second, notification de-duplication may become a\nbottleneck.\n\nThe trigger mechanism is designed to make jobs execute immediately after insert, rather than\nup to 1 second afterwards, and it can safely be disabled to improve insert throughput. Use the\nfollowing migration to drop the trigger:\n\n```elixir\ndefmodule MyApp.Repo.Migrations.DropObanJobsNotifyTrigger do\n  use Ecto.Migration\n\n  def change do\n    execute(\n      \"DROP TRIGGER IF EXISTS oban_notify ON public.oban_jobs\",\n      \"CREATE TRIGGER oban_notify AFTER INSERT ON public.oban_jobs FOR EACH ROW EXECUTE PROCEDURE public.oban_jobs_notify()\"\n    )\n  end\nend\n```\n"
                },
                "source_path": "lib/oban/notifiers/postgres.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/notifiers/postgres.ex#L2",
                "title": "Oban.Notifiers.Postgres",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Peers",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Peers.Global",
                "nested_title": null,
                "source_doc": {
                    "en": "A cluster based peer that coordinates through a distributed registry.\n\nLeadership is coordinated through global locks. It requires a functional distributed Erlang\ncluster, without one global plugins (Cron, Lifeline, etc.) will not function correctly.\n\n## Usage\n\nSpecify the `Global` peer in your Oban configuration.\n\n    config :my_app, Oban,\n      peer: Oban.Peers.Global,\n      ...\n"
                },
                "source_path": "lib/oban/peers/global.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/peers/global.ex#L1",
                "title": "Oban.Peers.Global",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Peers",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Peers.Postgres",
                "nested_title": null,
                "source_doc": {
                    "en": "A Postgres based peer that coordinates centrally through a database table.\n\nPostgres peers don't require clustering through distributed Erlang or any other\ninterconnectivity between nodes. Leadership is coordinated through the `oban_peers` table in\nyour database. With a standard Oban config the `oban_peers` table will only have one row, and\nthat node is the leader.\n\nApplications that run multiple Oban instances will have one row per instance. For example, an\numbrella application that runs `Oban.A` and `Oban.B` will have two rows in `oban_peers`.\n\n## Usage\n\nSpecify the `Postgres` peer in your Oban configuration.\n\n    config :my_app, Oban,\n      peer: Oban.Peers.Postgres,\n      ...\n"
                },
                "source_path": "lib/oban/peers/postgres.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/peers/postgres.ex#L1",
                "title": "Oban.Peers.Postgres",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Engines",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Engines.Basic",
                "nested_title": null,
                "source_doc": {
                    "en": "The default engine for use with Postgres databases.\n\n## Usage\n\nThis is the default engine, no additional configuration is necessary:\n\n    Oban.start_link(repo: MyApp.Repo, queues: [default: 10])\n"
                },
                "source_path": "lib/oban/engines/basic.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engines/basic.ex#L1",
                "title": "Oban.Engines.Basic",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Engines",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Engines.Inline",
                "nested_title": null,
                "source_doc": {
                    "en": "A testing-specific engine that's used when Oban's started with `testing: :inline`.\n\n## Usage\n\nThis is meant for testing and shouldn't be configured directly:\n\n    Oban.start_link(repo: MyApp.Repo, testing: :inline)\n"
                },
                "source_path": "lib/oban/engines/inline.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engines/inline.ex#L1",
                "title": "Oban.Engines.Inline",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Engines",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.Engines.Lite",
                "nested_title": null,
                "source_doc": {
                    "en": "An engine for running Oban with SQLite3.\n\n## Usage\n\nStart an `Oban` instance using the `Lite` engine:\n\n    Oban.start_link(\n      engine: Oban.Engines.Lite,\n      queues: [default: 10],\n      repo: MyApp.Repo\n    )\n"
                },
                "source_path": "lib/oban/engines/lite.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/engines/lite.ex#L1",
                "title": "Oban.Engines.Lite",
                "type": "module",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 2,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.CrashError",
                "nested_title": null,
                "source_doc": {
                    "en": "Wraps unhandled exits and throws that occur during job execution.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L1",
                "title": "Oban.CrashError",
                "type": "exception",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 17,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.PerformError",
                "nested_title": null,
                "source_doc": {
                    "en": "Wraps the reason returned by `{:error, reason}`, `{:cancel, reason}`, or `{:discard, reason}` in\na proper exception.\n\nThe original return tuple is available in the `:reason` key.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L16",
                "title": "Oban.PerformError",
                "type": "exception",
                "typespecs": []
            },
            {
                "annotations": [],
                "deprecated": null,
                "doc_format": "text/markdown",
                "doc_line": 37,
                "group": "Exceptions",
                "language": "Elixir.ExDoc.Language.Elixir",
                "module": "Elixir.Oban.TimeoutError",
                "nested_title": null,
                "source_doc": {
                    "en": "Returned when a job is terminated early due to a custom timeout.\n"
                },
                "source_path": "lib/oban/exceptions.ex",
                "source_url": "https://github.com/sorentwo/oban/blob/v2.16.2/lib/oban/exceptions.ex#L36",
                "title": "Oban.TimeoutError",
                "type": "exception",
                "typespecs": []
            }
        ],
        "protocols": [],
        "tasks": []
    },
    "language": "en",
    "name": "Oban",
    "version": "2.16.2"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment