good_job 1.1.4 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7134108a565e8b1efaa825cec90630cab0965fcfca5ff02f09751730f8839bdc
4
- data.tar.gz: d887e3b6cbb6d3d877793b186de2c0ca5a65178b968b2000821c6f9000c0fdd1
3
+ metadata.gz: 1f3fa6d323c18dfb49aaf3b87f0ca71492a935752e7ca140880b68daa72a60f9
4
+ data.tar.gz: 999b400b7e1dd22206898fb1cdc35f6255088ce94c3562fa48fe97fc20a23672
5
5
  SHA512:
6
- metadata.gz: cce4a02b62ded08da0b2665cb346bfbeda6fdffa15182c8ab680962f6ba1f3534a8b9ac4cad8ecfa60d34b15cdfaabff5c8c80e4b3f7bdaa99c2d7a0e4208fe9
7
- data.tar.gz: d9f13feb39a8700fe889c7b4250004095cdddeb5e1e9f06426610d10c13ce7c14d20db32810aa93dd4d1b7a075f585821bf4581634373a3585540bab0018f46c
6
+ metadata.gz: 1b9236562e20d4da1e5738d9b143fabe9377d854b392b6b0eb2d77c0009d9f8a0c82ef94b414446a7b5db0b2a43e477ad4b8a6c7bff737b69c9d8a744ee29151
7
+ data.tar.gz: cfcc994ea2f6e9a26c3b1115ccb2df6383e2897c3db891415745b86769dbdc411a62b7d60c89aa7ddb6880009c6124c795cef33efff55daa860ed7fd90e1b81d
@@ -1,5 +1,95 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.2.4](https://github.com/bensheldon/good_job/tree/v1.2.4) (2020-09-01)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.3...v1.2.4)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add environment variable to mirror `cleanup\_preserved\_jobs --before-seconds-ago=SECONDS` [\#110](https://github.com/bensheldon/good_job/issues/110)
10
+
11
+ **Closed issues:**
12
+
13
+ - Remove unused PgLocks class [\#121](https://github.com/bensheldon/good_job/issues/121)
14
+ - Fix minor issue with CommandLine option links in README.md [\#116](https://github.com/bensheldon/good_job/issues/116)
15
+ - Unused .advisory\_lock\_details in PgLocks [\#105](https://github.com/bensheldon/good_job/issues/105)
16
+
17
+ **Merged pull requests:**
18
+
19
+ - Remove unused PgLocks class [\#120](https://github.com/bensheldon/good_job/pull/120) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
20
+ - Better table name detection for Job queries [\#119](https://github.com/bensheldon/good_job/pull/119) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
21
+ - Fix readme CommandLine option links [\#115](https://github.com/bensheldon/good_job/pull/115) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
22
+ - Allow env variable config for cleanups [\#114](https://github.com/bensheldon/good_job/pull/114) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
23
+ - Have YARD render markdown files with GFM \(Github Flavored Markdown\) [\#113](https://github.com/bensheldon/good_job/pull/113) ([bensheldon](https://github.com/bensheldon))
24
+ - Add markdownlint to lint readme [\#109](https://github.com/bensheldon/good_job/pull/109) ([bensheldon](https://github.com/bensheldon))
25
+ - Remove unused method in PgLocks [\#107](https://github.com/bensheldon/good_job/pull/107) ([gadimbaylisahil](https://github.com/gadimbaylisahil))
26
+ - Re-organize Readme: frontload configuration, add Table of Contents [\#106](https://github.com/bensheldon/good_job/pull/106) ([bensheldon](https://github.com/bensheldon))
27
+
28
+ ## [v1.2.3](https://github.com/bensheldon/good_job/tree/v1.2.3) (2020-08-27)
29
+
30
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.2...v1.2.3)
31
+
32
+ **Closed issues:**
33
+
34
+ - requiring more dependencies in then needed [\#103](https://github.com/bensheldon/good_job/issues/103)
35
+
36
+ **Merged pull requests:**
37
+
38
+ - stop depending on all rails libs [\#104](https://github.com/bensheldon/good_job/pull/104) ([thilo](https://github.com/thilo))
39
+
40
+ ## [v1.2.2](https://github.com/bensheldon/good_job/tree/v1.2.2) (2020-08-27)
41
+
42
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.1...v1.2.2)
43
+
44
+ **Implemented enhancements:**
45
+
46
+ - Run Github Action tests against Ruby 2.5, 2.6, 2.7 [\#100](https://github.com/bensheldon/good_job/issues/100)
47
+
48
+ **Fixed bugs:**
49
+
50
+ - Freezes puma on code change [\#95](https://github.com/bensheldon/good_job/issues/95)
51
+ - Ruby 2.7 keyword arguments warning [\#93](https://github.com/bensheldon/good_job/issues/93)
52
+
53
+ **Closed issues:**
54
+
55
+ - Add test for `rails g good\_job:install` [\#57](https://github.com/bensheldon/good_job/issues/57)
56
+
57
+ **Merged pull requests:**
58
+
59
+ - Use more ActiveRecord in Lockable and not connection.execute [\#102](https://github.com/bensheldon/good_job/pull/102) ([bensheldon](https://github.com/bensheldon))
60
+ - Run CI tests on Ruby 2.5, 2.6, and 2.7 [\#101](https://github.com/bensheldon/good_job/pull/101) ([arku](https://github.com/arku))
61
+ - Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
62
+ - Fix Ruby 2.7 keyword arguments warning [\#98](https://github.com/bensheldon/good_job/pull/98) ([arku](https://github.com/arku))
63
+ - Remove executor/reloader for less interlocking [\#97](https://github.com/bensheldon/good_job/pull/97) ([sj26](https://github.com/sj26))
64
+ - Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
65
+ - Add test for `rails g good\_job:install` [\#94](https://github.com/bensheldon/good_job/pull/94) ([arku](https://github.com/arku))
66
+
67
+ ## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
68
+
69
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.0...v1.2.1)
70
+
71
+ **Fixed bugs:**
72
+
73
+ - undefined method `thread\_mattr\_accessor' when not requiring the Sprockets Railstie [\#85](https://github.com/bensheldon/good_job/issues/85)
74
+
75
+ **Closed issues:**
76
+
77
+ - Document comparison of GoodJob with other backends [\#51](https://github.com/bensheldon/good_job/issues/51)
78
+
79
+ **Merged pull requests:**
80
+
81
+ - Explicitly require thread\_mattr\_accessor from ActiveSupport [\#86](https://github.com/bensheldon/good_job/pull/86) ([bensheldon](https://github.com/bensheldon))
82
+ - Add comparison of other backends to Readme [\#84](https://github.com/bensheldon/good_job/pull/84) ([bensheldon](https://github.com/bensheldon))
83
+
84
+ ## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-20)
85
+
86
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
87
+
88
+ **Merged pull requests:**
89
+
90
+ - Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
91
+ - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
92
+
3
93
  ## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
4
94
 
5
95
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
@@ -11,7 +101,6 @@
11
101
 
12
102
  **Merged pull requests:**
13
103
 
14
- - Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
15
104
  - Allow Schedulers to filter \#create\_thread to avoid flood of queries when running async with multiple schedulers [\#81](https://github.com/bensheldon/good_job/pull/81) ([bensheldon](https://github.com/bensheldon))
16
105
  - Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
17
106
 
@@ -51,6 +140,7 @@
51
140
  **Merged pull requests:**
52
141
 
53
142
  - Allow instantiation of multiple schedulers via --queues [\#76](https://github.com/bensheldon/good_job/pull/76) ([bensheldon](https://github.com/bensheldon))
143
+ - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
54
144
 
55
145
  ## [v1.1.0](https://github.com/bensheldon/good_job/tree/v1.1.0) (2020-08-10)
56
146
 
@@ -85,7 +175,6 @@
85
175
 
86
176
  **Merged pull requests:**
87
177
 
88
- - Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
89
178
  - Re-perform a job if a StandardError bubbles up; better document job reliability [\#62](https://github.com/bensheldon/good_job/pull/62) ([bensheldon](https://github.com/bensheldon))
90
179
  - Update the setup documentation to use correct bin setup command [\#61](https://github.com/bensheldon/good_job/pull/61) ([jm96441n](https://github.com/jm96441n))
91
180
 
@@ -93,10 +182,6 @@
93
182
 
94
183
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.1...v1.0.2)
95
184
 
96
- **Fixed bugs:**
97
-
98
- - Fix counting of available execution threads [\#58](https://github.com/bensheldon/good_job/pull/58) ([bensheldon](https://github.com/bensheldon))
99
-
100
185
  **Merged pull requests:**
101
186
 
102
187
  - Add migration generator [\#56](https://github.com/bensheldon/good_job/pull/56) ([thedanbob](https://github.com/thedanbob))
@@ -108,7 +193,7 @@
108
193
 
109
194
  **Merged pull requests:**
110
195
 
111
- - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
196
+ - Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
112
197
 
113
198
  ## [v1.0.0](https://github.com/bensheldon/good_job/tree/v1.0.0) (2020-07-20)
114
199
 
@@ -118,6 +203,14 @@
118
203
 
119
204
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.2...v0.9.0)
120
205
 
206
+ **Fixed bugs:**
207
+
208
+ - Fix counting of available execution threads [\#58](https://github.com/bensheldon/good_job/pull/58) ([bensheldon](https://github.com/bensheldon))
209
+
210
+ **Merged pull requests:**
211
+
212
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
213
+
121
214
  ## [v0.8.2](https://github.com/bensheldon/good_job/tree/v0.8.2) (2020-07-18)
122
215
 
123
216
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.1...v0.8.2)
@@ -145,7 +238,6 @@
145
238
 
146
239
  **Merged pull requests:**
147
240
 
148
- - Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
149
241
  - Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
150
242
 
151
243
  ## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-16)
@@ -195,6 +287,7 @@
195
287
 
196
288
  - Improve ActiveRecord usage for advisory locking [\#24](https://github.com/bensheldon/good_job/pull/24) ([bensheldon](https://github.com/bensheldon))
197
289
  - Remove support for Rails 5.1 [\#23](https://github.com/bensheldon/good_job/pull/23) ([bensheldon](https://github.com/bensheldon))
290
+ - Clean up Gemspec [\#15](https://github.com/bensheldon/good_job/pull/15) ([bensheldon](https://github.com/bensheldon))
198
291
 
199
292
  ## [v0.3.0](https://github.com/bensheldon/good_job/tree/v0.3.0) (2020-03-22)
200
293
 
@@ -222,7 +315,6 @@
222
315
 
223
316
  **Merged pull requests:**
224
317
 
225
- - Clean up Gemspec [\#15](https://github.com/bensheldon/good_job/pull/15) ([bensheldon](https://github.com/bensheldon))
226
318
  - Set up Rubocop [\#14](https://github.com/bensheldon/good_job/pull/14) ([bensheldon](https://github.com/bensheldon))
227
319
  - Add pg gem as explicit dependency [\#13](https://github.com/bensheldon/good_job/pull/13) ([bensheldon](https://github.com/bensheldon))
228
320
  - Bump nokogiri from 1.10.7 to 1.10.9 [\#12](https://github.com/bensheldon/good_job/pull/12) ([dependabot[bot]](https://github.com/apps/dependabot))
data/README.md CHANGED
@@ -4,197 +4,281 @@ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
4
4
 
5
5
  **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, ActiveJob, and Postgres to be simple and performant for most workloads.**
6
6
 
7
- - **Designed for ActiveJob.** Complete support for [async, queues, delays, priorities, timeouts, and retries](https://edgeguides.rubyonrails.org/active_job_basics.html) with near-zero configuration.
8
- - **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).
7
+ - **Designed for ActiveJob.** Complete support for [async, queues, delays, priorities, timeouts, and retries](https://edgeguides.rubyonrails.org/active_job_basics.html) with near-zero configuration.
8
+ - **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).
9
9
  - **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.
10
10
  - **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue less than 1-million jobs/day.
11
11
 
12
12
  For more of the story of GoodJob, read the [introductory blog post](https://island94.org/2020/07/introducing-goodjob-1-0).
13
13
 
14
- ## Installation
14
+ <details markdown="1">
15
+ <summary><strong>📊 Comparison of GoodJob with other job queue backends (click to expand)</strong></summary>
16
+
17
+ | | Queues, priority, retries | Database | Concurrency | Reliability/Integrity | Latency |
18
+ |-----------------|---------------------------|---------------------------------------|-------------------|------------------------|--------------------------|
19
+ | **GoodJob** | ✅ Yes | ✅ Postgres | ✅ Multithreaded | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |
20
+ | **Que** | ✅ Yes | 🔶️ Postgres, requires `structure.sql` | ✅ Multithreaded | ✅ ACID, Advisory Locks | ✅ Postgres LISTEN/NOTIFY |
21
+ | **Delayed Job** | ✅ Yes | ✅ Postgres | 🔴 Single-threaded | ✅ ACID, record-based | 🔶 Polling |
22
+ | **Sidekiq** | ✅ Yes | 🔴 Redis | ✅ Multithreaded | 🔴 Crashes lose jobs | ✅ Redis BRPOP |
23
+ | **Sidekiq Pro** | ✅ Yes | 🔴 Redis | ✅ Multithreaded | ✅ Redis RPOPLPUSH | ✅ Redis RPOPLPUSH |
24
+
25
+ </details>
26
+
27
+ ## Table of contents
28
+
29
+ - [Set up](#set-up)
30
+ - [Configuration](#configuration)
31
+ - [Command-line options](#command-line-options)
32
+ - [`good_job start`](#good_job-start)
33
+ - [`good_job cleanup_preserved_jobs`](#good_job-cleanup_preserved_jobs)
34
+ - [Adapter options](#adapter-options)
35
+ - [Global options](#global-options)
36
+ - [Going deeper](#going-deeper)
37
+ - [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)
38
+ - [Exceptions](#exceptions)
39
+ - [Retries](#retries)
40
+ - [ActionMailer retries](#actionmailer-retries)
41
+ - [Timeouts](#timeouts)
42
+ - [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)
43
+ - [Database connections](#database-connections)
44
+ - [Executing jobs async / in-process](#executing-jobs-async--in-process)
45
+ - [Migrating to GoodJob from a different ActiveJob backend](#migrating-to-goodjob-from-a-different-activejob-backend)
46
+ - [Monitoring and preserving worked jobs](#monitoring-and-preserving-worked-jobs)
47
+ - [Contributing](#contributing)
48
+ - [Gem development](#gem-development)
49
+ - [Releasing](#releasing)
50
+ - [License](#license)
51
+
52
+ ## Set up
53
+
54
+ 1. Add `good_job` to your application's Gemfile:
15
55
 
16
- Add this line to your application's Gemfile:
56
+ ```ruby
57
+ gem 'good_job'
58
+ ```
17
59
 
18
- ```ruby
19
- gem 'good_job'
20
- ```
60
+ 1. Install the gem:
21
61
 
22
- And then execute:
23
- ```bash
24
- $ bundle install
25
- ```
62
+ ```bash
63
+ $ bundle install
64
+ ```
26
65
 
27
- ## Usage
66
+ 1. Run the GoodJob install generator. This will generate a database migration to create a table for GoodJob's job records:
28
67
 
29
- 1. Create a database migration:
30
-
31
- ```bash
68
+ ```bash
32
69
  $ bin/rails g good_job:install
33
70
  ```
34
71
 
35
72
  Run the migration:
36
-
73
+
37
74
  ```bash
38
75
  $ bin/rails db:migrate
39
76
  ```
40
-
77
+
41
78
  1. Configure the ActiveJob adapter:
42
-
43
- ```ruby
79
+
80
+ ```ruby
44
81
  # config/application.rb
45
82
  config.active_job.queue_adapter = :good_job
46
83
  ```
47
-
48
- By default, using `:good_job` is equivalent to manually configuring the adapter:
49
-
84
+
85
+ 1. Inside of your application, queue your job 🎉:
86
+
50
87
  ```ruby
51
- # config/environments/development.rb
52
- config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
53
-
54
- # config/environments/test.rb
55
- config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
56
-
57
- # config/environments/production.rb
58
- config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :external)
88
+ YourJob.perform_later
59
89
  ```
60
90
 
61
- 1. Queue your job 🎉:
62
-
91
+ GoodJob supports all ActiveJob features:
92
+
63
93
  ```ruby
64
94
  YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
65
95
  ```
66
96
 
67
- 1. In production, the scheduler is designed to run in its own process:
68
-
69
- ```bash
70
- $ bundle exec good_job
71
- ```
72
-
73
- Configuration options available with `help`:
97
+ 1. In development, GoodJob executes jobs immediately. In production, GoodJob provides different options:
74
98
 
75
- ```bash
76
- $ bundle exec good_job help start
77
-
78
- Usage:
79
- good_job start
80
-
81
- Options:
82
- [--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
83
- [--queues=queue1,queue2(;queue3,queue4:5;-queue1,queue2)] # Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)
84
- [--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
85
-
86
- Start job worker
87
- ```
99
+ - 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:
88
100
 
89
- 1. Optimize execution to reduce congestion and execution latency.
101
+ ```bash
102
+ $ bundle exec good_job start
103
+ ```
90
104
 
91
- By 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:
105
+ Ideally the command-line tool should be run on a separate machine or container from the web process. For example, on Heroku:
92
106
 
93
- - Multiple execution pools within a single process:
94
-
95
- ```bash
96
- $ bundle exec good_job --queues=transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;* --max-threads=5
107
+ ```Procfile
108
+ web: rails server
109
+ worker: bundle exec good_job start
97
110
  ```
98
-
99
- This configuration will result in a single process with 4 isolated thread execution pools. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
100
-
101
- - `transactional_messages:2`: execute jobs enqueued on `transactional_messages` with up to 2 threads.
102
- - `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
103
- - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
104
- - `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
105
-
106
- For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
107
-
108
- Configuration can be injected by environment variables too:
109
-
110
- ```bash
111
- $ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
111
+
112
+ The command-line tool supports a variety of options, see the reference below for command-line configuration.
113
+
114
+ - 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.
115
+
112
116
  ```
113
-
114
- - Multiple processes; for example, on Heroku:
115
-
116
- ```procfile
117
- # Procfile
118
-
119
- # Separate dyno types
120
- worker: bundle exec good_job --max-threads=5
121
- transactional_worker: bundle exec good_job --queues=transactional_messages --max-threads=2
122
- batch_worker: bundle exec good_job --queues=batch_processing --max-threads=1
123
-
124
- # Combined multi-process dyno
125
- combined_worker: bundle exec good_job --max-threads=5 & bundle exec good_job --queues=transactional_messages --max-threads=2 & bundle exec good_job --queues=batch_processing --max-threads=1 & wait -n
117
+ $ GOOD_JOB_EXECUTION_MODE=async rails server
126
118
  ```
127
-
128
- Running multiple processes can optimize for CPU performance at the expense of greater memory and system resource usage.
129
119
 
130
- _Keep in mind, queue operations and management is an advanced discipline. This stuff is complex, especially for heavy workloads and unique processing requirements. Good job 👍_
120
+ Additional configuration is likely necessary, see the reference below for async configuration.
131
121
 
132
- ### Error handling, retries, and reliability
122
+ ## Configuration
133
123
 
134
- GoodJob guarantees that a completely-performed job will run once and only once. GoodJob fully supports ActiveJob's built-in functionality for error handling, retries and timeouts. Writing reliable, transactional, and idempotent `ActiveJob#perform` methods is outside the scope of GoodJob.
124
+ ### Command-line options
135
125
 
136
- #### Error handling
126
+ There several top-level commands available through the `good_job` command-line tool.
137
127
 
138
- By default, if a job raises an error while it is being performed, _and it bubbles up to the GoodJob backend_, GoodJob will be immediately re-perform the job until it finishes successfully.
128
+ Configuration options are available with `help`.
139
129
 
140
- - `Exception`-type errors, such as a SIGINT, will always cause a job to be re-performed.
141
- - `StandardError`-type errors, by default, will cause a job to be re-performed, though this is configurable:
142
-
143
- ```ruby
144
- # config/initializers/good_job.rb
145
- GoodJob.reperform_jobs_on_standard_error = true # => default
146
- ```
130
+ #### `good_job start`
131
+
132
+ `good_job start` executes queued jobs.
133
+
134
+ ```bash
135
+ $ bundle exec good_job help start
136
+
137
+ Usage:
138
+ good_job start
139
+
140
+ Options:
141
+ [--max-threads=COUNT] # Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)
142
+ [--queues=QUEUE_LIST] # Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)
143
+ [--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)
144
+
145
+ Executes queued jobs.
146
+
147
+ All options can be configured with environment variables.
148
+ See option descriptions for the matching environment variable name.
149
+
150
+ == Configuring queues
151
+ Separate multiple queues with commas; exclude queues with a leading minus;
152
+ separate isolated execution pools with semicolons and threads with colons.
153
+ ```
154
+
155
+ #### `good_job cleanup_preserved_jobs`
156
+
157
+ `good_job cleanup_preserved_jobs` deletes preserved job records. See [`GoodJob.preserve_job_records` for when this command is useful.
158
+
159
+ ```bash
160
+ $ bundle exec good_job help cleanup_preserved_jobs
161
+
162
+ Usage:
163
+ good_job cleanup_preserved_jobs
164
+
165
+ Options:
166
+ [--before-seconds-ago=SECONDS] # Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)
167
+
168
+ Deletes preserved job records.
169
+
170
+ By default, GoodJob deletes job records when the job is performed and this
171
+ command is not necessary.
172
+
173
+ However, when `GoodJob.preserve_job_records = true`, the jobs will be
174
+ preserved in the database. This is useful when wanting to analyze or
175
+ inspect job performance.
176
+
177
+ If you are preserving job records this way, use this command regularly
178
+ to delete old records and preserve space in your database.
179
+ ```
180
+
181
+ ### Adapter options
182
+
183
+ To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job` or to an instance of `GoodJob::Adapter`, which you can configure with several options:
184
+
185
+ - `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:
186
+ - `:inline` executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.
187
+ - `:external` causes the adapter to equeue 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.
188
+ - `:async` causes the adapter to execute you jobs in separate threads in whatever process queued them (usually the web process). This is akin to running the command-line tool’s code inside your web server. It can be more economical for small workloads (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.
189
+ - `max_threads` (integer) sets the maximum number of threads to use when `execution_mode` is set to `:async`. You can also set this with the environment variable `GOOD_JOB_MAX_THREADS`.
190
+ - `queues` (string) determines which queues to execute jobs from when `execution_mode` is set to `:async`. See the description of `good_job start` for more details on the format of this string. You can also set this with the environment variable `GOOD_JOB_QUEUES`.
191
+ - `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`.
192
+
193
+ Using the symbol instead of explicitly configuring the options above (i.e. setting `config.active_job.queue_adapter = :good_job`) is equivalent to:
194
+
195
+ ```ruby
196
+ # config/environments/development.rb
197
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
198
+
199
+ # config/environments/test.rb
200
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
201
+
202
+ # config/environments/production.rb
203
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :external)
204
+ ```
205
+
206
+ ### Global options
207
+
208
+ Good Job’s general behavior can also be configured via several attributes directly on the `GoodJob` module:
209
+
210
+ - **`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`.
211
+ - **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
212
+ - **`GoodJob.reperform_jobs_on_standard_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
213
+ - **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when a job raises an error. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
147
214
 
148
- To report errors that _do_ bubble up to the GoodJob backend, assign a callable to `GoodJob.on_thread_error`. For example:
215
+ You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
149
216
 
150
217
  ```ruby
151
218
  # config/initializers/good_job.rb
219
+ GoodJob.preserve_job_records = true
220
+ GoodJob.reperform_jobs_on_standard_error = false
221
+ GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
222
+ ```
152
223
 
153
- # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
224
+ ## Going deeper
225
+
226
+ ### Exceptions, retries, and reliability
227
+
228
+ GoodJob guarantees that a completely-performed job will run once and only once. GoodJob fully supports ActiveJob's built-in functionality for error handling, retries and timeouts.
229
+
230
+ #### Exceptions
231
+
232
+ ActiveJob 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.
233
+
234
+ If 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.):
235
+
236
+ ```ruby
237
+ # config/initializers/good_job.rb
154
238
  GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
155
239
  ```
156
240
 
157
- ### Retrying jobs
241
+ #### Retries
158
242
 
159
- ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` will ensure that errors do not bubble up to the GoodJob backend:
243
+ By default, GoodJob will automatically and immediately retry a job when an exception is raised to GoodJob.
244
+
245
+ However, ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` prevents exceptions from reaching GoodJob:
160
246
 
161
247
  ```ruby
162
- class ApplicationJob < ActiveJob::Base
248
+ class ApplicationJob < ActiveJob::Base
163
249
  retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
164
250
  # ...
165
251
  end
166
252
  ```
167
253
 
168
- When specifying a limited number of retries, care must be taken to ensure that an error does not bubble up to the GoodJob backend because that will result in the job being re-performed:
254
+ When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob. GoodJob can be configured to discard un-handled exceptions instead of retrying them:
169
255
 
170
256
  ```ruby
171
- class ApplicationJob < ActiveJob::Base
257
+ # config/initializers/good_job.rb
258
+ GoodJob.reperform_jobs_on_standard_error = false
259
+ ```
260
+
261
+ Alternatively, pass a block to `retry_on` to handle the final exception instead of raising it to GoodJob:
262
+
263
+ ```ruby
264
+ class ApplicationJob < ActiveJob::Base
172
265
  retry_on StandardError, attempts: 5 do |_job, _exception|
173
- # Log error, etc.
174
- # You must implement this block, otherwise,
175
- # Active Job will re-raise the error.
176
- # Do not re-raise the error, otherwise
177
- # GoodJob will immediately re-perform the job.
266
+ # Log error, do nothing, etc.
178
267
  end
179
268
  # ...
180
269
  end
181
270
  ```
182
271
 
183
- GoodJob can be configured to allow omitting `retry_on`'s block argument and implicitly discard un-handled errors:
272
+ When 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:
184
273
 
185
274
  ```ruby
186
- # config/initializers/good_job.rb
187
-
188
- # Do NOT re-perform a job if a StandardError bubbles up to the GoodJob backend
189
- GoodJob.reperform_jobs_on_standard_error = false
190
- ```
275
+ class ApplicationJob < ActiveJob::Base
276
+ retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
191
277
 
192
- When using an exception monitoring service (e.g. Sentry, Bugsnag, Airbrake, Honeybadger, etc), the use of `rescue_on` may be incompatible with their ActiveJob integration. It's safest to explicitly wrap jobs with an exception reporter. For example:
278
+ retry_on SpecialError, attempts: 5 do |_job, exception|
279
+ Raven.capture_exception(exception)
280
+ end
193
281
 
194
- ```ruby
195
- class ApplicationJob < ActiveJob::Base
196
- retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
197
-
198
282
  around_perform do |_job, block|
199
283
  block.call
200
284
  rescue StandardError => e
@@ -205,12 +289,11 @@ class ApplicationJob < ActiveJob::Base
205
289
  end
206
290
  ```
207
291
 
208
-
209
- ActiveJob's `discard_on` functionality is supported too.
210
-
211
292
  #### ActionMailer retries
212
293
 
213
- Using a Mailer's `#deliver_later` will enqueue an instance of `ActionMailer::DeliveryJob` which inherits from `ActiveJob::Base` rather than your applications `ApplicationJob`. You can use an initializer to configure retries on `ActionMailer::DeliveryJob`:
294
+ Any configuration in `ApplicationJob` will have to be duplicated on `ActionMailer::DeliveryJob` because ActionMailer uses a custom class, `ActionMailer::DeliveryJob`, which inherits from `ActiveJob::Base`, rather than your applications `ApplicationJob`.
295
+
296
+ You can use an initializer to configure `ActionMailer::DeliveryJob`, for example:
214
297
 
215
298
  ```ruby
216
299
  # config/initializers/good_job.rb
@@ -225,14 +308,14 @@ rescue StandardError => e
225
308
  end
226
309
  ```
227
310
 
228
- #### Timeouts
311
+ ### Timeouts
229
312
 
230
313
  Job timeouts can be configured with an `around_perform`:
231
314
 
232
315
  ```ruby
233
- class ApplicationJob < ActiveJob::Base
316
+ class ApplicationJob < ActiveJob::Base
234
317
  JobTimeoutError = Class.new(StandardError)
235
-
318
+
236
319
  around_perform do |_job, block|
237
320
  # Timeout jobs after 10 minutes
238
321
  Timeout.timeout(10.minutes, JobTimeoutError) do
@@ -242,20 +325,61 @@ class ApplicationJob < ActiveJob::Base
242
325
  end
243
326
  ```
244
327
 
245
- ### Configuring job execution threads
246
-
247
- GoodJob executes enqueued jobs using threads. There is a lot than can be said about [multithreaded behavior in Ruby on Rails](https://guides.rubyonrails.org/threading_and_code_execution.html), but briefly:
328
+ ### Optimize queues, threads, and processes
329
+
330
+ By 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:
331
+
332
+ - Multiple execution pools within a single process:
248
333
 
249
- - Each GoodJob execution thread requires its own database connection, which are automatically checked out from Rails’s connection pool. _Allowing GoodJob to schedule more threads than are available in the database connection pool can lead to timeouts and is not recommended._
250
- - The maximum number of GoodJob threads can be configured, in decreasing precedence:
251
- 1. `$ bundle exec good_job --max_threads 4`
252
- 2. `$ GOOD_JOB_MAX_THREADS=4 bundle exec good_job`
253
- 3. `$ RAILS_MAX_THREADS=4 bundle exec good_job`
254
- 4. Implicitly via Rails's database connection pool size (`ActiveRecord::Base.connection_pool.size`)
334
+ ```bash
335
+ $ bundle exec good_job --queues=transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;* --max-threads=5
336
+ ```
337
+
338
+ This configuration will result in a single process with 4 isolated thread execution pools. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
339
+
340
+ - `transactional_messages:2`: execute jobs enqueued on `transactional_messages` with up to 2 threads.
341
+ - `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
342
+ - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
343
+ - `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
344
+
345
+ For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
346
+
347
+ Configuration can be injected by environment variables too:
348
+
349
+ ```bash
350
+ $ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
351
+ ```
352
+
353
+ - Multiple processes; for example, on Heroku:
354
+
355
+ ```procfile
356
+ # Procfile
357
+
358
+ # Separate dyno types
359
+ worker: bundle exec good_job --max-threads=5
360
+ transactional_worker: bundle exec good_job --queues=transactional_messages --max-threads=2
361
+ batch_worker: bundle exec good_job --queues=batch_processing --max-threads=1
362
+
363
+ # Combined multi-process dyno
364
+ combined_worker: bundle exec good_job --max-threads=5 & bundle exec good_job --queues=transactional_messages --max-threads=2 & bundle exec good_job --queues=batch_processing --max-threads=1 & wait -n
365
+ ```
366
+
367
+ Running multiple processes can optimize for CPU performance at the expense of greater memory and system resource usage.
368
+
369
+ Keep in mind, queue operations and management is an advanced discipline. This stuff is complex, especially for heavy workloads and unique processing requirements. Good job 👍
370
+
371
+ ### Database connections
372
+
373
+ Each GoodJob execution thread requires its own database connection that is automatically checked out from Rails’s connection pool. _Allowing GoodJob to create more threads than available database connections can lead to timeouts and is not recommended._ For example:
374
+
375
+ ```yaml
376
+ # config/database.yml
377
+ pool: <%= [ENV.fetch("RAILS_MAX_THREADS", 5).to_i, ENV.fetch("GOOD_JOB_MAX_THREADS", 4).to_i].max %>
378
+ ```
255
379
 
256
380
  ### Executing jobs async / in-process
257
381
 
258
- GoodJob is able to run "async" in the same process as the webserver (e.g. `bin/rail 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:
382
+ GoodJob can execute jobs "async" in the same process as the webserver (e.g. `bin/rail 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:
259
383
 
260
384
  - Directly configure the ActiveJob adapter:
261
385
 
@@ -263,12 +387,13 @@ GoodJob is able to run "async" in the same process as the webserver (e.g. `bin/r
263
387
  # config/environments/production.rb
264
388
  config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :async, max_threads: 4, poll_interval: 30)
265
389
  ```
390
+
266
391
  - Or, when using `...queue_adapter = :good_job`, via environment variables:
267
392
 
268
393
  ```bash
269
394
  $ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
270
395
  ```
271
-
396
+
272
397
  Depending on your application configuration, you may need to take additional steps:
273
398
 
274
399
  - Ensure that you have enough database connections for both web and job execution threads:
@@ -282,27 +407,27 @@ Depending on your application configuration, you may need to take additional ste
282
407
 
283
408
  ```ruby
284
409
  # config/puma.rb
285
-
410
+
286
411
  before_fork do
287
412
  GoodJob.shutdown
288
413
  end
289
-
414
+
290
415
  on_worker_boot do
291
416
  GoodJob.restart
292
417
  end
293
-
418
+
294
419
  on_worker_shutdown do
295
420
  GoodJob.shutdown
296
421
  end
297
-
422
+
298
423
  MAIN_PID = Process.pid
299
424
  at_exit do
300
425
  GoodJob.shutdown if Process.pid == MAIN_PID
301
426
  end
302
427
  ```
303
-
428
+
304
429
  GoodJob is compatible with Puma's `preload_app!` method.
305
-
430
+
306
431
  ### Migrating to GoodJob from a different ActiveJob backend
307
432
 
308
433
  If your application is already using an ActiveJob backend, you will need to install GoodJob to enqueue and perform newly created jobs _and_ finish performing pre-existing jobs on the previous backend.
@@ -318,7 +443,7 @@ If your application is already using an ActiveJob backend, you will need to inst
318
443
  ```
319
444
 
320
445
  1. 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:
321
-
446
+
322
447
  ```procfile
323
448
  # Procfile
324
449
  # ...
@@ -331,7 +456,7 @@ If your application is already using an ActiveJob backend, you will need to inst
331
456
 
332
457
  GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
333
458
 
334
- By default, GoodJob will delete job records after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
459
+ By default, GoodJob will delete job records after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
335
460
 
336
461
  To preserve job records for later inspection, set an initializer:
337
462
 
@@ -343,7 +468,7 @@ GoodJob.preserve_job_records = true
343
468
  It is also necessary to delete these preserved jobs from the database after a certain time period:
344
469
 
345
470
  - For example, in a Rake task:
346
-
471
+
347
472
  ```ruby
348
473
  GoodJob::Job.finished(1.day.ago).delete_all
349
474
  ```
@@ -413,7 +538,7 @@ $ gem signin
413
538
  # Update version number, changelog, and create git commit:
414
539
  $ bundle exec rake release[minor] # major,minor,patch
415
540
 
416
- # ..and follow subsequent directions.
541
+ # ..and follow subsequent directions.
417
542
  ```
418
543
 
419
544
  ## License
@@ -8,20 +8,44 @@ require 'good_job/job'
8
8
  require 'good_job/scheduler'
9
9
  require 'good_job/multi_scheduler'
10
10
  require 'good_job/adapter'
11
- require 'good_job/pg_locks'
12
11
  require 'good_job/performer'
13
12
  require 'good_job/current_execution'
14
13
  require 'good_job/notifier'
15
14
 
16
15
  require 'active_job/queue_adapters/good_job_adapter'
17
16
 
17
+ # GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
18
+ #
19
+ # +GoodJob+ is the top-level namespace and exposes configuration attributes.
18
20
  module GoodJob
21
+ # @!attribute [rw] logger
22
+ # @!scope class
23
+ # The logger used by GoodJob
24
+ # @return [Logger]
19
25
  mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
26
+
27
+ # @!attribute [rw] preserve_job_records
28
+ # @!scope class
29
+ # Whether to preserve job records in the database after they have finished for inspection
30
+ # @return [Boolean]
20
31
  mattr_accessor :preserve_job_records, default: false
32
+
33
+ # @!attribute [rw] reperform_jobs_on_standard_error
34
+ # @!scope class
35
+ # Whether to re-perform a job when a type of +StandardError+ is raised and bubbles up to the GoodJob backend
36
+ # @return [Boolean]
21
37
  mattr_accessor :reperform_jobs_on_standard_error, default: true
22
- mattr_accessor :on_thread_error, default: nil
23
38
 
24
- ActiveSupport.run_load_hooks(:good_job, self)
39
+ # @!attribute [rw] on_thread_error
40
+ # @!scope class
41
+ # Called when a thread raises an error
42
+ # @example Send errors to Sentry
43
+ # # config/initializers/good_job.rb
44
+ #
45
+ # # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
46
+ # GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
47
+ # @return [#call, nil]
48
+ mattr_accessor :on_thread_error, default: nil
25
49
 
26
50
  # Shuts down all execution pools
27
51
  # @param wait [Boolean] whether to wait for shutdown
@@ -43,4 +67,6 @@ module GoodJob
43
67
  Notifier.instances.each(&:restart)
44
68
  Scheduler.instances.each(&:restart)
45
69
  end
70
+
71
+ ActiveSupport.run_load_hooks(:good_job, self)
46
72
  end
@@ -9,10 +9,12 @@ module GoodJob
9
9
  end
10
10
 
11
11
  configuration = GoodJob::Configuration.new(
12
- execution_mode: execution_mode,
13
- queues: queues,
14
- max_threads: max_threads,
15
- poll_interval: poll_interval
12
+ {
13
+ execution_mode: execution_mode,
14
+ queues: queues,
15
+ max_threads: max_threads,
16
+ poll_interval: poll_interval,
17
+ }
16
18
  )
17
19
 
18
20
  @execution_mode = configuration.execution_mode
@@ -4,17 +4,29 @@ module GoodJob
4
4
  class CLI < Thor
5
5
  RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
6
6
 
7
- desc :start, "Start job worker"
7
+ desc :start, <<~DESCRIPTION
8
+ Executes queued jobs.
9
+
10
+ All options can be configured with environment variables.
11
+ See option descriptions for the matching environment variable name.
12
+
13
+ == Configuring queues
14
+ Separate multiple queues with commas; exclude queues with a leading minus;
15
+ separate isolated execution pools with semicolons and threads with colons.
16
+
17
+ DESCRIPTION
8
18
  method_option :max_threads,
9
19
  type: :numeric,
10
- desc: "Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)"
20
+ banner: 'COUNT',
21
+ desc: "Maximum number of threads to use for working jobs. (env var: GOOD_JOB_MAX_THREADS, default: 5)"
11
22
  method_option :queues,
12
23
  type: :string,
13
- banner: "queue1,queue2(;queue3,queue4:5;-queue1,queue2)",
14
- desc: "Queues to work from. Separate multiple queues with commas; exclude queues with a leading minus; separate isolated execution pools with semicolons and threads with colons (default: *)"
24
+ banner: "QUEUE_LIST",
25
+ desc: "Queues to work from. (env var: GOOD_JOB_QUEUES, default: *)"
15
26
  method_option :poll_interval,
16
27
  type: :numeric,
17
- desc: "Interval between polls for available jobs in seconds (default: 1)"
28
+ banner: 'SECONDS',
29
+ desc: "Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1)"
18
30
  def start
19
31
  set_up_application!
20
32
 
@@ -39,17 +51,35 @@ module GoodJob
39
51
 
40
52
  default_task :start
41
53
 
42
- desc :cleanup_preserved_jobs, "Delete preserved job records"
54
+ desc :cleanup_preserved_jobs, <<~DESCRIPTION
55
+ Deletes preserved job records.
56
+
57
+ By default, GoodJob deletes job records when the job is performed and this
58
+ command is not necessary.
59
+
60
+ However, when `GoodJob.preserve_job_records = true`, the jobs will be
61
+ preserved in the database. This is useful when wanting to analyze or
62
+ inspect job performance.
63
+
64
+ If you are preserving job records this way, use this command regularly
65
+ to delete old records and preserve space in your database.
66
+
67
+ DESCRIPTION
43
68
  method_option :before_seconds_ago,
44
69
  type: :numeric,
45
- default: 24 * 60 * 60,
46
- desc: "Delete records finished more than this many seconds ago"
47
-
70
+ banner: 'SECONDS',
71
+ desc: "Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)"
48
72
  def cleanup_preserved_jobs
49
73
  set_up_application!
50
74
 
51
- timestamp = Time.current - options[:before_seconds_ago]
52
- ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { before_seconds_ago: options[:before_seconds_ago], timestamp: timestamp }) do |payload|
75
+ configuration = GoodJob::Configuration.new(options)
76
+
77
+ timestamp = Time.current - configuration.cleanup_preserved_jobs_before_seconds_ago
78
+
79
+ ActiveSupport::Notifications.instrument(
80
+ "cleanup_preserved_jobs.good_job",
81
+ { before_seconds_ago: configuration.cleanup_preserved_jobs_before_seconds_ago, timestamp: timestamp }
82
+ ) do |payload|
53
83
  deleted_records_count = GoodJob::Job.finished(timestamp).delete_all
54
84
 
55
85
  payload[:deleted_records_count] = deleted_records_count
@@ -51,5 +51,13 @@ module GoodJob
51
51
  1
52
52
  ).to_i
53
53
  end
54
+
55
+ def cleanup_preserved_jobs_before_seconds_ago
56
+ (
57
+ options[:before_seconds_ago] ||
58
+ env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
59
+ 24 * 60 * 60
60
+ ).to_i
61
+ end
54
62
  end
55
63
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
2
+
1
3
  module GoodJob
2
4
  # Thread-local attributes for passing values from Instrumentation.
3
5
  # (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
@@ -58,7 +58,8 @@ module GoodJob
58
58
 
59
59
  unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
60
60
  good_job = good_jobs.first
61
- break unless good_job
61
+ # TODO: Determine why some records are fetched without an advisory lock at all
62
+ break unless good_job&.owns_advisory_lock?
62
63
 
63
64
  result, error = good_job.perform
64
65
  end
@@ -25,8 +25,8 @@ module GoodJob
25
25
  join_sql = <<~SQL
26
26
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
27
27
  AND pg_locks.objsubid = 1
28
- AND pg_locks.classid = ('x'||substr(md5(:table_name || "#{table_name}"."#{primary_key}"::text), 1, 16))::bit(32)::int
29
- AND pg_locks.objid = (('x'||substr(md5(:table_name || "#{table_name}"."#{primary_key}"::text), 1, 16))::bit(64) << 32)::bit(32)::int
28
+ AND pg_locks.classid = ('x'||substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(32)::int
29
+ AND pg_locks.objid = (('x'||substr(md5(:table_name || #{quoted_table_name}.#{quoted_primary_key}::text), 1, 16))::bit(64) << 32)::bit(32)::int
30
30
  SQL
31
31
 
32
32
  joins(sanitize_sql_for_conditions([join_sql, { table_name: table_name }]))
@@ -55,19 +55,17 @@ module GoodJob
55
55
  end
56
56
 
57
57
  def advisory_lock
58
- query = <<~SQL
59
- SELECT 1 AS one
60
- WHERE pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
58
+ where_sql = <<~SQL
59
+ pg_try_advisory_lock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
61
60
  SQL
62
- self.class.connection.execute(sanitize_sql_for_conditions([query, { table_name: self.class.table_name, id: send(self.class.primary_key) }])).ntuples.positive?
61
+ self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
63
62
  end
64
63
 
65
64
  def advisory_unlock
66
- query = <<~SQL
67
- SELECT 1 AS one
68
- WHERE pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
65
+ where_sql = <<~SQL
66
+ pg_advisory_unlock(('x'||substr(md5(:table_name || :id::text), 1, 16))::bit(64)::bigint)
69
67
  SQL
70
- self.class.connection.execute(sanitize_sql_for_conditions([query, { table_name: self.class.table_name, id: send(self.class.primary_key) }])).ntuples.positive?
68
+ self.class.unscoped.where(where_sql, { table_name: self.class.table_name, id: send(self.class.primary_key) }).exists?
71
69
  end
72
70
 
73
71
  def advisory_lock!
@@ -85,11 +83,11 @@ module GoodJob
85
83
  end
86
84
 
87
85
  def advisory_locked?
88
- self.class.advisory_locked.where(id: send(self.class.primary_key)).any?
86
+ self.class.unscoped.advisory_locked.where(id: send(self.class.primary_key)).exists?
89
87
  end
90
88
 
91
89
  def owns_advisory_lock?
92
- self.class.owns_advisory_locked.where(id: send(self.class.primary_key)).any?
90
+ self.class.unscoped.owns_advisory_locked.where(id: send(self.class.primary_key)).exists?
93
91
  end
94
92
 
95
93
  def advisory_unlock!
@@ -7,6 +7,7 @@ module GoodJob # :nodoc:
7
7
  class Notifier
8
8
  CHANNEL = 'good_job'.freeze
9
9
  POOL_OPTIONS = {
10
+ name: name,
10
11
  min_threads: 0,
11
12
  max_threads: 1,
12
13
  auto_terminate: true,
@@ -69,13 +70,12 @@ module GoodJob # :nodoc:
69
70
 
70
71
  def listen
71
72
  future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
72
- Rails.application.reloader.wrap do
73
- conn = ActiveRecord::Base.connection.raw_connection
74
- ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
75
- conn.async_exec "LISTEN #{CHANNEL}"
76
- end
73
+ begin
74
+ with_listen_connection do |conn|
75
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
76
+ conn.async_exec "LISTEN #{CHANNEL}"
77
+ end
77
78
 
78
- begin
79
79
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
80
80
  while pool.running?
81
81
  listening.make_true
@@ -93,14 +93,14 @@ module GoodJob # :nodoc:
93
93
  listening.make_false
94
94
  end
95
95
  end
96
- rescue StandardError => e
97
- ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
98
- raise
99
- ensure
100
- @listening.make_false
101
- ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
102
- conn.async_exec "UNLISTEN *"
103
- end
96
+ end
97
+ rescue StandardError => e
98
+ ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: e })
99
+ raise
100
+ ensure
101
+ @listening.make_false
102
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
103
+ conn.async_exec "UNLISTEN *"
104
104
  end
105
105
  end
106
106
  end
@@ -112,5 +112,16 @@ module GoodJob # :nodoc:
112
112
  def listen_observer(_time, _result, _thread_error)
113
113
  listen unless shutdown?
114
114
  end
115
+
116
+ def with_listen_connection
117
+ ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
118
+ ActiveRecord::Base.connection_pool.remove(conn)
119
+ end
120
+ pg_conn = ar_conn.raw_connection
121
+ pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}")
122
+ yield pg_conn
123
+ ensure
124
+ ar_conn.disconnect!
125
+ end
115
126
  end
116
127
  end
@@ -21,6 +21,7 @@ module GoodJob # :nodoc:
21
21
 
22
22
  # Defaults for instance of Concurrent::ThreadPoolExecutor
23
23
  DEFAULT_POOL_OPTIONS = {
24
+ name: name,
24
25
  min_threads: 0,
25
26
  max_threads: Concurrent.processor_count,
26
27
  auto_terminate: true,
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '1.1.4'.freeze
2
+ VERSION = '1.2.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-19 00:00:00.000000000 Z
11
+ date: 2020-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.1.0
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: concurrent-ruby
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +67,7 @@ dependencies:
39
67
  - !ruby/object:Gem::Version
40
68
  version: 1.0.0
41
69
  - !ruby/object:Gem::Dependency
42
- name: rails
70
+ name: railties
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - ">="
@@ -150,6 +178,48 @@ dependencies:
150
178
  - - ">="
151
179
  - !ruby/object:Gem::Version
152
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: kramdown
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: kramdown-parser-gfm
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: mdl
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
153
223
  - !ruby/object:Gem::Dependency
154
224
  name: pry-rails
155
225
  requirement: !ruby/object:Gem::Requirement
@@ -319,7 +389,6 @@ files:
319
389
  - lib/good_job/multi_scheduler.rb
320
390
  - lib/good_job/notifier.rb
321
391
  - lib/good_job/performer.rb
322
- - lib/good_job/pg_locks.rb
323
392
  - lib/good_job/railtie.rb
324
393
  - lib/good_job/scheduler.rb
325
394
  - lib/good_job/version.rb
@@ -1,21 +0,0 @@
1
- module GoodJob
2
- class PgLocks < ActiveRecord::Base
3
- self.table_name = 'pg_locks'.freeze
4
-
5
- # https://www.postgresql.org/docs/9.6/view-pg-locks.html
6
- # Advisory locks can be acquired on keys consisting of either a single bigint value or two integer values.
7
- # A bigint key is displayed with its high-order half in the classid column, its low-order half in the objid column, and objsubid equal to 1.
8
- # The original bigint value can be reassembled with the expression (classid::bigint << 32) | objid::bigint.
9
- # Integer keys are displayed with the first key in the classid column, the second key in the objid column, and objsubid equal to 2.
10
- # The actual meaning of the keys is up to the user. Advisory locks are local to each database, so the database column is meaningful for an advisory lock.
11
- def self.advisory_lock_details
12
- connection.select <<~SQL
13
- SELECT *
14
- FROM pg_locks
15
- WHERE
16
- locktype = 'advisory' AND
17
- objsubid = 1
18
- SQL
19
- end
20
- end
21
- end