{"id":13416331,"url":"https://github.com/bensheldon/good_job","last_synced_at":"2026-05-27T05:01:15.396Z","repository":{"id":37031313,"uuid":"242765280","full_name":"bensheldon/good_job","owner":"bensheldon","description":"Multithreaded, Postgres-based, Active Job backend for Ruby on Rails.","archived":false,"fork":false,"pushed_at":"2026-04-21T19:22:05.000Z","size":13019,"stargazers_count":2970,"open_issues_count":132,"forks_count":243,"subscribers_count":19,"default_branch":"main","last_synced_at":"2026-05-22T19:42:01.718Z","etag":null,"topics":["activejob","activejob-backend","hacktoberfest","multithreaded","rails","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"https://goodjob-demo.herokuapp.com/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bensheldon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["bensheldon"]}},"created_at":"2020-02-24T15:01:22.000Z","updated_at":"2026-05-22T09:49:32.000Z","dependencies_parsed_at":"2022-07-14T03:50:40.793Z","dependency_job_id":"5400ab4a-a7b7-4e39-accc-a8be3f8e38ae","html_url":"https://github.com/bensheldon/good_job","commit_stats":{"total_commits":1168,"total_committers":137,"mean_commits":8.525547445255475,"dds":"0.30308219178082196","last_synced_commit":"956e872d321df21738c599aa1a86a8514e05a7ef"},"previous_names":[],"tags_count":272,"template":false,"template_full_name":null,"purl":"pkg:github/bensheldon/good_job","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensheldon%2Fgood_job","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensheldon%2Fgood_job/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensheldon%2Fgood_job/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensheldon%2Fgood_job/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bensheldon","download_url":"https://codeload.github.com/bensheldon/good_job/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensheldon%2Fgood_job/sbom","scorecard":{"id":112983,"data":{"date":"2025-08-11","repo":{"name":"github.com/bensheldon/good_job","commit":"79b218746bf6e2d1a3a7451c128551fbaaa3269a"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":6.3,"checks":[{"name":"Maintained","score":10,"reason":"20 commit(s) and 6 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":4,"reason":"Found 12/30 approved changesets -- score normalized to 4","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql-analysis.yml:28","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yml:29","Warn: no topLevel permission defined: .github/workflows/codeql-analysis.yml:1","Warn: no topLevel permission defined: .github/workflows/test.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:45: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:60: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:73: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:58: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:62: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:74: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:82: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:139: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:142: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:147: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:176: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:184: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/bensheldon/good_job/test.yml/main?enable=pin","Warn: containerImage not pinned by hash: .devcontainer/Dockerfile:3","Info:   0 out of  11 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   4 third-party GitHubAction dependencies pinned","Info:   0 out of   1 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/bensheldon/.github/SECURITY.md:1","Info: Found linked content: github.com/bensheldon/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/bensheldon/.github/SECURITY.md:1","Info: Found text in security policy: github.com/bensheldon/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":8,"reason":"2 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-353f-x4gh-cqq8","Warn: Project is vulnerable to: GHSA-mqcp-p2hv-vw6x"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":9,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 20 commits out of 21 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-15T15:40:28.711Z","repository_id":37031313,"created_at":"2025-08-15T15:40:28.711Z","updated_at":"2025-08-15T15:40:28.711Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33410345,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T18:09:33.147Z","status":"ssl_error","status_checked_at":"2026-05-23T18:09:31.380Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activejob","activejob-backend","hacktoberfest","multithreaded","rails","ruby","ruby-on-rails"],"created_at":"2024-07-30T21:00:57.205Z","updated_at":"2026-05-27T05:01:15.386Z","avatar_url":"https://github.com/bensheldon.png","language":"Ruby","funding_links":["https://github.com/sponsors/bensheldon"],"categories":["Ruby","Gems","Queues and Messaging","Background Jobs \u0026 Queues"],"sub_categories":["Articles","Postgres-backed Job Queues"],"readme":"# GoodJob\n\n[![Gem Version](https://badge.fury.io/rb/good_job.svg)](https://rubygems.org/gems/good_job)\n[![Test Status](https://github.com/bensheldon/good_job/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/bensheldon/good_job/actions/workflows/test.yml?query=branch%3Amain)\n[![Ruby Toolbox](https://img.shields.io/badge/dynamic/json?color=blue\u0026label=Ruby%20Toolbox\u0026query=%24.projects%5B0%5D.score\u0026url=https%3A%2F%2Fwww.ruby-toolbox.com%2Fapi%2Fprojects%2Fcompare%2Fgood_job\u0026logo=data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJmbGFzayIgY2xhc3M9InN2Zy1pbmxpbmUtLWZhIGZhLWZsYXNrIGZhLXctMTQiIHJvbGU9ImltZyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDQ4IDUxMiI+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik00MzcuMiA0MDMuNUwzMjAgMjE1VjY0aDhjMTMuMyAwIDI0LTEwLjcgMjQtMjRWMjRjMC0xMy4zLTEwLjctMjQtMjQtMjRIMTIwYy0xMy4zIDAtMjQgMTAuNy0yNCAyNHYxNmMwIDEzLjMgMTAuNyAyNCAyNCAyNGg4djE1MUwxMC44IDQwMy41Qy0xOC41IDQ1MC42IDE1LjMgNTEyIDcwLjkgNTEyaDMwNi4yYzU1LjcgMCA4OS40LTYxLjUgNjAuMS0xMDguNXpNMTM3LjkgMzIwbDQ4LjItNzcuNmMzLjctNS4yIDUuOC0xMS42IDUuOC0xOC40VjY0aDY0djE2MGMwIDYuOSAyLjIgMTMuMiA1LjggMTguNGw0OC4yIDc3LjZoLTE3MnoiPjwvcGF0aD48L3N2Zz4=)](https://www.ruby-toolbox.com/projects/good_job)\n\nGoodJob is a multithreaded, Postgres-based, Active Job backend for Ruby on Rails.\n\n**Inspired by [Delayed::Job](https://github.com/collectiveidea/delayed_job) and [Que](https://github.com/que-rb/que), GoodJob is designed for maximum compatibility with Ruby on Rails, Active Job, and Postgres to be simple and performant for most workloads.**\n\n- **Designed for Active Job.** Complete support for [async, queues, delays, priorities, timeouts, and retries](https://edgeguides.rubyonrails.org/active_job_basics.html) with near-zero configuration.\n- **Built for Rails.** Fully adopts Ruby on Rails [threading and code execution guidelines](https://guides.rubyonrails.org/threading_and_code_execution.html) with [Concurrent::Ruby](https://github.com/ruby-concurrency/concurrent-ruby).\n- **Backed by Postgres.** Relies upon Postgres integrity, session-level Advisory Locks to provide run-once safety and stay within the limits of `schema.rb`, and LISTEN/NOTIFY to reduce queuing latency.\n- **Fully featured.** Includes support for cron-like scheduled jobs, batches, concurrency and throttling controls, and a powerful Web Dashboard (check out the [Demo](https://goodjob-demo.herokuapp.com/)).\n- **Flexible and lightweight.** Safely runnable within a single existing web process or scaled via an independent CLI process across development, test, and production environments.\n- **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue 1-million jobs/day and more.\n\nFor more of the story of GoodJob, read the [introductory blog post](https://island94.org/2020/07/introducing-goodjob-1-0).\n\n\u003cdetails markdown=\"1\"\u003e\n\u003csummary\u003e\u003cstrong\u003e📊 Comparison of GoodJob with other job queue backends (click to expand)\u003c/strong\u003e\u003c/summary\u003e\n\n|                 | Queues, priority, retries | Database                              | Concurrency       | Reliability/Integrity  | Latency                  |\n|-----------------|---------------------------|---------------------------------------|-------------------|------------------------|--------------------------|\n| **GoodJob**     | ✅ Yes                     | ✅ Postgres                            | ✅ Multithreaded   | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |\n| **Solid Queue** | ✅ Yes                     | ✅ Postgres and other databases ✨     | 🔶 Multithreaded in forked process   | ✅ ACID, Advisory Locks | 🔶 Polling |\n| **Que**         | ✅ Yes                     | 🔶️ Postgres, requires  `structure.sql` | ✅ Multithreaded   | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |\n| **Delayed Job** | ✅ Yes                     | ✅ Postgres                            | 🔴 Single-threaded | ✅ ACID, record-based   | 🔶 Polling                |\n| **Sidekiq**     | ✅ Yes                     | 🔴 Redis                               | ✅ Multithreaded   | 🔴 Crashes lose jobs    | ✅ Redis BRPOP            |\n| **Sidekiq Pro** | ✅ Yes                     | 🔴 Redis                               | ✅ Multithreaded   | ✅ Redis RPOPLPUSH      | ✅ Redis RPOPLPUSH        |\n\n\u003c/details\u003e\n\n## Table of contents\n\n- [Set up](#set-up)\n- [Compatibility](#compatibility)\n- [Configuration](#configuration)\n    - [Command-line options](#command-line-options)\n        - [`good_job start`](#good_job-start)\n        - [`good_job cleanup_preserved_jobs`](#good_job-cleanup_preserved_jobs)\n    - [Configuration options](#configuration-options)\n    - [Global options](#global-options)\n    - [Dashboard](#dashboard)\n        - [API-only Rails applications](#api-only-rails-applications)\n        - [Live polling](#live-polling)\n        - [Extending dashboard views](#extending-dashboard-views)\n    - [Job priority](#job-priority)\n    - [Concurrency controls](#concurrency-controls)\n        - [How concurrency controls work](#how-concurrency-controls-work)\n    - [Cron-style repeating/recurring jobs](#cron-style-repeatingrecurring-jobs)\n    - [Bulk enqueue](#bulk-enqueue)\n    - [Batches](#batches)\n    - [Updating](#updating)\n        - [Upgrading minor versions](#upgrading-minor-versions)\n        - [Upgrading v3 to v4](#upgrading-v3-to-v4)\n        - [Upgrading v2 to v3](#upgrading-v2-to-v3)\n        - [Upgrading v1 to v2](#upgrading-v1-to-v2)\n- [Go deeper](#go-deeper)\n    - [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)\n        - [Exceptions](#exceptions)\n        - [Retries](#retries)\n        - [Action Mailer retries](#action-mailer-retries)\n        - [Interrupts, graceful shutdown, and SIGKILL](#Interrupts-graceful-shutdown-and-SIGKILL)\n    - [Timeouts](#timeouts)\n    - [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)\n    - [Database connections](#database-connections)\n    - [Production setup](#production-setup)\n    - [Queue performance with Queue Select Limit](#queue-performance-with-queue-select-limit)\n    - [Execute jobs async / in-process](#execute-jobs-async--in-process)\n    - [Migrate to GoodJob from a different Active Job backend](#migrate-to-goodjob-from-a-different-active-job-backend)\n    - [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)\n    - [Write tests](#write-tests)\n    - [PgBouncer compatibility](#pgbouncer-compatibility)\n    - [CLI HTTP health check probes](#cli-http-health-check-probes)\n    - [Pausing Jobs](#pausing-jobs)\n- [Doing your best job with GoodJob](#doing-your-best-job-with-goodjob)\n    - [Sizing jobs: mice and elephants](#sizing-jobs-mice-and-elephants)\n    - [Isolating by total latency](#isolating-by-total-latency)\n    - [Configuring your queues](#configuring-your-queues)\n    - [Additional observations](#additional-observations)\n- [Contribute](#contribute)\n    - [Gem development](#gem-development)\n        - [Development setup](#development-setup)\n        - [Rails development harness](#rails-development-harness)\n        - [Running tests](#running-tests)\n    - [Release](#release)\n- [License](#license)\n\n## Set up\n\n1. Add `good_job` to your application's Gemfile and install the gem:\n\n    ```sh\n    bundle add good_job\n    ```\n\n1. Run the GoodJob install generator. This will generate a database migration to create a table for GoodJob's job records:\n\n    ```bash\n    bin/rails g good_job:install\n    ```\n\n    Run the migration:\n\n    ```bash\n    bin/rails db:migrate\n    ```\n\n   Optional: If using Rails' multiple databases with the `migrations_paths` configuration option, use the `--database` option:\n\n    ```bash\n    bin/rails g good_job:install --database animals\n    bin/rails db:migrate:animals\n    ```\n\n1. Configure the Active Job adapter:\n\n    ```ruby\n    # config/application.rb or config/environments/{RAILS_ENV}.rb\n    config.active_job.queue_adapter = :good_job\n    ```\n\n1. Inside of your application, queue your job 🎉:\n\n    ```ruby\n    YourJob.perform_later\n    ```\n\n    GoodJob supports all Active Job features:\n\n    ```ruby\n    YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later\n    ```\n\n1. **In Rails' development environment**, by default, GoodJob's Adapter executes jobs `async` in a background thread pool in `rails server`.\n    - Because of Rails deferred autoloading, jobs enqueued via the `rails console` may not begin executing on a separate server process until the Rails application is fully initialized by loading a web page once. To force early initialization (so pre-existing jobs run immediately on boot), add the following to an initializer:\n\n        ```ruby\n        # config/initializers/good_job.rb\n        Rails.configuration.after_initialize do\n          ActiveJob::Base \u0026\u0026 ActiveRecord::Base\n        end\n        ```\n\n    - Remember, only Active Job's `perform_later` sends jobs to the queue adapter; Active Job's `perform_now` executes the job immediately and does not invoke the queue adapter. GoodJob is not involved in `perform_now` jobs.\n1. **In Rails' test environment**, by default, GoodJob's Adapter executes jobs `inline` immediately in the current thread.\n    - Future-scheduled jobs can be executed with `GoodJob.perform_inline` using a tool like Timecop or `ActiveSupport::Testing::TimeHelpers`.\n    - Note that Active Job's TestAdapter, which powers test helpers (e.g. `assert_enqueued_with`), may override GoodJob's Adapter in [some configurations](https://github.com/rails/rails/issues/37270).\n1. **In Rails' production environment**, by default, GoodJob's Adapter enqueues jobs in `external` mode to be executed by a separate execution process:\n    - By default, GoodJob separates job enqueuing from job execution so that jobs can be scaled independently of the web server.  Use the GoodJob command-line tool to execute jobs:\n\n        ```bash\n        bundle exec good_job start\n        ```\n\n        Ideally the command-line tool should be run on a separate machine or container from the web process. For example, on Heroku:\n\n        ```Procfile\n        web: rails server\n        worker: bundle exec good_job start\n        ```\n\n        The command-line tool supports a variety of options, see the reference below for command-line configuration.\n\n    - GoodJob can also be configured to execute jobs within the web server process to save on resources. This is useful for low-workloads when economy is paramount.\n\n        ```bash\n        GOOD_JOB_EXECUTION_MODE=async rails server\n        ```\n\n        Additional configuration is likely necessary, see the reference below for configuration.\n\n## Compatibility\n\n- **Ruby on Rails:** 6.1+\n- **Ruby:** Ruby 3.0+. JRuby 9.4+\n- **Postgres:** 10.0+\n\n## Configuration\n\n### Command-line options\n\nThere are several top-level commands available through the `good_job` command-line tool.\n\nConfiguration options are available with `help`.\n\n#### `good_job start`\n\n`good_job start` executes queued jobs.\n\n```bash\n$ bundle exec good_job help start\n\nUsage:\n  good_job start\n\nOptions:\n  [--queues=QUEUE_LIST]           # Queues or pools to work from. (env var: GOOD_JOB_QUEUES, default: *)\n  [--max-threads=COUNT]           # Default number of threads per pool to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)\n  [--poll-interval=SECONDS]       # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 10)\n  [--max-cache=COUNT]             # Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)\n  [--shutdown-timeout=SECONDS]    # Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))\n  [--enable-cron]                 # Whether to run cron process (default: false)\n  [--enable-listen-notify]        # Whether to enqueue and read jobs with Postgres LISTEN/NOTIFY (default: true)\n  [--idle-timeout=SECONDS]        # Exit process when no jobs have been performed for this many seconds (env var: GOOD_JOB_IDLE_TIMEOUT, default: nil)\n  [--daemonize]                   # Run as a background daemon (default: false)\n  [--pidfile=PIDFILE]             # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)\n  [--probe-port=PORT]             # Port for http health check (env var: GOOD_JOB_PROBE_PORT, default: nil)\n  [--probe-handler=PROBE_HANDLER] # Use 'webrick' to use WEBrick to handle probe server requests which is Rack compliant, otherwise default server that is not Rack compliant is used.\n  [--queue-select-limit=COUNT]    # The number of queued jobs to select when polling for a job to run. (env var: GOOD_JOB_QUEUE_SELECT_LIMIT, default: nil)\"\n\nExecutes queued jobs.\n\nAll options can be configured with environment variables.\nSee option descriptions for the matching environment variable name.\n\n== Configuring queues\n\nSeparate multiple queues with commas; exclude queues with a leading minus;\nseparate isolated execution pools with semicolons and threads with colons.\n```\n\n#### `good_job cleanup_preserved_jobs`\n\n`good_job cleanup_preserved_jobs` destroys preserved job records. See `GoodJob.preserve_job_records` for when this command is useful.\n\n```bash\n$ bundle exec good_job help cleanup_preserved_jobs\n\nUsage:\n  good_job cleanup_preserved_jobs\n\nOptions:\n  [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var:  GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))\n\nManually destroys preserved job records.\n\nBy default, GoodJob automatically destroys job records when the job is performed\nand this command is not required to be used.\n```\n\n### Configuration options\n\nActive Job configuration depends on where the code is placed:\n\n- `config.active_job.queue_adapter = :good_job` within `config/application.rb` or `config/environments/*.rb`.\n- `ActiveJob::Base.queue_adapter = :good_job` within an initializer (e.g. `config/initializers/active_job.rb`).\n\nGoodJob configuration can be placed within Rails `config` directory for all environments (`config/application.rb`), within a particular environment (e.g. `config/environments/development.rb`), or within an initializer (e.g. `config/initializers/good_job.rb`).\n\nConfiguration examples:\n\n```ruby\n# config/initializers/good_job.rb OR config/application.rb OR config/environments/{RAILS_ENV}.rb\n\nRails.application.configure do\n  # Configure options individually...\n  config.good_job.preserve_job_records = true\n  config.good_job.retry_on_unhandled_error = false\n  config.good_job.on_thread_error = -\u003e (exception) { Rails.error.report(exception) }\n  config.good_job.execution_mode = :async\n  config.good_job.queues = '*'\n  config.good_job.max_threads = 5\n  config.good_job.poll_interval = 30 # seconds\n  config.good_job.shutdown_timeout = 25 # seconds\n  config.good_job.enable_cron = true\n  config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob'  } }\n  config.good_job.cron_graceful_restart_period = 5.minutes\n  config.good_job.dashboard_default_locale = :en\n\n  # ...or all at once.\n  config.good_job = {\n    preserve_job_records: true,\n    retry_on_unhandled_error: false,\n    on_thread_error: -\u003e (exception) { Rails.error.report(exception) },\n    execution_mode: :async,\n    queues: '*',\n    max_threads: 5,\n    poll_interval: 30,\n    shutdown_timeout: 25,\n    enable_cron: true,\n    cron: {\n      example: {\n        cron: '0 * * * *',\n        class: 'ExampleJob'\n      },\n    },\n    dashboard_default_locale: :en,\n  }\nend\n```\n\nAvailable configuration options are:\n\n- `execution_mode` (symbol) specifies how and where jobs should be executed. You can also set this with the environment variable `GOOD_JOB_EXECUTION_MODE`. It can be any one of:\n    - `:inline` executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.\n    - `:external` causes the adapter to enqueue jobs, but not execute them. When using this option (the default for production environments), you’ll need to use the command-line tool to actually execute your jobs.\n    - `:async` (or `:async_server`) executes jobs in separate threads within the Rails web server process (`bundle exec rails server`). It can be more economical for small workloads because you don’t need a separate machine or environment for running your jobs, but if your web server is under heavy load or your jobs require a lot of resources, you should choose `:external` instead.  When not in the Rails web server, jobs will execute in `:external` mode to ensure jobs are not executed within `rails console`, `rails db:migrate`, `rails assets:prepare`, etc.\n    - `:async_all` executes jobs in separate threads in _any_ Rails process.\n- `queues` (string) sets queues or pools to execute jobs. You can also set this with the environment variable `GOOD_JOB_QUEUES`.\n- `max_threads` (integer) sets the default number of threads per pool to use for working jobs. You can also set this with the environment variable `GOOD_JOB_MAX_THREADS`.\n- `poll_interval` (integer) sets the number of seconds between polls for jobs when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_POLL_INTERVAL`. A poll interval of `-1` disables polling completely.\n    - production default: 10 seconds (in case of a LISTEN/NOTIFY blip)\n    - development default: -1, disabled (because the application is likely being restarted often and won't be running unobserved). You can enable it by setting a `poll_interval`.\n    - LISTEN/NOTIFY is enabled in both production and development, so polling is not strictly necessary.\n    - If LISTEN/NOTIFY is disabled, you should configure polling for future-scheduled jobs. GoodJob will cache in memory the scheduled time and check for executable jobs at that time. If the cache is exceeded (10k scheduled jobs by default) that's another reason to poll just in case.\n- `max_cache` (integer) sets the maximum number of scheduled jobs that will be stored in memory to reduce execution latency when also polling for scheduled jobs. Caching 10,000 scheduled jobs uses approximately 20MB of memory. You can also set this with the environment variable `GOOD_JOB_MAX_CACHE`.\n- `shutdown_timeout` (integer) number of seconds to wait for jobs to finish when shutting down before stopping the thread. Defaults to forever: `-1`. You can also set this with the environment variable `GOOD_JOB_SHUTDOWN_TIMEOUT`.\n- `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`.\n- `cron_graceful_restart_period` (integer) when restarting cron, attempt to re-enqueue jobs that would have been enqueued by cron within this time period (e.g. `1.minute`). This should match the expected downtime during deploys.\n- `enable_listen_notify` (boolean) whether to enqueue and read jobs with Postgres LISTEN/NOTIFY. Defaults to `true`. You can also set this with the environment variable `GOOD_JOB_ENABLE_LISTEN_NOTIFY`.\n- `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`\n- `cleanup_discarded_jobs` (boolean) whether to destroy discarded jobs when cleaning up preserved jobs using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `true`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS`.\n- `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `1209600` (14 days). Can also be set with  the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`.\n- `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `1000`. Disable with `false`. Can also be set with  the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS` and disabled with `0`).\n- `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `600` (10 minutes). Disable with `false`. Can also be set with  the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS` and disabled with `0`).\n- `inline_execution_respects_schedule` (boolean) Opt-in to future behavior of inline execution respecting scheduled jobs. Defaults to `false`.\n- `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).\n- `preserve_job_records` (boolean, symbol, or lambda) keeps job records in your database even after jobs are completed. If set to `true`, all job records are preserved. If set to `:on_unhandled_error`, only jobs that finished with an unhandled error are preserved. If set to a lambda, the lambda will be called with the error_event (e.g., `:discarded`, `:retry_stopped`, or `:unhandled`) and should return a boolean indicating whether to preserve the job. (Default: `true`)\n- `advisory_lock_heartbeat` (boolean) whether to use an advisory lock for the purpose of determining whether an execeution process is active. (Default `true` in Development; `false` in other environments)\n- `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `false`)\n- `on_thread_error` (proc, lambda, or callable) will be called when there is an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:\n\n    ```ruby\n    config.good_job.on_thread_error = -\u003e (exception) { Rails.error.report(exception) }\n    ```\n\n- `probe_server_app` (Rack application) allows you to specify a Rack application to be used for the probe server. Defaults to `nil` which uses the default probe server. Example:\n\n    ```ruby\n    config.good_job.probe_app = -\u003e (env) { [200, {}, [\"OK\"]] }\n    ```\n\n- `probe_handler` (string) allows you to use WEBrick, a fully Rack compliant webserver instead of the simple default server. **Note:** You'll need to ensure WEBrick is in your load path as GoodJob doesn't have WEBrick as a dependency. Example:\n\n    ```ruby\n    config.good_job.probe_handler = 'webrick'\n    ```\n\n- `enable_pauses` (boolean) whether job processing can be paused. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_PAUSES`.\n\nBy default, GoodJob configures the following execution modes per environment:\n\n```ruby\n\n# config/environments/development.rb\nconfig.active_job.queue_adapter = :good_job\nconfig.good_job.execution_mode = :async\n\n# config/environments/test.rb\nconfig.active_job.queue_adapter = :good_job\nconfig.good_job.execution_mode = :inline\n\n# config/environments/production.rb\nconfig.active_job.queue_adapter = :good_job\nconfig.good_job.execution_mode = :external\n```\n\n### Global options\n\nGood Job’s general behavior can also be configured via attributes directly on the `GoodJob` module:\n\n- **`GoodJob.configure_active_record { ... }`** Inject Active Record configuration into GoodJob's base model, for example, when using [multiple databases with Active Record](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the Active Record model to connect to the Postgres database. Example:\n\n    ```ruby\n    # config/initializers/good_job.rb\n    GoodJob.configure_active_record do\n      connects_to database: { writing: :special_database }\n      self.table_name_prefix = \"special_application_\"\n    end\n    ```\n\n- **`GoodJob.active_record_parent_class`** (string) Alternatively, modify the Active Record parent class inherited by GoodJob's Active Record model `GoodJob::Job` (defaults to `\"ActiveRecord::Base\"`). Configure this _The value must be a String to avoid premature initialization of Active Record._\n\nYou’ll generally want to configure these in `config/initializers/good_job.rb`, like so:\n\n```ruby\n# config/initializers/good_job.rb\nGoodJob.active_record_parent_class = \"ApplicationRecord\"\n```\n\nThe following options are also configurable via accessors, but you are encouraged to use the configuration attributes instead because these may be deprecated and removed in the future:\n\n- **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.\n- **`GoodJob.preserve_job_records`** (boolean, symbol, or lambda) keeps job records in your database even after jobs are completed. If set to `true`, all job records are preserved. If set to `:on_unhandled_error`, only jobs that finished with an unhandled error are preserved. If set to a lambda, the lambda will be called with Active Job instance, and if it exists, the exception the error_event (e.g., `:discarded`, `:retry_stopped`, or `:unhandled`) and should return a boolean indicating whether to preserve the job (e.g. `-\u003e (active_job, error, error_event) { !(active_job.is_a(Turbo::Streams::BroadcastStreamJob) || error_event == :retry_stopped`). (Default: `true`)\n- **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `false`)\n- **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when there is an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.\n\n### Dashboard\n\n![Dashboard UI](https://github.com/bensheldon/good_job/raw/main/SCREENSHOT.png)\n\n_🚧 GoodJob's dashboard is a work in progress. Please contribute ideas and code on [Github](https://github.com/bensheldon/good_job/issues)._\n\nGoodJob includes a Dashboard as a mountable `Rails::Engine`.\n\n1. Mount the engine in your `config/routes.rb` file. The following will mount it at `http://example.com/good_job`.\n\n    ```ruby\n    # config/routes.rb\n    # ...\n    mount GoodJob::Engine =\u003e 'good_job'\n    ```\n\n1. Configure authentication. Because jobs can potentially contain sensitive information, you should authorize access. For example, using Devise's `authenticate` helper, that might look like:\n\n    ```ruby\n    # config/routes.rb\n    # ...\n    authenticate :user, -\u003e(user) { user.admin? } do\n      mount GoodJob::Engine =\u003e 'good_job'\n    end\n    ```\n\n    Another option is using basic auth like this:\n\n    ```ruby\n    # config/initializers/good_job.rb\n    GoodJob::Engine.middleware.use(Rack::Auth::Basic) do |username, password|\n      ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.good_job_username, username) \u0026\n        ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.good_job_password, password)\n    end\n    ```\n\n    To support custom authentication, you can extend GoodJob's `ApplicationController` using the following hook:\n\n    ```ruby\n    # config/initializers/good_job.rb\n\n    ActiveSupport.on_load(:good_job_application_controller) do\n      # context here is GoodJob::ApplicationController\n\n      before_action do\n        raise ActionController::RoutingError.new('Not Found') unless current_user\u0026.admin?\n      end\n\n      def current_user\n        # load current user\n      end\n    end\n    ```\n\n_To view finished jobs (succeeded and discarded) on the Dashboard, GoodJob must be configured to preserve job records. Preservation is enabled by default._\n\n**Troubleshooting the Dashboard:** Some applications are unable to autoload the Goodjob Engine. To work around this, explicitly require the Engine at the top of your `config/application.rb` file, immediately after Rails is required and before Bundler requires the Rails' groups.\n\n```ruby\n# config/application.rb\nrequire_relative 'boot'\nrequire 'rails/all'\nrequire 'good_job/engine' # \u003c= Add this line\n# ...\n```\n\n#### API-only Rails applications\n\nAPI-only Rails applications may not have all of the required Rack middleware for the GoodJob Dashboard to function. To re-add the middleware:\n\n```ruby\n# config/application.rb\nmodule MyApp\n  class Application \u003c Rails::Application\n    #...\n    config.middleware.use Rack::MethodOverride\n    config.middleware.use ActionDispatch::Flash\n    config.middleware.use ActionDispatch::Cookies\n    config.middleware.use ActionDispatch::Session::CookieStore\n  end\nend\n```\n\n#### Live polling\n\nThe Dashboard can be set to automatically refresh by checking \"Live Poll\" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).\n\n#### Extending dashboard views\n\nGoodJob exposes some views that are intended to be overridden by placing views in your application:\n\n- [`app/views/good_job/_custom_head.html.erb`](app/views/good_job/_custom_head.html.erb): content added to this partial will be added at the end of the `\u003chead\u003e` tag in all GoodJob views. This is ideal for injecting custom scripts or styles.\n- [`app/views/good_job/_custom_job_details.html.erb`](app/views/good_job/_custom_job_details.html.erb): content added to this partial will be displayed above the argument list on the good_job/jobs#show page.\n- [`app/views/good_job/_custom_execution_details.html.erb`](app/views/good_job/_custom_execution_details.html.erb): content added to this partial will be displayed above each execution on the good_job/jobs#show page.\n\n**Warning:** these partials expose classes (such as `GoodJob::Job`) that are considered internal implementation details of GoodJob. You should always test your custom partials after upgrading GoodJob.\n\nFor example, if your app deals with widgets and you want to show a link to the widget a job acted on, you can add the following to `app/views/good_job/_custom_job_details.html.erb`:\n\n```erb\n\u003c%# file: app/views/good_job/_custom_job_details.html.erb %\u003e\n\u003c% arguments = job.active_job.arguments rescue [] %\u003e\n\u003c% widgets = arguments.select { |arg| arg.is_a?(Widget) } %\u003e\n\u003c% if widgets.any? %\u003e\n  \u003cdiv class=\"my-4\"\u003e\n    \u003ch5\u003eWidgets\u003c/h5\u003e\n    \u003cul\u003e\n      \u003c% widgets.each do |widget| %\u003e\n        \u003cli\u003e\u003c%= link_to widget.name, main_app.widget_url(widget) %\u003e\u003c/li\u003e\n      \u003c% end %\u003e\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c% end %\u003e\n```\n\nAs a second example, you may wish to show a link to a log aggregator next to each job execution. You can do this by adding the following to `app/views/good_job/_custom_execution_details.html.erb`:\n\n```erb\n\u003c%# file: app/views/good_job/_custom_execution_details.html.erb %\u003e\n\u003cdiv class=\"py-3\"\u003e\n  \u003c%= link_to \"Logs\", main_app.logs_url(filter: { job_id: job.id }, start_time: execution.performed_at, end_time: execution.finished_at + 1.minute) %\u003e\n\u003c/div\u003e\n```\n\n### Job priority\n\nSmaller `priority` values have higher priority and run first (default: `0`), in accordance with [Active Job's definition of priority](https://github.com/rails/rails/blob/e17faead4f2aff28da079d50f02ea5b015322d5b/activejob/lib/active_job/core.rb#L22).\n\nPrior to GoodJob v4, this was reversed: higher priority numbers ran first in all versions of GoodJob v3.x and below. When migrating from v3 to v4, new behavior can be opted into by setting `config.good_job.smaller_number_is_higher_priority = true` in your GoodJob initializer or `application.rb`.\n\n### Labelled jobs\n\nLabels are the recommended way to add context or metadata to specific jobs. For example, all jobs that have a dependency on an email service could be labeled `email`. Using labels requires adding the Active Job extension `GoodJob::ActiveJobExtensions::Labels` to your job class.\n\n```ruby\nclass ApplicationJob \u003c ActiveJob::Base\n  include GoodJob::ActiveJobExtensions::Labels\nend\n\n# Add a default label to every job within the class\nclass WelcomeJob \u003c ApplicationJob\n  self.good_job_labels = [\"email\"]\n\n  def perform\n    # Labels can be inspected from within the job\n    puts good_job_labels # =\u003e [\"email\"]\n  end\nend\n\n# Or add to individual jobs when enqueued\nWelcomeJob.set(good_job_labels: [\"email\"]).perform_later\n```\n\nLabels can be used to search jobs in the Dashboard. For example, to find all jobs labeled `email`, search for `email`.\n\n### Concurrency controls\n\nGoodJob can extend Active Job to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unnecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.\n\n```ruby\nclass MyJob \u003c ApplicationJob\n  include GoodJob::ActiveJobExtensions::Concurrency\n\n  # Define one or more concurrency rules. Each rule is scoped to a label,\n  # which is a value derived from the job's arguments at enqueue time and\n  # stored on the job record. Jobs must be enqueued with the matching label\n  # via `good_job_labels:` for the rule to apply.\n  #\n  # Multiple rules can be defined; they are evaluated in order and the first\n  # exceeded rule short-circuits the rest.\n  good_job_concurrency_rule(\n    # A label that scopes this rule. Can be a static String or a Lambda/Proc\n    # invoked in the context of the job instance. The rule only applies to jobs\n    # that were enqueued with this label in `good_job_labels`.\n    label: -\u003e { arguments.first[:user_id] },\n\n    # Maximum number of unfinished jobs with this label to allow.\n    # Can be an Integer or Lambda/Proc invoked in the context of the job.\n    total_limit: 1,\n\n    # Or, if more control is needed:\n    # Maximum number of jobs with this label to be concurrently enqueued\n    # (excludes performing jobs). Can be an Integer or Lambda/Proc.\n    enqueue_limit: 2,\n\n    # Maximum number of jobs with this label to be concurrently performed\n    # (excludes enqueued jobs). Can be an Integer or Lambda/Proc.\n    perform_limit: 1,\n\n    # Maximum number of jobs with this label to be enqueued within the time\n    # period, looking backwards from now. Must be [count, period].\n    enqueue_throttle: [10, 1.minute],\n\n    # Maximum number of jobs with this label to be performed within the time\n    # period, looking backwards from now. Must be [count, period].\n    perform_throttle: [100, 1.hour],\n\n    # Note: Under heavy load, the total number of jobs may exceed the\n    # sum of `enqueue_limit` and `perform_limit` because of race conditions\n    # caused by imperfectly disjunctive states. If you need to constrain\n    # the total number of jobs, use `total_limit` instead. See #378.\n  )\n  # Additional rules\n  good_job_concurrency_rule(...)\n  good_job_concurrency_rule(...)\n\n  def perform(user_id:)\n    # do work\n  end\nend\n```\n\nJobs must be enqueued with the matching label for rules to take effect:\n\n```ruby\nMyJob.set(good_job_labels: [current_user.id]).perform_later(user_id: current_user.id)\n```\n\n#### How concurrency controls work\n\nGoodJob's concurrency control strategy for `perform_limit` is \"optimistic retry with an incremental backoff\".  The [code is readable](https://github.com/bensheldon/good_job/blob/main/lib/good_job/active_job_extensions/concurrency.rb).\n\n- \"Optimistic\" meaning that the implementation's performance trade-off assumes that collisions are atypical (e.g. two users enqueue the same job at the same time) rather than regular (e.g. the system enqueues thousands of colliding jobs at the same time). Depending on your concurrency requirements, you may also want to manage concurrency through the number of GoodJob threads and processes that are performing a given queue.\n- \"Retry with an incremental backoff\" means that when `perform_limit` is exceeded, the job will raise a `GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError` which is caught by a `retry_on` handler which re-schedules the job to execute in the near future with an incremental backoff.\n- First-in-first-out job execution order is not preserved when a job is retried with incremental back-off.\n- For pessimistic usecases that collisions are expected, use number of threads/processes (e.g., `good_job --queues \"serial:1;-serial:5\"`) to control concurrency. It is also a good idea to use `perform_limit` as backstop.\n\n#### Legacy: `good_job_control_concurrency_with`\n\nThe original concurrency interface uses a single configuration hash and scopes limits to a concurrency _key_ (a string derived from the job) stored on the job record, rather than a label. It remains fully supported.\n\n```ruby\nclass MyJob \u003c ApplicationJob\n  include GoodJob::ActiveJobExtensions::Concurrency\n\n  good_job_control_concurrency_with(\n    # Maximum number of unfinished jobs to allow with the concurrency key\n    # Can be an Integer or Lambda/Proc that is invoked in the context of the job\n    total_limit: 1,\n\n    # Or, if more control is needed:\n    # Maximum number of jobs with the concurrency key to be\n    # concurrently enqueued (excludes performing jobs)\n    # Can be an Integer or Lambda/Proc that is invoked in the context of the job\n    enqueue_limit: 2,\n\n    # Maximum number of jobs with the concurrency key to be\n    # concurrently performed (excludes enqueued jobs)\n    # Can be an Integer or Lambda/Proc that is invoked in the context of the job\n    perform_limit: 1,\n\n    # Maximum number of jobs with the concurrency key to be enqueued within\n    # the time period, looking backwards from the current time. Must be an array\n    # with two elements: the number of jobs and the time period.\n    enqueue_throttle: [10, 1.minute],\n\n    # Maximum number of jobs with the concurrency key to be performed within\n    # the time period, looking backwards from the current time. Must be an array\n    # with two elements: the number of jobs and the time period.\n    perform_throttle: [100, 1.hour],\n\n    # A unique key to be globally locked against.\n    # Can be String or Lambda/Proc that is invoked in the context of the job.\n    #\n    # If a key is not provided GoodJob will use the job class name.\n    #\n    # To disable concurrency control, for example in a subclass, set the\n    # key explicitly to nil (e.g. `key: nil` or `key: -\u003e { nil }`)\n    #\n    # If you provide a custom concurrency key (for example, if concurrency is supposed\n    # to be controlled by the first job argument) make sure that it is sufficiently unique across\n    # jobs and queues by adding the job class or queue to the key yourself, if needed.\n    #\n    # Note: When using a model instance as part of your custom concurrency key, make sure\n    # to explicitly use its `id` or `to_global_id` because otherwise it will not stringify as expected.\n    #\n    # Note: Arguments passed to #perform_later can be accessed through Active Job's `arguments` method\n    # which is an array containing positional arguments and, optionally, a kwarg hash.\n    key: -\u003e { \"#{self.class.name}-#{queue_name}-#{arguments.first}-#{arguments.last[:version]}\" } #  MyJob.perform_later(\"Alice\", version: 'v2') =\u003e \"MyJob-default-Alice-v2\"\n  )\n\n  def perform(first_name, version:)\n    # do work\n  end\nend\n```\n\nWhen testing, the resulting concurrency key value can be inspected:\n\n```ruby\njob = MyJob.perform_later(\"Alice\", version: 'v1')\njob.good_job_concurrency_key #=\u003e \"MyJob-default-Alice-v1\"\n```\n\n### Cron-style repeating/recurring jobs\n\nGoodJob can enqueue Active Job jobs on a recurring basis that can be used as a replacement for cron.\n\nCron-style jobs can be enequeued by any GoodJob process (e.g., CLI or `:async` execution mode) that has `config.good_job.enable_cron` set to `true`. Enabling cron on multiple processes will not enqueue duplicate jobs; GoodJob's cron uses unique indexes to ensure that only a single job is enqueued for a given time interval.  In order for this to work, GoodJob must preserve cron-created job records; these records will be automatically deleted like any other preserved record.\n\nCron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem, which has support for seconds-level resolution (e.g. `* * * * * *`) and natural language parsing (e.g. `every second`).\n\nIf you use the [Dashboard](#dashboard) the scheduled tasks can be viewed in the 'cron' menu. In this view you can also disable a task or run/enqueue a task immediately.\n\n```ruby\n# config/environments/application.rb or a specific environment e.g. production.rb\n\n# Enable cron enqueuing in this process\nconfig.good_job.enable_cron = true\n\n# Without zero-downtime deploys, re-attempt previous schedules after a deploy\nconfig.good_job.cron_graceful_restart_period = 1.minute\n\n# Configure cron with a hash that has a unique key for each recurring job\nconfig.good_job.cron = {\n  # Every 15 minutes, enqueue `ExampleJob.set(priority: -10).perform_later(42, \"life\", name: \"Alice\")`\n  frequent_task: { # each recurring job must have a unique key\n    cron: \"*/15 * * * *\", # cron-style scheduling format by fugit gem\n    class: \"ExampleJob\", # name of the job class as a String; must reference an Active Job job class\n    args: [42, \"life\"], # positional arguments to pass to the job; can also be a proc e.g. `-\u003e { [Time.now] }`\n    kwargs: { name: \"Alice\" }, # keyword arguments to pass to the job; can also be a proc e.g. `-\u003e { { name: NAMES.sample } }`\n    set: { priority: -10 }, # additional Active Job properties; can also be a lambda/proc e.g. `-\u003e { { priority: [1,2].sample } }`\n    description: \"Something helpful\", # optional description that appears in Dashboard\n  },\n  production_task: {\n    cron: \"0 0,12 * * *\",\n    class: \"ProductionJob\",\n    enabled_by_default: -\u003e { Rails.env.production? } # Only enable in production, otherwise can be enabled manually through Dashboard\n  },\n  complex_schedule: {\n    class: \"ComplexScheduleJob\",\n    cron: -\u003e (last_ran) { (last_ran.blank? ? Time.now : last_ran + 14.hours).at_beginning_of_minute }\n  }\n  # etc.\n}\n```\n\n### Bulk enqueue\n\nGoodJob's Bulk-enqueue functionality can buffer and enqueue multiple jobs at once, using a single INSERT statement. This can be more performant when enqueuing a large number of jobs.\n\n```ruby\n# Capture jobs using `.perform_later`:\nactive_jobs = GoodJob::Bulk.enqueue do\n  MyJob.perform_later\n  AnotherJob.perform_later\n  # If an exception is raised within this block, no jobs will be inserted.\nend\n\n# All Active Job instances are returned from GoodJob::Bulk.enqueue.\n# Jobs that have been successfully enqueued have a `provider_job_id` set.\nactive_jobs.all?(\u0026:provider_job_id)\n\n# Bulk enqueue Active Job instances directly without using `.perform_later`:\nGoodJob::Bulk.enqueue([MyJob.new, AnotherJob.new])\n```\n\n### Batches\n\nBatches track a set of jobs, and enqueue an optional callback job when all of the jobs have finished (succeeded or discarded).\n\n- A simple example that enqueues your `MyBatchCallbackJob` after the two jobs have finished, and passes along the current user as a batch property:\n\n    ```ruby\n    GoodJob::Batch.enqueue(on_finish: MyBatchCallbackJob, user: current_user) do\n      MyJob.perform_later\n      OtherJob.perform_later\n    end\n\n    # When these jobs have finished, it will enqueue your `MyBatchCallbackJob.perform_later(batch, context)`\n    class MyBatchCallbackJob \u003c ApplicationJob\n      # Callback jobs must accept a `batch` and `context` argument\n      def perform(batch, context)\n        # The batch object will contain the Batch's properties, which are mutable\n        batch.properties[:user] # =\u003e \u003cUser id: 1, ...\u003e\n\n        # Context is a hash containing additional context (more may be added in the future)\n        context[:event] # =\u003e :finish, :success, :discard\n      end\n    end\n    ```\n\n- Jobs can be added to an existing batch. Jobs in a batch are enqueued and performed immediately/asynchronously. The final callback job will not be enqueued until `GoodJob::Batch#enqueue` is called.\n\n    ```ruby\n    batch = GoodJob::Batch.new\n    batch.add do\n      10.times { MyJob.perform_later }\n    end\n\n    batch.add do\n      10.times { OtherJob.perform_later }\n    end\n    batch.enqueue(on_finish: MyBatchCallbackJob, age: 42)\n    ```\n\n- If you need to access the batch within a job that is part of the batch, include [`GoodJob::ActiveJobExtensions::Batches`](lib/good_job/active_job_extensions/batches.rb) in your job class:\n\n  ```ruby\n    class MyJob \u003c ApplicationJob\n      include GoodJob::ActiveJobExtensions::Batches\n\n      def perform\n        self.batch # =\u003e \u003cGoodJob::Batch id: 1, ...\u003e\n      end\n    end\n    ```\n\n- [`GoodJob::Batch`](app/models/good_job/batch.rb) has a number of assignable attributes and methods:\n\n```ruby\nbatch = GoodJob::Batch.new\nbatch.description = \"My batch\"\nbatch.on_finish = \"MyBatchCallbackJob\" # Callback job when all jobs have finished\nbatch.on_success = \"MyBatchCallbackJob\" # Callback job when/if all jobs have succeeded\nbatch.on_discard = \"MyBatchCallbackJob\" # Callback job when the first job in the batch is discarded\nbatch.callback_queue_name = \"special_queue\" # Optional queue for callback jobs, otherwise will defer to job class\nbatch.callback_priority = 10 # Optional priority name for callback jobs, otherwise will defer to job class\nbatch.properties = { age: 42 } # Custom data and state to attach to the batch\nbatch.add do\n  MyJob.perform_later\nend\nbatch.enqueue\n\nbatch.discarded? # =\u003e Boolean\nbatch.discarded_at # =\u003e \u003cDateTime\u003e\nbatch.finished? # =\u003e Boolean\nbatch.finished_at # =\u003e \u003cDateTime\u003e\nbatch.succeeded? # =\u003e Boolean\nbatch.active_jobs # =\u003e Array of ActiveJob::Base-inherited jobs that are part of the batch\n\nbatch = GoodJob::Batch.find(batch.id)\nbatch.description = \"Updated batch description\"\nbatch.save\nbatch.reload\n```\n\n### Batch callback jobs\n\nBatch callbacks are Active Job jobs that are enqueued at certain events during the execution of jobs within the batch:\n\n- `:finish` - Enqueued when all jobs in the batch have finished, after all retries. Jobs will either be discarded or succeeded.\n- `:success` - Enqueued only when all jobs in the batch have finished and succeeded.\n- `:discard` - Enqueued immediately the first time a job in the batch is discarded.\n\nCallback jobs must accept a `batch` and `context` argument in their `perform` method:\n\n```ruby\nclass MyBatchCallbackJob \u003c ApplicationJob\n  def perform(batch, context)\n    # The batch object will contain the Batch's properties\n    batch.properties[:user] # =\u003e \u003cUser id: 1, ...\u003e\n    # Batches are mutable\n    batch.properties[:user] = User.find(2)\n    batch.save\n\n    # Context is a hash containing additional context (more may be added in the future)\n    context[:event] # =\u003e :finish, :success, :discard\n  end\nend\n```\n\n#### Complex batches\n\nConsider a multi-stage batch with both parallel and serial job steps:\n\n```mermaid\ngraph TD\n    0{\"BatchJob\\n{ stage: nil }\"}\n    0 --\u003e a[\"WorkJob]\\n{ step: a }\"]\n    0 --\u003e b[\"WorkJob]\\n{ step: b }\"]\n    0 --\u003e c[\"WorkJob]\\n{ step: c }\"]\n    a --\u003e 1\n    b --\u003e 1\n    c --\u003e 1\n    1{\"BatchJob\\n{ stage: 1 }\"}\n    1 --\u003e d[\"WorkJob]\\n{ step: d }\"]\n    1 --\u003e e[\"WorkJob]\\n{ step: e }\"]\n    e --\u003e f[\"WorkJob]\\n{ step: f }\"]\n    d --\u003e 2\n    f --\u003e 2\n    2{\"BatchJob\\n{ stage: 2 }\"}\n```\n\nThis can be implemented with a single, mutable batch job:\n\n```ruby\nclass WorkJob \u003c ApplicationJob\n  include GoodJob::ActiveJobExtensions::Batches\n\n  def perform(step)\n    # ...\n    if step == 'e'\n      batch.add { WorkJob.perform_later('f') }\n    end\n  end\nend\n\nclass BatchJob \u003c ApplicationJob\n  def perform(batch, context)\n    if batch.properties[:stage].nil?\n      batch.enqueue(stage: 1) do\n        WorkJob.perform_later('a')\n        WorkJob.perform_later('b')\n        WorkJob.perform_later('c')\n      end\n    elsif batch.properties[:stage] == 1\n      batch.enqueue(stage: 2) do\n        WorkJob.perform_later('d')\n        WorkJob.perform_later('e')\n      end\n    elsif batch.properties[:stage] == 2\n      # ...\n    end\n  end\nend\n\nGoodJob::Batch.enqueue(on_finish: BatchJob)\n```\n\n#### Other batch details\n\n- Whether to enqueue a callback job is evaluated once the batch is in an `enqueued?`-state by using `GoodJob::Batch.enqueue` or `batch.enqueue`.\n- Callback job enqueueing will be re-triggered if additional jobs are `enqueue`'d to the batch; use `add` to add jobs to the batch without retriggering callback jobs.\n- Callback jobs will be enqueued even if the batch contains no jobs.\n- Callback jobs perform asynchronously. It's possible that `:finish` and `:success` or `:discard` callback jobs perform at the same time. Keep this in mind when updating batch properties.\n- Batch properties are serialized using Active Job serialization. This is flexible, but can lead to deserialization errors if a GlobalID record is directly referenced but is subsequently deleted and thus unloadable.\n- 🚧Batches are a work in progress. Please let us know what would be helpful to improve their functionality and usefulness.\n\n### Updating\n\nGoodJob follows semantic versioning, though updates may be encouraged through deprecation warnings in minor versions.\n\n#### Upgrading minor versions\n\nUpgrading between minor versions (e.g. v1.4 to v1.5) should not introduce breaking changes, but can introduce new deprecation warnings and database migration warnings.\n\nDatabase migrations introduced in minor releases are _not required_ to be applied until the next major release. If you would like to apply newly introduced migrations immediately, assert `GoodJob.migrated?` in your application's test suite.\n\nTo perform upgrades to the GoodJob database tables:\n\n1. Generate new database migration files:\n\n    ```bash\n    bin/rails g good_job:update\n    ```\n\n   Optional: If using Rails' multiple databases with the `migrations_paths` configuration option, use the `--database` option:\n\n    ```bash\n    bin/rails g good_job:update --database animals\n    ```\n\n1. Run the database migration locally\n\n    ```bash\n    bin/rails db:migrate\n    ```\n\n1. Commit the migration files and resulting `db/schema.rb` changes.\n1. Deploy the code, run the migrations against the production database, and restart server/worker processes.\n\n#### Upgrading v3 to v4\n\nGoodJob v4 changes how job and job execution records are stored in the database; moving from job and executions being commingled in the `good_jobs` table to separately and discretely storing job executions in `good_job_executions`. To safely upgrade, all unfinished jobs must use the new format. This change was introduced in GoodJob [v3.15.4 (April 2023)](https://github.com/bensheldon/good_job/releases/tag/v3.15.4), so your application is likely ready-to-upgrade in this respect if you have kept up with GoodJob updates and applied migrations (`bin/rails g good_job:update`). _Please be sure to doublecheck you are not missing subsequent migrations or deprecations too by following the instructions below._\n\nTo upgrade:\n\n1. Upgrade to v3.99.x, following the minor version upgrade process, running any remaining database migrations (rails g good_job:update) and addressing deprecation warnings.\n1. Check if your application is safe to upgrade to the new job record format by running either:\n    - In a production console, run `GoodJob.v4_ready?` which should return `true` when safely upgradable.\n    - Or, when connected to the production database verify that `SELECT COUNT(*) FROM \"good_jobs\" WHERE finished_at IS NULL AND is_discrete IS NOT TRUE` returns `0`\n\n    If not all unfinished jobs are stored in the new format, either wait to upgrade until those jobs finish or discard them. Not waiting could prevent those jobs from successfully running when upgrading to v4.\n1. Upgrade from v3.99.x to v4.x.\n\nNotable changes:\n\n- Only supports Rails 6.1+, CRuby 3.0+ and JRuby 9.4+. Rails 6.0 is no longer supported. CRuby 2.6 and 2.7 are no longer supported. JRuby 9.3 is no longer supported.\n- Changes job `priority` to give smaller numbers higher priority (default: `0`), in accordance with Active Job's definition of priority.\n- Enqueues and executes jobs via the `GoodJob::Job` model instead of `GoodJob::Execution`\n- Setting `config.good_job.cleanup_interval_jobs`, `GOOD_JOB_CLEANUP_INTERVAL_JOBS`, `config.good_job.cleanup_interval_seconds`, or `GOOD_JOB_CLEANUP_INTERVAL_SECONDS` to `nil` or `\"\"` no longer disables count- or time-based cleanups. Set to `false` to disable, or `-1` to run a cleanup after every job execution.\n\n#### Upgrading v2 to v3\n\nGoodJob v3 is operationally identical to v2; upgrading to GoodJob v3 should be simple. If you are already using `\u003e= v2.9+` no other changes are necessary.\n\n1. Upgrade to `v2.99.x`, following the minor version upgrade process, running any remaining database migrations (`rails g good_job:update`) and addressing deprecation warnings.\n1. Upgrade from `v2.99.x` to `v3.x`\n\nNotable changes:\n\n- Defaults to preserve job records, and automatically delete them after 14 days.\n- Defaults to discarding failed jobs, instead of immediately retrying them.\n- `:inline` execution mode respects job schedules. Tests can invoke  `GoodJob.perform_inline` to execute jobs.\n- `GoodJob::Adapter` can no longer can be initialized with custom execution options (`queues:`, `max_threads:`, `poll_interval:`).\n- Renames `GoodJob::ActiveJobJob` to `GoodJob::Job`.\n- Removes support for Rails 5.2.\n\n#### Upgrading v1 to v2\n\nGoodJob v2 introduces a new Advisory Lock key format that is operationally different than the v1 advisory lock key format; it's therefore necessary to perform a simple, but staged production upgrade. If you are already using `\u003e= v1.12+` no other changes are necessary.\n\n1. Upgrade your production environment to `v1.99.x` following the minor version upgrade process, including database migrations. `v1.99` is a transitional release that is safely compatible with both `v1.x` and `v2.0.0` because it uses both `v1`- and `v2`-formatted advisory locks.\n1. Address any deprecation warnings generated by `v1.99`.\n1. Upgrade your production environment from `v1.99.x` to `v2.0.x` again following the _minor_ upgrade process.\n\nNotable changes:\n\n- Renames `:async_server` execution mode to `:async`; renames prior `:async` execution mode to `:async_all`.\n- Sets default Development environment's execution mode to `:async` with disabled polling.\n- Excludes performing jobs from `enqueue_limit`'s count in `GoodJob::ActiveJobExtensions::Concurrency`.\n- Triggers `GoodJob.on_thread_error` for unhandled Active Job exceptions.\n- Renames `GoodJob.reperform_jobs_on_standard_error` accessor to `GoodJob.retry_on_unhandled_error`.\n- Renames `GoodJob::Adapter.shutdown(wait:)` argument to `GoodJob::Adapter.shutdown(timeout:)`.\n- Changes Advisory Lock key format from `good_jobs[ROW_ID]` to `good_jobs-[ACTIVE_JOB_ID]`.\n- Expects presence of columns `good_jobs.active_job_id`, `good_jobs.concurrency_key`, `good_jobs.concurrency_key`, and `good_jobs.retried_good_job_id`.\n\n## Go deeper\n\n### Exceptions, retries, and reliability\n\nGoodJob guarantees that a completely-performed job will run once and only once. GoodJob fully supports Active Job's built-in functionality for error handling, retries and timeouts.\n\n#### Exceptions\n\nActive Job provides [tools for rescuing and retrying exceptions](https://guides.rubyonrails.org/active_job_basics.html#exceptions), including `retry_on`, `discard_on`, `rescue_from` that will rescue exceptions before they get to GoodJob.\n\nIf errors do reach GoodJob, you can assign a callable to `GoodJob.on_thread_error` to be notified. For example, to log errors to an exception monitoring service like Sentry (or Bugsnag, Airbrake, Honeybadger, etc.):\n\n```ruby\n# config/initializers/good_job.rb\nGoodJob.on_thread_error = -\u003e (exception) { Rails.error.report(exception) }\n```\n\n#### Retries\n\nBy default, GoodJob relies on Active Job's retry functionality.\n\nActive Job can be configured to retry an infinite number of times, with a polynomial backoff. Using Active Job's `retry_on` prevents exceptions from reaching GoodJob:\n\n```ruby\nclass ApplicationJob \u003c ActiveJob::Base\n  retry_on StandardError, wait: :polynomially_longer, attempts: Float::INFINITY\n  # ...\nend\n```\n\nWhen using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob's error handler. To avoid this, pass a block to `retry_on` to handle the final exception instead of raising it to GoodJob:\n\n```ruby\nclass ApplicationJob \u003c ActiveJob::Base\n  retry_on StandardError, attempts: 5 do |_job, _exception|\n    # Log error, do nothing, etc.\n  end\n  # ...\nend\n```\n\nWhen using `retry_on` with an infinite number of retries, exceptions will never be raised to GoodJob, which means `GoodJob.on_thread_error` will never be called. To report log or report exceptions to an exception monitoring service (e.g. Sentry, Bugsnag, Airbrake, Honeybadger, etc), create an explicit exception wrapper. For example:\n\n```ruby\nclass ApplicationJob \u003c ActiveJob::Base\n  retry_on StandardError, wait: :polynomially_longer, attempts: Float::INFINITY\n\n  retry_on SpecialError, attempts: 5 do |_job, exception|\n    Rails.error.report(exception)\n  end\n\n  around_perform do |_job, block|\n    block.call\n  rescue StandardError =\u003e e\n    Rails.error.report(e)\n    raise\n  end\n  # ...\nend\n```\n\nBy default, jobs will not be retried unless `retry_on` is configured. This can be overridden by setting `GoodJob.retry_on_unhandled_error` to `true`; GoodJob will then retry the failing job immediately and infinitely, potentially causing high load.\n\n#### Action Mailer retries\n\nAny configuration in `ApplicationJob` will have to be duplicated on `ActionMailer::MailDeliveryJob` because ActionMailer uses that custom class which inherits from `ActiveJob::Base`,  rather than your application's `ApplicationJob`.\n\nYou can use an initializer to configure `ActionMailer::MailDeliveryJob`, for example:\n\n```ruby\n# config/initializers/good_job.rb\nActionMailer::MailDeliveryJob.retry_on StandardError, wait: :polynomially_longer, attempts: Float::INFINITY\n\n# With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)\nActionMailer::MailDeliveryJob.around_perform do |_job, block|\n  block.call\nrescue StandardError =\u003e e\n  Rails.error.report(e)\n  raise\nend\n```\n\nNote, that `ActionMailer::MailDeliveryJob` is a default since Rails 6.0. Be sure that your app is using that class, as it\nmight also be configured to use (deprecated now) `ActionMailer::DeliveryJob`.\n\n### Interrupts, graceful shutdown, and SIGKILL\n\nWhen GoodJob receives an interrupt (SIGINT, SIGTERM) or explicitly with `GoodJob.shutdown`, GoodJob will attempt to gracefully shut down, waiting for all jobs to finish before exiting based on the `shutdown_timeout` configuration.\n\nTo detect the start of a graceful shutdown from within a performing job, for example while looping/iterating over multiple items, you can call `GoodJob.current_thread_shutting_down?` or `GoodJob.current_thread_running?` from within the job. For example:\n\n```ruby\ndef perform(lots_of_records)\n  lots_of_records.each do |record|\n    break if GoodJob.current_thread_shutting_down? # or `unless GoodJob.current_thread_running?`\n    # process record ...\n  end\nend\n````\n\nNote that when running jobs in `:inline` execution mode, `GoodJob.current_thread_running?` will always be truthy and `GoodJob.current_thread_shutting_down?` will always be falsey.\n\nJobs will be automatically retried if the process is interrupted while performing a job and the job is unable to finish before the timeout or as the result of a `SIGKILL` or power failure. The interrupted execution's error record will show `GoodJob::InterruptedError` to distinguish it from the rescuable `GoodJob::InterruptError` that is raised when the job is retried.\n\nIf you need more control over interrupt-caused retries, include the `GoodJob::ActiveJobExtensions::InterruptErrors` extension in your job class. When an interrupted job is retried, the extension will raise a `GoodJob::InterruptError` exception within the job, which allows you to use Active Job's `retry_on` and `discard_on` to control the behavior of the job.\n\n```ruby\nclass MyJob \u003c ApplicationJob\n  # The extension must be included before other extensions\n  include GoodJob::ActiveJobExtensions::InterruptErrors\n  # Discard the job if it is interrupted\n  discard_on GoodJob::InterruptError\n  # Retry the job if it is interrupted\n  retry_on GoodJob::InterruptError, wait: 0, attempts: Float::INFINITY\nend\n```\n\n### Timeouts\n\nAvoid using Ruby's built-in [Timeout](https://github.com/ruby/timeout) mechanism\n([1](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/),\n[2](https://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html)).\nInstead, declare either of Active Job's [discard_on](https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-discard_on) or [retry_on](https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on) to handle\nthe underlying mechanism's timeout exceptions (when available).\n\nFor example, rescue from `Net::OpenTimeout` or `Net::ReadTimeout` and discard\nthe job:\n\n```ruby\nclass MyJob \u003c ApplicationJob\n  discard_on Net::OpenTimeout, Net::ReadTimeout\n\n  def perform(uri)\n    Net::HTTP.start(uri.host, uri.port, open_timeout: 3, read_timeout: 3) do |http|\n      http.request(...)\n    end\n  end\nend\n```\n\nIf you have no other choice but to use a Ruby Timeout, it can be configured with an `around_perform`:\n\n```ruby\nclass ApplicationJob \u003c ActiveJob::Base\n  JobTimeoutError = Class.new(StandardError)\n\n  around_perform do |_job, block|\n    # Timeout jobs after 10 minutes\n    Timeout.timeout(10.minutes, JobTimeoutError) do\n      block.call\n    end\n  end\nend\n```\n\n### Optimize queues, threads, and processes\n\nBy default, GoodJob creates a single thread execution pool that will execute jobs from any queue. Depending on your application's workload, job types, and service level objectives, you may wish to optimize execution resources. For example, providing dedicated execution resources for transactional emails so they are not delayed by long-running batch jobs. Some options:\n\n- Multiple isolated execution pools within a single process:\n\n    For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.\n\n    A pool is configured with the following syntax `\u003cparticipating_queues\u003e:\u003cthread_count\u003e`:\n\n    - `\u003cparticipating_queues\u003e`: either `queue1,queue2` (only those queues), `+queue1,queue2` (only those queues, and processed in order), `*` (all) or `-queue1,queue2` (all except those queues).\n    - `\u003cthread_count\u003e`: a count overriding for this specific pool the global `max-threads`.\n\n    Pool configurations are separated with a semicolon (;) in the `queues` configuration\n\n    ```bash\n    $ bundle exec good_job \\\n        --queues=\"transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*\" \\\n        --max-threads=5\n    ```\n\n    This configuration will result in a single process with 4 isolated thread execution pools.\n\n    - `transactional_messages:2`: execute jobs enqueued on `transactional_messages`, with up to 2 threads.\n    - `batch_processing:1` execute jobs enqueued on `batch_processing`, with a single thread.\n    - `-transactional_messages,batch_processing:2`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.\n    - `*`: execute jobs on any queue, with up to 5 threads (as configured by `--max-threads=5`).\n\n    When a pool is performing jobs from multiple queues, jobs will be performed from specified queues, ordered by priority and creation time. To perform jobs from queues in the queues' given order, use the `+` modifier. In this example, jobs in `batch_processing` will be performed only when there are no jobs in `transactional_messages`:\n\n    ```bash\n    bundle exec good_job --queues=\"+transactional_messages,batch_processing\"\n    ```\n\n    Configuration can be injected by environment variables too:\n\n    ```bash\n    $ GOOD_JOB_QUEUES=\"transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*\" \\\n      GOOD_JOB_MAX_THREADS=5 \\\n      bundle exec good_job\n    ```\n\n- Multiple processes:\n\n    While multiple isolated thread execution pools offer a way to provide dedicated execution resources, those resources are bound to a single machine. To scale them independently, define several processes.\n\n    For example, this configuration on Heroku allows to customize the dyno count (instances), or type (CPU/RAM), per process type:\n\n    ```procfile\n    # Procfile\n\n    # Separate process types\n    worker: bundle exec good_job --max-threads=5\n    transactional_worker: bundle exec good_job --queues=\"transactional_messages\" --max-threads=2\n    batch_worker: bundle exec good_job --queues=\"batch_processing\" --max-threads=1\n    ```\n\n    To optimize for CPU performance at the expense of greater memory and system resource usage, while keeping a single process type (and thus a single dyno), combine several processes and wait for them:\n\n    ```procfile\n    # Procfile\n\n    # Combined multi-process\n    combined_worker: bundle exec good_job --max-threads=5 \u0026 bundle exec good_job --queues=\"transactional_messages\" --max-threads=2 \u0026 bundle exec good_job --queues=\"batch_processing\" --max-threads=1 \u0026 wait -n\n    ```\n\nKeep in mind, queue operations and management is an advanced discipline. This stuff is complex, especially for heavy workloads and unique processing requirements. Good job 👍\n\n### Database connections\n\nGoodJob job executor processes require the following database connections:\n\n- 1 connection per execution pool thread. E.g., `--queues=mice:2;elephants:1` is 3 threads and thus 3 connections. Pool size defaults to `--max-threads`.\n- 2 additional connections that GoodJob uses for utility functionality (e.g. LISTEN/NOTIFY, cron, etc.)\n- 1 connection per subthread, if your application makes multithreaded database queries (e.g. `load_async`) within a job.\n\nThe executor process will not crash if the connections pool is exhausted, instead it will report an exception (eg. `ActiveRecord::ConnectionTimeoutError`).\n\nWhen GoodJob runs in `:inline` mode (in Rails' test environment, by default), the default database pool configuration works.\n\n```yml\n# config/database.yml\n\npool: \u003c%= ENV.fetch(\"RAILS_MAX_THREADS\") { 5 } %\u003e\n```\n\nWhen GoodJob runs in `:async` mode (in Rails's development environment, by default), the following database pool configuration works, where:\n\n- `ENV.fetch(\"RAILS_MAX_THREADS\", 5)` is the number of threads used by the web server\n- `1` is the number of connections used by the job listener\n- `2` is the number of connections used by the cron scheduler and executor\n- `ENV.fetch(\"GOOD_JOB_MAX_THREADS\", 5)` is the number of threads used to perform jobs\n\n```yaml\n# config/database.yml\n\npool: \u003c%= ENV.fetch(\"RAILS_MAX_THREADS\", 5).to_i + 1 + 2 + ENV.fetch(\"GOOD_JOB_MAX_THREADS\", 5).to_i %\u003e\n```\n\nWhen GoodJob runs in `:external` mode (in Rails' production environment, by default), the following database pool configurations work for web servers and worker processes, respectively.\n\n```yaml\n# config/database.yml\n\npool: \u003c%= ENV.fetch(\"RAILS_MAX_THREADS\", 5) %\u003e\n```\n\n```yaml\n# config/database.yml\n\npool: \u003c%= 1 + 2 + ENV.fetch(\"GOOD_JOB_MAX_THREADS\", 5).to_i %\u003e\n```\n\n### Production setup\n\nWhen running GoodJob in a production environment, you should be mindful of:\n\n- [Execution mode](#execute-jobs-async--in-process)\n- [Database connection pool size](#database-connections)\n- [Health check probes](#cli-http-health-check-probes) and potentially the [instrumentation support](#monitor-and-preserve-worked-jobs)\n\nThe recommended way to monitor the queue in production is:\n\n- have an exception notifier callback (see [`on_thread_error`](#configuration-options))\n- if possible, run the queue as a dedicated instance and use available HTTP health check probes instead of PID-based monitoring\n- keep an eye on the number of jobs in the queue (abnormal high number of unscheduled jobs means the queue could be underperforming)\n- consider performance monitoring services which support the built-in Rails instrumentation (eg. Sentry, Skylight, etc.)\n\n### Queue performance with Queue Select Limit\n\nGoodJob’s advisory locking strategy uses a materialized CTE (Common Table Expression). This strategy can be non-performant when querying a very large queue of executable jobs (100,000+) because the database query must materialize all executable jobs before acquiring an advisory lock.\n\nGoodJob's Queue Select Limit restricts how many jobs are materialized per advisory lock cycle. **It defaults to `1000` — no configuration needed.** Override it if your deployment requires a different value:\n\n```none\n# CLI option\n--queue-select-limit=1000\n\n# Rails configuration\nconfig.good_job.queue_select_limit = 1000\n\n# Environment Variable\nGOOD_JOB_QUEUE_SELECT_LIMIT=1000\n```\n\nThe default of `1000` is a rough upper-bound that exceeds the available database connections on most PaaS offerings, while still offering a significant performance boost for GoodJob when executing very large queues.\n\nTo explain where this value is used, here is the pseudo-query that GoodJob uses to find executable jobs:\n\n```sql\n  SELECT *\n  FROM good_jobs\n  WHERE id IN (\n    WITH rows AS MATERIALIZED (\n      SELECT id, active_job_id\n      FROM good_jobs\n      WHERE (scheduled_at \u003c= NOW() OR scheduled_at IS NULL) AND finished_at IS NULL\n      ORDER BY priority DESC NULLS LAST, created_at ASC\n      [LIMIT 1000] -- \u003c= applied by default; configurable via queue_select_limit\n    )\n    SELECT id\n    FROM rows\n    WHERE pg_try_advisory_lock(('x' || substr(md5('good_jobs' || '-' || active_job_id::text), 1, 16))::bit(64)::bigint)\n    LIMIT 1\n  )\n```\n\nBy default, advisory lock keys are derived with `md5`. The string key (e.g. `good_jobs-\u003cactive_job_id\u003e`) is hashed and truncated to a 64-bit bigint, which is the key space PostgreSQL advisory locks operate in. `md5` is used for its wide availability — no PostgreSQL extensions required — and good bit distribution, not for any cryptographic property.\n\nIf you need a different hash strategy, set it globally:\n\n```ruby\nGoodJob::AdvisoryLockable.hash_function = \"sha256\"\n```\n\nSupported values are `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `hashtext`, and `uuid_v5`.\n\n- `md5` (default): requires no PostgreSQL extensions.\n- `hashtext`: PostgreSQL's internal 32-bit hash; requires no extensions.\n- `sha*` algorithms require the `pgcrypto` extension (`digest()`).\n- `uuid_v5` requires the `uuid-ossp` extension (`uuid_generate_v5()`).\n\n### Execute jobs async / in-process\n\nGoodJob can execute jobs \"async\" in the same process as the web server (e.g. `bin/rails s`). GoodJob's async execution mode offers benefits of economy by not requiring a separate job worker process, but with the tradeoff of increased complexity. Async mode can be configured in two ways:\n\n- Via Rails configuration:\n\n    ```ruby\n    # config/environments/production.rb\n    config.active_job.queue_adapter = :good_job\n\n    # To change the execution mode\n    config.good_job.execution_mode = :async\n\n    # Or with more configuration\n    config.good_job = {\n      execution_mode: :async,\n      max_threads: 4,\n      poll_interval: 30\n    }\n    ```\n\n- Or, with environment variables:\n\n    ```bash\n    GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server\n    ```\n\nDepending on your application configuration, you may need to take additional steps:\n\n- Ensure that you have enough database connections for both web and job execution threads:\n\n    ```yaml\n    # config/database.yml\n    pool: \u003c%= ENV.fetch(\"RAILS_MAX_THREADS\", 5).to_i + ENV.fetch(\"GOOD_JOB_MAX_THREADS\", 4).to_i %\u003e\n    ```\n\n- When running Puma with workers (`WEB_CONCURRENCY \u003e 0`) or another process-forking web server, GoodJob's threadpool schedulers should be stopped before forking, restarted after fork, and cleanly shut down on exit. Stopping GoodJob's scheduler pre-fork is recommended to ensure that GoodJob does not continue executing jobs in the parent/controller process. For example, with Puma:\n\n    ```ruby\n    # config/puma.rb\n\n    if ENV.fetch(\"WEB_CONCURRENCY\", 0).to_i \u003e 0\n      before_fork do\n        GoodJob.shutdown\n      end\n\n      before_worker_boot do\n        GoodJob.restart\n      end\n\n      before_worker_shutdown do\n        GoodJob.shutdown\n      end\n    end\n\n    MAIN_PID = Process.pid\n    at_exit do\n      GoodJob.shutdown if Process.pid == MAIN_PID\n    end\n    ```\n\n  GoodJob is compatible with Puma's `preload_app!` method.\n\n  For Passenger:\n\n    ```Ruby\n    if defined? PhusionPassenger\n      PhusionPassenger.on_event :starting_worker_process do |forked|\n        # If `forked` is true, we're in smart spawning mode.\n        # https://www.phusionpassenger.com/docs/advanced_guides/in_depth/ruby/spawn_methods.html#smart-spawning-hooks\n        if forked\n          GoodJob.logger.info { 'Starting Passenger worker process.' }\n          GoodJob.restart\n        end\n      end\n\n      PhusionPassenger.on_event :stopping_worker_process do\n        GoodJob.logger.info { 'Stopping Passenger worker process.' }\n        GoodJob.shutdown\n      end\n    end\n\n    # GoodJob also starts in the Passenger preloader process. This one does not\n    # trigger the above events, thus we catch it with `Kernel#at_exit`.\n    PRELOADER_PID = Process.pid\n    at_exit do\n      if Process.pid == PRELOADER_PID\n        GoodJob.logger.info { 'Passenger AppPreloader shutting down.' }\n        GoodJob.shutdown\n      end\n    end\n    ```\n\n  If you are using cron-style jobs, you might also want to look at your Passenger configuration, especially at [`passenger_pool_idle_time`](https://www.phusionpassenger.com/library/config/nginx/reference/#passenger_pool_idle_time) and [`passenger_min_instances`](https://www.phusionpassenger.com/library/config/nginx/reference/#passenger_min_instances) to make sure there's always at least once process running that can execute cron-style scheduled jobs. See also [Passenger's optimization guide](https://www.phusionpassenger.com/library/config/nginx/optimization/#minimizing-process-spawning) for more information.\n\n### Migrate to GoodJob from a different Active Job backend\n\nIf your application is already using an Active Job backend, you will need to install GoodJob to enqueue and perform newly created jobs _and_ finish performing pre-existing jobs on the previous backend.\n\n1. Enqueue newly created jobs on GoodJob either entirely by setting `ActiveJob::Base.queue_adapter = :good_job` or progressively via individual job classes:\n\n    ```ruby\n    # jobs/specific_job.rb\n    class SpecificJob \u003c ApplicationJob\n      self.queue_adapter = :good_job\n      # ...\n    end\n    ```\n\n1. Continue running executors for both backends. For example, on Heroku it's possible to run [two processes](https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno) within the same dyno:\n\n   ```procfile\n    # Procfile\n    # ...\n    worker: bundle exec que ./config/environment.rb \u0026 bundle exec good_job \u0026 wait -n\n    ```\n\n1. Once you are confident that no unperformed jobs remain in the previous Active Job backend, code and configuration for that backend can be completely removed.\n\n### Monitor and preserve worked jobs\n\nGoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).\n\nBy default, GoodJob will preserve job records for 14 days after they are run, regardless of whether they succeed or raised an exception.\n\nTo instead delete job records immediately after they are finished:\n\n```ruby\n# config/initializers/good_job.rb\nconfig.good_job.preserve_job_records = false # defaults to true; can also be `false`, `:on_unhandled_error`, or a lambda that takes error_event argument\n\n# Example of using a lambda to preserve only discarded jobs\nconfig.good_job.preserve_job_records = -\u003e(error_event) { error_event == :discarded }\n\n```\n\nGoodJob will automatically delete preserved job records after 14 days. The retention period, as well as the frequency GoodJob checks for deletable records can be configured:\n\n```ruby\nconfig.good_job.cleanup_preserved_jobs_before_seconds_ago = 14.days\nconfig.good_job.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.\nconfig.good_job.cleanup_interval_seconds = 10.minutes # Number of seconds between deletion sweeps.\n```\n\nIt is also possible to manually trigger a cleanup of preserved job records:\n\n- For example, in a Rake task:\n\n    ```ruby\n    GoodJob.cleanup_preserved_jobs # Will use default retention period\n    GoodJob.cleanup_preserved_jobs(older_than: 7.days) # custom retention period\n    ```\n\n- For example, using the `good_job` command-line utility:\n\n    ```bash\n    bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400\n    ```\n\n### Write tests\n\nBy default, GoodJob uses its inline adapter in the test environment; the inline adapter is designed for the test environment. When enqueuing a job with GoodJob's inline adapter, the job will be executed immediately on the current thread; unhandled exceptions will be raised.\n\nIn GoodJob 2.0, the inline adapter will execute future scheduled jobs immediately. In the next major release, GoodJob 3.0, the inline adapter will not execute future scheduled jobs and instead enqueue them in the database.\n\nTo opt into this behavior immediately set: `config.good_job.inline_execution_respects_schedule = true`\n\nTo perform jobs inline at any time, use `GoodJob.perform_inline`. For example, using time helpers within an integration test:\n\n```ruby\nMyJob.set(wait: 10.minutes).perform_later\ntravel_to(15.minutes.from_now) { GoodJob.perform_inline }\n```\n\n_Note: Rails `travel`/`travel_to` time helpers do not have millisecond precision, so you must leave at least 1 second between the schedule and time traveling for the job to be executed. This [behavior may change in Rails 7.1](https://github.com/rails/rails/pull/44088)._\n\n### SKIP LOCKED experimental mode\n\nBy default, GoodJob claims jobs using PostgreSQL advisory locks. As an alternative, GoodJob can use `SELECT FOR UPDATE SKIP LOCKED` to claim jobs, which writes the lock state directly to the `good_jobs` table rather than relying on session-level advisory locks.\n\nTwo strategies are available:\n\n- **`:skiplocked`** — Claims jobs using `SELECT FOR UPDATE SKIP LOCKED` only. No advisory locks are held. Compatible with PgBouncer in transaction mode.\n- **`:hybrid`** — Claims jobs using `SELECT FOR UPDATE SKIP LOCKED` and _also_ acquires a session-level advisory lock on the job. Intended for rolling deploys where some workers are still using the default `:advisory` strategy.\n\nConfigure the lock strategy in an initializer or via environment variable:\n\n```ruby\n# config/initializers/good_job.rb\nGoodJob.configure do |config|\n  config.lock_strategy = :skiplocked\nend\n```\n\n```bash\nGOOD_JOB_LOCK_STRATEGY=skiplocked\n```\n\nAll three strategies (`:advisory`, `:skiplocked`, `:hybrid`) can coexist safely during a rolling deploy — each strategy excludes jobs that are already locked by another worker regardless of which strategy that worker uses.\n\n#### PgBouncer configuration\n\nGoodJob's `:skiplocked` mode makes it compatible with PgBouncer in _transaction_ mode. In addition to setting the lock strategy, you must also disable the `LISTEN/NOTIFY` notifier (which requires a persistent connection) and rely on polling instead:\n\n```ruby\n# config/initializers/good_job.rb\nGoodJob.configure do |config|\n  config.lock_strategy = :skiplocked\n  config.enable_listen_notify = false\n  config.advisory_lock_heartbeat = false\n  config.poll_interval = 5 # seconds; tune based on your latency tolerance\nend\n```\n\n```bash\nGOOD_JOB_LOCK_STRATEGY=skiplocked\nGOOD_JOB_ENABLE_LISTEN_NOTIFY=false\nGOOD_JOB_ADVISORY_LOCK_HEARTBEAT=false\nGOOD_JOB_POLL_INTERVAL=5\n```\n\nWith these four settings, GoodJob will not hold any session-level state between queries and is safe to use behind PgBouncer in transaction mode.\n\n### PgBouncer compatibility\n\nGoodJob is not compatible with PgBouncer in _transaction_ mode, but is compatible with PgBouncer's _connection_ mode. GoodJob uses connection-based advisory locks and LISTEN/NOTIFY, both of which require full database connections.\n\nIf you want to use PgBouncer with the rest of your Rails app you can workaround this limitation by making a direct database connection available to GoodJob. With Rails 6.0's support for [multiple databases](https://guides.rubyonrails.org/active_record_multiple_databases.html), a direct connection to the database can be configured by following the three steps below.\n\n1. Define a direct connection to your database that is not proxied through PgBouncer, for example:\n\n    ```yml\n    # config/database.yml\n\n    production:\n      primary:\n        url: postgres://pgbouncer_host/my_database\n      primary_direct:\n        url: postgres://database_host/my_database\n    ```\n\n1. Create a new Active Record base class that uses the direct database connection\n\n    ```ruby\n    # app/models/application_direct_record.rb\n\n    class ApplicationDirectRecord \u003c ActiveRecord::Base\n      self.abstract_class = true\n      connects_to database: :primary_direct\n    end\n    ```\n\n1. Configure GoodJob to use the newly created Active Record base class:\n\n    ```ruby\n    # config/initializers/good_job.rb\n\n    GoodJob.active_record_parent_class = \"ApplicationDirectRecord\"\n    ```\n\n### CLI HTTP health check probes\n\n#### Default configuration\n\nGoodJob's CLI offers an http health check probe to better manage process lifecycle in containerized environments like Kubernetes:\n\n```bash\n# Run the CLI with a health check on port 7001\ngood_job start --probe-port=7001\n\n# or via an environment variable\nGOOD_JOB_PROBE_PORT=7001 good_job start\n\n# Probe the status\ncurl localhost:7001/status\ncurl localhost:7001/status/started\ncurl localhost:7001/status/connected\n```\n\nMultiple health checks are available at different paths:\n\n- `/` or `/status`: the CLI process is running\n- `/status/started`: the multithreaded job executor is running\n- `/status/connected`: the database connection is established\n\nThis can be configured, for example with Kubernetes:\n\n```yaml\nspec:\n  containers:\n    - name: good_job\n      image: my_app:latest\n      env:\n        - name: RAILS_ENV\n          value: production\n        - name: GOOD_JOB_PROBE_PORT\n          value: 7001\n      command:\n          - good_job\n          - start\n      ports:\n        - name: probe-port\n          containerPort: 7001\n      startupProbe:\n        httpGet:\n          path: \"/status/started\"\n          port: probe-port\n        failureThreshold: 30\n        periodSeconds: 10\n      livenessProbe:\n        httpGet:\n          path: \"/status/connected\"\n          port: probe-port\n        failureThreshold: 1\n        periodSeconds: 10\n```\n\n#### Custom configuration\n\nThe CLI health check probe server can be customized to serve additional information. Two things to note when customizing the probe server:\n\n- By default, the probe server uses a homespun single thread, blocking server so your custom app should be very simple and lightly used and could affect job performance.\n- The default probe server is not fully Rack compliant. Rack specifies various mandatory fields and some Rack apps assume those fields exist. If you do need to use a Rack app that depends on being fully Rack compliant, you can configure GoodJob to [use WEBrick as the server](#using-webrick)\n\nTo customize the probe server, set `config.good_job.probe_app` to a Rack app or a Rack builder:\n\n```ruby\n# config/initializers/good_job.rb OR config/application.rb OR config/environments/{RAILS_ENV}.rb\n\nRails.application.configure do\n  config.good_job.probe_app = Rack::Builder.new do\n    # Add your custom middleware\n    use Custom::AuthorizationMiddleware\n    use Custom::PrometheusExporter\n\n    # This is the default middleware\n    use GoodJob::ProbeServer::HealthcheckMiddleware\n    run GoodJob::ProbeServer::NotFoundApp # will return 404 for all other requests\n  end\nend\n```\n\n##### Using WEBrick\n\nIf your custom app requires a fully Rack compliant server, you can configure GoodJob to use WEBrick as the server:\n\n```ruby\n# config/initializers/good_job.rb OR config/application.rb OR config/environments/{RAILS_ENV}.rb\n\nRails.application.configure do\n  config.good_job.probe_handler = :webrick\nend\n\n```\n\nYou can also enable WEBrick through the command line:\n\n```bash\ngood_job start --probe-handler=webrick\n```\n\nor via an environment variable:\n\n```bash\nGOOD_JOB_PROBE_HANDLER=webrick good_job start\n```\n\nNote that GoodJob doesn't include WEBrick as a dependency, so you'll need to add it to your Gemfile:\n\n```ruby\n# Gemfile\ngem 'webrick'\n```\n\nIf WEBrick is configured to be used, but the dependency is not found, GoodJob will log a warning and fallback to the default probe server.\n\n### Pausing Jobs\n\nGoodJob allows for pausing jobs by queue or job class. This feature is currently opt-in because the performance impact of loading and filtering these attributes is not yet known. To enable this feature, add the following to your configuration:\n\n\u003e ```ruby\n\u003e config.good_job.enable_pauses = true\n\u003e ```\n\nPausing can be done via the Dashboard's Performance page, or in Ruby\n\n```ruby\n# To pause:\nGoodJob.pause(queue: \"default\")\nGoodJob.pause(job_class: \"MyJob\")\n\n# To check status\nGoodJob.paused # =\u003e { queues: [\"default\"], job_classes: [\"MyJob\"] }\nGoodJob.paused?(queue: \"default\") # =\u003e true\nGoodJob.paused?(job_class: \"MyJob\") # =\u003e true\nGoodJob.paused? # =\u003e true\n\n# To unpause\nGoodJob.unpause(queue: \"default\")\nGoodJob.unpause(job_class: \"MyJob\")\n````\n\n## Doing your best job with GoodJob\n\n_This section explains how to use GoodJob the most efficiently and performantly, according to its maintainers. GoodJob is very flexible and you don't necessarily have to use it this way, but the concepts explained here are part of GoodJob's design intent._\n\nBackground jobs are hard. There are two extremes:\n\n- **Throw resources (compute, servers, money) at it** by creating dedicated processes (or servers) for each type of job or queue and scaling them independently to achieve the lowest latency and highest throughput.\n- **Do the best you can in a small budget** by creating dedicated _thread pools_ within a process for each type of job or queue to produce quality-of-service and compromise maximum latency (or tail latency) because of shared resources and thread contention. You can even run them in the web process if you’re really cheap.\n\nThis section will largely focused on optimizing within the latter small-budget scenario, but the concepts and explanation should help you optimize the big-budget scenario too.\n\nLet’s start with anti-patterns, and then the rest of this section will explain an alternative:\n\n- **Don’t use functional names for your queues** like `mailers` or `sms` or `turbo` or `batch`.  Instead name them after the total latency target (the total duration within queue and executing till finish) you expect for that job e.g.`latency_30s` or `latency_5m` or `literally_whenever`.\n- **Priority can’t fix a lack of capacity.** Priority rules (i.e. weighing or ordering which jobs or queues execute first) only works when there is  capacity available to execute that _next_ job. When all capacity is in-use, priority cannot preempt a job that is already executing (\"head-of-line blocking\").\n\nThe following will explain methods to create homogenous workloads (based on latency) and increase execution capacity when queuing latency causes the jobs to exceed their total latency target.\n\n### Sizing jobs: mice and elephants\n\nQueuing theory will refer to fast/small/low-latency tasks as **Mice** (e.g. a password reset email, an MFA token via SMS) and slow/big/high-latency tasks as **Elephants** (e.g. sending an email newsletter to 10k recipients, a batched update that touches every record in the database).\n\nExplicitly group your jobs by their latency: how quickly you expect them to finish to achieve your expected quality of service. This should be their **total latency** (or duration) which is the sum of: **queuing latency** which is how long the job waits in queue until execution capacity becomes available (which ideally should be zero, because you have idle capacity and can start executing a job immediately as soon as it is enqueued or upon its scheduled time) and **execution latency** which is how long the job’s execution takes (e.g. the email being sent). Example: I expect this Password Reset Email Job to have a total latency of 30 seconds or less.\n\nIn a working application, you likely will have more gradations than just small and big or slow and fast (analogously: badgers, wildebeests; maybe even tardigrades or blue whales for tiny and huge, respectively), but there will regardless be a relatively small and countable number of discrete latency buckets to organize your jobs into.\n\n### Isolating by total latency\n\nThe most efficient workloads are homogenous (similar) workloads. If you know every job to be executed will take about the same amount of time, you can estimate the maximum delay for a new job at the back of the queue and have that drive decisions about capacity. Alternatively, if those jobs are heterogenous (mixed) it’s possible that a very slow/long-duration job could hold everything back for much longer than anticipated and it’s sorta random. That’s bad!\n\nA fun visual image here for a single-file queue is a doorway: If you only have 1 doorway, it must be big enough to fit an elephant. But if an elephant is going through the door (and it will go through slowly!) no mice can fit through the door until the elephant is fully clear. Your mice will be delayed!\n\nPriority will not help when an elephant is in the doorway. Yes, you could say mice have a higher priority than elephants and always allow any mouse to go _before_ any elephant in queue will start. But once an elephant *has started* going through the door, any subsequent mouse who arrives must wait for the elephant to egress regardless of their priority. In Active Job and Ruby, it’s really hard to stop or cancel or preempt a running job (unless you’ve already architected that into your jobs, like with the [`job-iteration`](https://github.com/Shopify/job-iteration) library)\n\nThe best solution is to have a 2nd door, but only sized for mice, so an elephant can’t ever block it. With a mouse-sized doorway _and_ an elephant-sized doorway, mice can still go through the big elephant door when an elephant isn’t using it. Each door has a _maximum_ size (or “latency”) we want it to accept, and smaller is ok, just not larger.\n\n### Configuring your queues\n\nIf we wanted to capture the previous 2-door scenario in GoodJob, we’d configure the queues like this;\n\n```ruby\nconfig.good_job.queues = \"mice:1; elephant,mice:1\"\n```\n\nThis configuration creates two isolated thread pools (separated by a semicolon) each with 1 thread each (the number after the colon). The 2nd thread pool recognizes that both elephants and mice can use that isolated thread pool; if there is an influx of mice, it's possible to use the elephant’s thread pool if an elephant isn't already in progress.\n\nSo what if we add an intermediately-sized `badgers` ? In that case, we can make 3 distinct queues:\n\n```ruby\nconfig.good_job.queues = \"mice:1; badgers,mice:1; elephants,badgers,mice:1\"\n```\n\nIn this case, we make a mouse sized queue, a badger sized queue, and an elephant sized queue. We can simplify this even further:\n\n```ruby\nconfig.good_job.queues = \"mice:1; badgers,mice:1; *:1\"\n```\n\nUsing the wildcard  `*` for any queue also helps ensure that if a job is enqueued to a newly declared queue (maybe via a dependency or just inadvertently) it will still get executed until you notice and decide on its appropriate latency target.\n\nIn these examples, the order doesn’t matter; it just is maybe more readable to go from the lowest-latency to largest-latency pool (the semicolon groups), and then within a pool to list the largest allowable latency first (the commas). Nothing here is about “job priority” or “queue priority”, this is wholly about grouping.\n\nIn your application, not the zoo, you’ll want to enqueue your `PaswordResetJob` on the `mice` queue, your `CreateComplicatedObjectJob` on the `badger` queue, and your `AuditEveryAccountEverJob` on the `elephant` queue. But you want to name your queues by latency, so that ends up being:\n\n```ruby\nconfig.good_job.queues = \"latency_30s:1; latency_2m,latency_30s:1; *:1\"\n```\n\nAnd you likely want to have more than one thread (though more than 3-5 threads per process will cause thread contention and slow everything down a bit):\n\n```ruby\nconfig.good_job.queues = \"latency_30s:2; latency_2m,latency_30s:2; *:2\"\n```\n\n### Additional observations\n\n- Unlike GoodJob, other Active Job backends may treat a \"queue\" and an \"isolated execution pool\" as one and the same. GoodJob allows composing multiple Active Job queues into the same pool for flexibility and to make it easier to migrate from functionally-named queues to latency-based ones.\n- You don't *have* to name your queues explicitly like `latency_30s` but it makes it easier to identify outliers and communicate your operational targets. Many people push back on this; that's ok. An option to capture functional details is to use GoodJob's Labels feature instead of encoding them in the queue name.\n- The downside of organizing your jobs like this is that you may have jobs with the same latency target but wildly different operational parameters, like being coupled to another system that has limited throughput or questionable reliability. GoodJob offers Concurrency and Throttling Controls, but isolation is always the most performant and reliable option, though it requires dedicated resources and costs more.\n- Observe, monitor, and adapt your job queues over time. You likely have incomplete information about the execution latency of your jobs inclusive of all dependencies across all scenarios. You should expect to adjust your queues and grouping over time as you observe their behavior.\n- If you find you have unreliable external dependencies that introduce latency, you may also want to further isolate your jobs based on those dependencies, for example, isolating `latency_10s_email_service` to its own execution pool.\n- Scale on queue latency. Per the previous point in which you do not have complete control over execution latency, you do have control over the queue latency. If queue latency is causing your jobs to miss their total latency target, you must add more capacity (e.g. processes or servers.\n- This is all largely about latency-based queue design. It’s possible to go further and organize by latency _and_ parallelism. For that I recommend Nate Berkopec’s [*Complete Guide to Rails Performance*](https://www.railsspeed.com/) which covers things like Amdahl’s Law.\n\n## Contribute\n\n\u003c!-- Please keep this section in sync with CONTRIBUTING.md --\u003e\n\nAll contributions, from feedback to code and beyond, are welcomed and appreciated 🙏\n\n- Review the [Prioritized Project Backlog](https://github.com/bensheldon/good_job/projects/1).\n- Open a new issue or contribute to an [existing Issue](https://github.com/bensheldon/good_job/issues). Questions or suggestions are fantastic.\n- Participate according to our [Code of Conduct](/CODE_OF_CONDUCT.md).\n- Financially support the project via [Sponsorship](https://github.com/sponsors/bensheldon).\n\nFor gem development and debugging information, please review the [README's Gem Development section](/README.md#gem-development).\n\n### Gem development\n\n#### Development setup\n\n```bash\n# Clone the repository locally\ngit clone git@github.com:bensheldon/good_job.git\n\n# Set up the gem development environment\nbin/setup\n```\n\n#### Rails development harness\n\nA Rails application exists within `demo` that is used for development, test, and GoodJob Demo environments.\n\n```bash\n# Run a local development webserver\nbin/rails s\n\n# Disable job execution and cron for cleaner console output\nGOOD_JOB_ENABLE_CRON=0 GOOD_JOB_EXECUTION_MODE=external bin/rails s\n\n# Open the Rails console\nbin/rails c\n```\n\nFor developing locally within another Ruby on Rails project:\n\n```bash\n# Within Ruby on Rails project directory\n# Ensure that the Gemfile is set to git with a branch e.g.\n# gem \"good_job\", git: \"https://github.com/bensheldon/good_job.git\", branch: \"main\"\n# Then, override the Bundle config to point to the local filesystem's good_job repository\nbundle config local.good_job /path/to/local/good_job/repository\n\n# Confirm that the local copy is used\nbundle install\n\n# =\u003e Using good_job 0.1.0 from https://github.com/bensheldon/good_job.git (at /Users/You/Projects/good_job@dc57fb0)\n```\n\n#### Running tests\n\nTests can be run against the primary development environment:\n\n```bash\n# Set up the gem development environment\nbin/setup\n\n# Run the tests\nbin/rspec\n```\n\nEnvironment variables that may help with debugging:\n\n- `LOUD=1`: display all stdout/stderr output from all sources. This is helpful because GoodJob wraps some tests with `quiet { }` for cleaner test output, but it can hinder debugging.\n- `SHOW_BROWSER=1`: Run system tests headfully with Chrome/Cuprite. Use `binding.irb` in the system tests to pause.\n\nThe gemfiles in `gemfiles/` can be used to run tests against different rails versions:\n\n```bash\n# Install dependencies\nBUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle install\n\n# Run the tests\nBUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bin/rspec\n```\n\n### Release\n\nPackage maintainers can release this gem by running:\n\n```bash\n# Sign into rubygems\n$ gem signin\n\n# Add a .env file with the following:\n# CHANGELOG_GITHUB_TOKEN= # Github Personal Access Token\n\n# Update version number, changelog, and create git commit:\n$ bundle exec rake release_good_job[minor] # major,minor,patch\n\n# ..and follow subsequent directions.\n```\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbensheldon%2Fgood_job","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbensheldon%2Fgood_job","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbensheldon%2Fgood_job/lists"}