Oban Documentation

repository·main·Indexed 26 days ago

https://github.com/oban-bg/oban

A robust, reliable, and observable background job processing library for Elixir. Oban uses SQL databases (PostgreSQL, MySQL, or SQLite3) to manage job queues, ensuring transactional integrity by enqueuing jobs atomically with other database changes. It supports advanced scaling, clustered environments via Distributed Erlang, and a licensed Pro version featuring workflows, batches, and global rate limiting.

Tokens
27.7K
Snippets
101
Records
148
Agent score
87%

What's inside Oban

  1. Understand Oban Clustering Modes

    main

    Oban supports running in clusters of nodes using two primary communication methods:

    1. Global Mode: Nodes are connected via distributed Erlang. Oban notifies queues of available jobs via pub/sub to minimize database load.
    2. Local Mode: Nodes are not connected via distributed Erlang. Each queue polls the database independently. This mode is less efficient and is used as a fallback if pub/sub is unavailable.

    When does Oban switch to Local Mode? Oban switches to polling in local mode if both of the following conditions are met:

    • You are running with a connection pooler (like pg_bouncer) in transaction mode.
    • You are running without clustering (no distributed Erlang).

    If pub/sub notifications are unavailable due to these conditions, Oban defaults to local mode to ensure job processing continues.

  2. Oban Pro features

    main

    Oban Pro is a licensed set of extensions for advanced job processing needs. Key features include:

    • Smart Engine: Global concurrency, global rate limiting, and queue partitioning.
    • Pro Worker: Execution hooks, structured args, output recording, and encrypted args.
    • Workflows: Sequential, fan-out, and fan-in execution patterns.
    • Batches: Tracking progress across nodes with callbacks.
    • Dynamic Plugins: Runtime configuration of cron schedules and scaling.
    • Decorator: Inserting jobs directly from regular functions.
  3. Attach the default Oban structured logger

    main

    Oban provides a built-in structured logger via the Oban.Telemetry module. This logger handles all Oban telemetry events. To enable it, call Oban.Telemetry.attach_default_logger/0 within your application's startup process.

    :ok = Oban.Telemetry.attach_default_logger()
  4. Implement recursive jobs for data backfilling

    main

    Recursive jobs are a pattern where a worker enqueues a new version of itself after executing. This is useful for backfilling large datasets, especially when tasks involve external services with rate limits, heavy database pressure, or long execution times that might be interrupted by deployments.

    To implement a recursive job:

    1. Define multiple clauses for perform/1. One clause handles the core logic, and another handles the recursion logic.
    2. In the recursion clause, check if the work was successful.
    3. If successful, find the next item to process (e.g., via a database query).
    4. If a next item exists, enqueue a new instance of the worker using new(scheduled_in: delay) to alleviate queue pressure.
    5. If no items remain, return :ok to stop the recursion.
    defmodule MyApp.Workers.TimezoneWorker do
      use Oban.Worker
    
      import Ecto.Query
      alias MyApp.{Repo, User}
    
      @backfill_delay 1
    
      @impl true
      def perform(%{args: %{"id" => id, "backfill" => true}}) do
        with :ok <- perform(%{args: %{"id" => id}}) do
          case fetch_next(id) do
            next_id when is_integer(next_id) ->
              %{id: next_id, backfill: true}
              |> new(scheduled_in: @backfill_delay)
              |> Oban.insert()
    
            nil ->
              :ok
          end
        end
      end
    
      def perform(%{args: %{"id" => id}}) do
        update_timezone(id)
      end
    
      defp fetch_next(current_id) do
        User
        |> where([u], is_nil(u.timezone))
        |> where([u], u.id > ^current_id)
        |> order_by(asc: :id)
        |> limit(1)
        |> select([u], u.id)
        |> Repo.one()
      end
    
      defp update_timezone(_id), do: Enum.random([:ok, {:error, :reason}])
    end
  5. Guidelines for queue planning and resource management

    main

    When designing your queue architecture, follow these best practices:

    Concurrency and Distribution

    • Local Limits: Queue limits are per-node, not per-cluster. If you have a limit of 2 and run 3 nodes, you will have 6 concurrent jobs globally. Use Oban Pro's Smart Engine for true global concurrency management.
    • Resource Exhaustion: Ensure your system (especially database connections) can handle the sum of all queue limits. Total concurrency = sum of all limit values across all queues.

    Workload Organization

    • Workload Characteristics: Group jobs by their resource needs.
      • CPU-intensive: Use dedicated queues with low concurrency.
      • I/O-bound (e.g., emails): Can typically handle higher concurrency.
      • Priority work: Use dedicated queues with higher concurrency.
    • External Processes: If jobs shell out to external tools (like FFMpeg or ImageMagick), use dedicated queues with low concurrency to prevent overwhelming the host system.

    Important Note

    Only jobs in configured queues will execute. Any job submitted to a queue name not present in your queues configuration will remain in the database and will not be processed.

  6. Handle time zones for job scheduling

    main

    Oban performs all scheduling in UTC. If you have a local datetime, you must convert it to UTC using DateTime.shift_zone! before passing it to the scheduled_at option to ensure consistent execution and avoid daylight saving time issues.

    # Convert a datetime in a local timezone to UTC for scheduling
    utc_datetime = DateTime.shift_zone!(local_datetime, "Etc/UTC")
    
    %{id: 1}
    |> MyApp.SomeWorker.new(scheduled_at: utc_datetime)
    |> Oban.insert()
  7. Insert jobs as 'available' or 'scheduled'

    main

    When inserting a job via Oban.insert/2, it will enter one of two initial states:

    1. available: The default state for jobs that should be executed immediately.
    2. scheduled: Occurs when you provide a scheduled_at timestamp or a scheduled_in delay.

    Once a scheduled job's time arrives, it transitions to available before being claimed for executing.

    # Job inserted as "available"
    %{id: 123} |> MyApp.Worker.new() |> Oban.insert()
    
    # Job inserted as "scheduled"
    %{id: 123} |> MyApp.Worker.new(scheduled_in: 60) |> Oban.insert()
  8. Update Oban tests for v2.0

    main
    Use the new perform_job/2 helper in unit tests to replace direct calls to perform/2. This helper validates the worker, arguments, and options. For integration tests, update Oban.drain_queue/3 to Oban.drain_queue/2 by passing arguments as a keyword list.
  9. Implement reliable recursive scheduled jobs

    main

    To implement a job that repeats indefinitely with a fixed interval (e.g., daily digests), use a recursive pattern within the perform/1 function. To ensure you don't schedule duplicate future jobs during retries, use pattern matching on the attempt field of the Oban.Job struct.

    By matching only on attempt: 1, you can schedule the next occurrence of the job before executing the business logic. Subsequent retries will fall through to a different clause that only executes the business logic without rescheduling, providing at-most-once semantics for scheduling and at-least-once semantics for delivery.

    defmodule MyApp.Workers.ScheduledWorker do
      use Oban.Worker, queue: :scheduled, max_attempts: 10
    
      alias MyApp.Mailer
    
      @one_day 60 * 60 * 24
    
      @impl true
      def perform(%{args: %{"email" => email} = args, attempt: 1}) do
        # Schedule the next occurrence only on the first attempt
        args
        |> new(scheduled_in: @one_day)
        |> Oban.insert!()
    
        Mailer.deliver_email(email)
      end
    
      def perform(%{args: %{"email" => email}}) do
        # Subsequent retries only attempt the business logic
        Mailer.deliver_email(email)
      end
    end
  10. Configure the Gossip plugin

    main

    In v2.6, Oban Pro no longer uses the oban_beats table for heartbeats. Instead, any Oban instance running queues must include the Oban.Plugins.Gossip plugin to broadcast status via PubSub. By default, it broadcasts every 1 second. You can adjust this using the interval option.

    # Default configuration (1 second interval)
    config :my_app, Oban,
      plugins: [
        Oban.Plugins.Gossip
        ...
      ]
    
    # Custom interval configuration
    config :my_app, Oban,
      plugins: [
        {Oban.Plugins.Gossip, interval: :timer.seconds(5)}
        ...
      ]
  11. Insert Oban jobs from other languages via PostgreSQL

    main

    Because Oban uses a structured oban_jobs table in PostgreSQL and serializes data using JSON, you can enqueue jobs from any language that has a PostgreSQL adapter without needing an Oban client.

    To successfully insert a job, you must populate the oban_jobs table with the correct data. The most critical fields are:

    • worker: The name of the Elixir worker responsible for the job.
    • args: A JSON object containing the job arguments.
    • queue: The name of the queue (e.g., "default").
    • state: The execution state. Use "available" for jobs that should run immediately, or "scheduled" for jobs intended for the future.
    • scheduled_at: A timestamp used when the state is "scheduled".