GoodJob Documentation

repository·main·Indexed 25 days ago

https://github.com/bensheldon/good_job

A multithreaded, Postgres-based Active Job backend for Ruby on Rails. It utilizes Advisory Locks and LISTEN/NOTIFY for performance and reliability. Features include a mountable dashboard, concurrency controls via labels, multiple execution modes (inline, external, async, async_all), and support for Rails 6.1+, Ruby 3.0+, and Postgres 10.0+.

Tokens
12.1K
Snippets
37
Records
58
Agent score
84%

What's inside GoodJob

  1. Install and set up GoodJob

    main

    To integrate GoodJob into your Rails application, follow these steps:

    1. Add the gem to your Gemfile and install:
      bundle add good_job
    2. Run the installation generator to create the necessary database migrations:
      bin/rails g good_job:install
      Note: If using Rails multiple databases with migrations_paths, use the --database flag, e.g., bin/rails g good_job:install --database animals.
    3. Run the migrations:
      bin/rails db:migrate
    4. Configure Active Job to use the GoodJob adapter in config/application.rb or your environment-specific config files:
      config.active_job.queue_adapter = :good_job
    bundle add good_job
    bin/rails g good_job:install
    bin/rails db:migrate
  2. Set up the GoodJob gem development environment

    main

    To develop the GoodJob gem itself, clone the repository and run the provided setup script to prepare the environment.

    # Clone the repository locally
    git clone git@github.com:bensheldon/good_job.git
    
    # Set up the gem development environment
    bin/setup
  3. Run GoodJob in production

    main

    In production, GoodJob typically runs in external mode, meaning jobs are executed by a separate process from your web server. Use the GoodJob CLI to start the worker process:

    bundle exec good_job start

    For example, in a Heroku Procfile:

    web: rails server
    worker: bundle exec good_job start

    Alternatively, you can run jobs within the web server process (useful for low-workloads) by setting the GOOD_JOB_EXECUTION_MODE environment variable to async:

    GOOD_JOB_EXECUTION_MODE=async rails server
  4. Understand GoodJob concurrency control strategy

    main

    GoodJob uses an "optimistic retry with an incremental backoff" strategy for perform_limit:

    1. Optimistic: Assumes collisions are atypical. For high-collision scenarios, manage concurrency via the number of GoodJob threads/processes (e.g., good_job --queues "serial:1;-serial:5").
    2. Retry with Backoff: When perform_limit is exceeded, a GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError is raised. This is caught by a retry_on handler which re-schedules the job with incremental backoff.
    3. Ordering: First-in-first-out (FIFO) order is not preserved when a job is retried via backoff.
  5. Upgrade GoodJob database migrations

    main

    To upgrade GoodJob database tables during minor version updates, follow these steps:

    1. Generate migration files: bin/rails g good_job:update (If using Rails multiple databases, use --database <name>)
    2. Run the migration locally: bin/rails db:migrate
    3. Commit the migration files and db/schema.rb.
    4. Deploy and run migrations against production, then restart server/worker processes.
    bin/rails g good_job:update
    # or
    bin/rails g good_job:update --database animals
    
    bin/rails db:migrate
  6. Configure GoodJob for development environment

    main

    In development, GoodJob defaults to async mode, executing jobs in a background thread pool within rails server.

    To prevent issues with Rails deferred autoloading (where jobs enqueued via rails console might not run until a web page is loaded), add this to an initializer to force early initialization:

    # config/initializers/good_job.rb
    Rails.configuration.after_initialize do
      ActiveJob::Base && ActiveRecord::Base
    end
  7. Extend GoodJob Dashboard views

    main

    You can override specific Dashboard views by placing custom partials in your application. Note that these partials expose internal classes like GoodJob::Job and should be tested after upgrades.

    Available partials:

    • app/views/good_job/_custom_head.html.erb: Injected at the end of the <head> tag.
    • app/views/good_job/_custom_job_details.html.erb: Displayed above the argument list on the job show page.
    • app/views/good_job/_custom_execution_details.html.erb: Displayed above each execution on the job show page.
    <%# app/views/good_job/_custom_job_details.html.erb %>
    <% arguments = job.active_job.arguments rescue [] %>
    <% widgets = arguments.select { |arg| arg.is_a?(Widget) } %>
    <% if widgets.any? %>
      <div class="my-4">
        <h5 class="font-bold">Widgets</h5>
        <ul>
          <% widgets.each do |widget| %>
            <li><%= link_to widget.name, main_app.widget_url(widget) %></li>
          <% end %>
        </ul>
      </div>
    <% end %>
  8. Configure GoodJob to use a direct database connection for PgBouncer

    main

    If you use PgBouncer in connection mode, GoodJob is compatible. However, if you use PgBouncer in transaction mode, you can work around it by providing a direct (non-proxied) connection to GoodJob using Rails multiple databases support.

    1. Define a primary_direct connection in database.yml.
    2. Create an abstract ApplicationDirectRecord < ActiveRecord::Base that connects_to database: :primary_direct.
    3. Set GoodJob.active_record_parent_class = "ApplicationDirectRecord" in an initializer.
    # config/initializers/good_job.rb
    GoodJob.active_record_parent_class = "ApplicationDirectRecord"
  9. Upgrade from GoodJob v3 to v4

    main

    GoodJob v4 changes how job and execution records are stored (moving executions to a discrete good_job_executions table).

    Prerequisites:

    • All unfinished jobs must use the new format.
    • You should have already applied migrations from v3.x using bin/rails g good_job:update.

    Upgrade Steps:

    1. Upgrade to v3.99.x and run all remaining migrations.
    2. Verify readiness. Run GoodJob.v4_ready? in a production console. It must return true. Alternatively, verify via SQL: SELECT COUNT(*) FROM "good_jobs" WHERE finished_at IS NULL AND is_discrete IS NOT TRUE should return 0.
    3. Upgrade from v3.99.x to v4.x.

    Notable v4 Changes:

    • Supports Rails 6.1+, CRuby 3.0+, and JRuby 9.4+.
    • Job priority follows Active Job definition (smaller numbers = higher priority; default 0).
    • Uses GoodJob::Job model for enqueuing/executing.
    • To disable cleanups, set config.good_job.cleanup_interval_jobs (or env GOOD_JOB_CLEANUP_INTERVAL_JOBS) to false. Setting to nil or "" no longer disables them.
  10. Migrate to GoodJob from another Active Job backend

    main

    To migrate without losing jobs:

    1. Set ActiveJob::Base.queue_adapter = :good_job (or set it on specific job classes) to enqueue new jobs into GoodJob.
    2. Keep running executors for both the old backend and GoodJob simultaneously.
    3. Once the old backend's queue is empty, remove the old configuration and executors.
    # jobs/specific_job.rb
    class SpecificJob < ApplicationJob
      self.queue_adapter = :good_job
      # ...
    end
  11. Optimize job performance using latency-based queues

    main

    To achieve predictable performance and avoid 'head-of-line blocking' (where a long-running job prevents short jobs from starting), GoodJob recommends organizing jobs into isolated thread pools based on their total latency target (queuing latency + execution latency) rather than functional names (like mailers or sms).

    Best Practices

    • Avoid functional names: Instead of mailers, use names like latency_30s or latency_5m.
    • Group by latency: Categorize jobs into 'Mice' (fast/small) and 'Elephants' (slow/big) to ensure small tasks aren't stuck behind large ones.
    • Use isolated thread pools: Create dedicated pools for different latency tiers so that an 'Elephant' in one pool cannot block a 'Mouse' in another.
    • Scale based on queue latency: If jobs are missing their latency targets due to queuing delays, increase capacity (processes or threads).
  12. Use SKIP LOCKED lock strategy for PgBouncer compatibility

    main

    If you use PgBouncer in transaction mode, you must use the :skiplocked or :hybrid lock strategy. :skiplocked uses SELECT FOR UPDATE SKIP LOCKED instead of advisory locks.

    When using :skiplocked with PgBouncer transaction mode, you must also disable LISTEN/NOTIFY and the advisory lock heartbeat, and rely on polling instead.

    # config/initializers/good_job.rb
    GoodJob.configure do |config|
      config.lock_strategy = :skiplocked
      config.enable_listen_notify = false
      config.advisory_lock_heartbeat = false
      config.poll_interval = 5 # seconds
    end