good_job 1.1.1 → 1.2.1
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 +4 -4
- data/CHANGELOG.md +63 -1
- data/README.md +36 -11
- data/exe/good_job +1 -0
- data/lib/good_job.rb +51 -1
- data/lib/good_job/adapter.rb +16 -12
- data/lib/good_job/cli.rb +13 -5
- data/lib/good_job/current_execution.rb +37 -0
- data/lib/good_job/job.rb +36 -5
- data/lib/good_job/log_subscriber.rb +84 -27
- data/lib/good_job/multi_scheduler.rb +11 -2
- data/lib/good_job/notifier.rb +116 -0
- data/lib/good_job/performer.rb +8 -1
- data/lib/good_job/railtie.rb +10 -0
- data/lib/good_job/scheduler.rb +89 -28
- data/lib/good_job/version.rb +1 -1
- metadata +47 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08edeaef5cda608f5e5afd0ee0c8813805e4e88791a338dc66ab9129a9f727f6'
|
4
|
+
data.tar.gz: 3608a46f95035a843ea422cca420f344d870a8be558fe9f2cdb6c765c5cbd153
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57f471ffef16a4f1def70922e8601b3612de84441233523492a543b7276dbb00c5dd8e37a8fd7c58d930ac78c3ea0f65704a4bef4175ef588fbf65d11051eca7
|
7
|
+
data.tar.gz: e0d76a4990f613b81431e42c89f5ff1172b5353a9025d6684d866048771cbd845d51a12a212fe1df85934d7854f298a333d426b8738b0fbfd2fbbad27880f02a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,67 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.0...v1.2.1)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- undefined method `thread\_mattr\_accessor' when not requiring the Sprockets Railstie [\#85](https://github.com/bensheldon/good_job/issues/85)
|
10
|
+
- Document comparison of GoodJob with other backends [\#51](https://github.com/bensheldon/good_job/issues/51)
|
11
|
+
|
12
|
+
**Merged pull requests:**
|
13
|
+
|
14
|
+
- Explicitly require thread\_mattr\_accessor from ActiveSupport [\#86](https://github.com/bensheldon/good_job/pull/86) ([bensheldon](https://github.com/bensheldon))
|
15
|
+
- Add comparison of other backends to Readme [\#84](https://github.com/bensheldon/good_job/pull/84) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
|
17
|
+
## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-20)
|
18
|
+
|
19
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
|
20
|
+
|
21
|
+
**Merged pull requests:**
|
22
|
+
|
23
|
+
- Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
|
24
|
+
|
25
|
+
## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
|
26
|
+
|
27
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
|
28
|
+
|
29
|
+
**Implemented enhancements:**
|
30
|
+
|
31
|
+
- Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
|
32
|
+
- Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
|
33
|
+
|
34
|
+
**Merged pull requests:**
|
35
|
+
|
36
|
+
- Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
|
37
|
+
- 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))
|
38
|
+
- Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
|
39
|
+
|
40
|
+
## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
|
41
|
+
|
42
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
|
43
|
+
|
44
|
+
**Fixed bugs:**
|
45
|
+
|
46
|
+
- Job exceptions not properly attached to good\_jobs record [\#72](https://github.com/bensheldon/good_job/issues/72)
|
47
|
+
|
48
|
+
**Merged pull requests:**
|
49
|
+
|
50
|
+
- Capture errors via instrumentation from retry\_on and discard\_on [\#79](https://github.com/bensheldon/good_job/pull/79) ([bensheldon](https://github.com/bensheldon))
|
51
|
+
- Document GoodJob::Scheduler with Yard [\#78](https://github.com/bensheldon/good_job/pull/78) ([bensheldon](https://github.com/bensheldon))
|
52
|
+
|
53
|
+
## [v1.1.2](https://github.com/bensheldon/good_job/tree/v1.1.2) (2020-08-13)
|
54
|
+
|
55
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.1...v1.1.2)
|
56
|
+
|
57
|
+
**Implemented enhancements:**
|
58
|
+
|
59
|
+
- Allow the omission of queue names within a scheduler [\#73](https://github.com/bensheldon/good_job/issues/73)
|
60
|
+
|
61
|
+
**Merged pull requests:**
|
62
|
+
|
63
|
+
- Allow named queues to be excluded with a minus [\#77](https://github.com/bensheldon/good_job/pull/77) ([bensheldon](https://github.com/bensheldon))
|
64
|
+
|
3
65
|
## [v1.1.1](https://github.com/bensheldon/good_job/tree/v1.1.1) (2020-08-12)
|
4
66
|
|
5
67
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.0...v1.1.1)
|
@@ -124,7 +186,6 @@
|
|
124
186
|
- Add more examples to Readme [\#39](https://github.com/bensheldon/good_job/pull/39) ([bensheldon](https://github.com/bensheldon))
|
125
187
|
- Add additional Rubocops and lint [\#38](https://github.com/bensheldon/good_job/pull/38) ([bensheldon](https://github.com/bensheldon))
|
126
188
|
- Always store a default queue\_name, priority and scheduled\_at; index by queue\_name and scheduled\_at [\#37](https://github.com/bensheldon/good_job/pull/37) ([bensheldon](https://github.com/bensheldon))
|
127
|
-
- Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
|
128
189
|
|
129
190
|
## [v0.6.0](https://github.com/bensheldon/good_job/tree/v0.6.0) (2020-07-15)
|
130
191
|
|
@@ -140,6 +201,7 @@
|
|
140
201
|
- Improve generation of changelog [\#36](https://github.com/bensheldon/good_job/pull/36) ([bensheldon](https://github.com/bensheldon))
|
141
202
|
- Update Github Action Workflow for Backlog Project Board [\#35](https://github.com/bensheldon/good_job/pull/35) ([bensheldon](https://github.com/bensheldon))
|
142
203
|
- Add configuration options to good\_job executable [\#33](https://github.com/bensheldon/good_job/pull/33) ([bensheldon](https://github.com/bensheldon))
|
204
|
+
- Extract Job querying behavior out of Scheduler [\#31](https://github.com/bensheldon/good_job/pull/31) ([bensheldon](https://github.com/bensheldon))
|
143
205
|
- Allow configuration of Rails queue adapter with `:good\_job` [\#28](https://github.com/bensheldon/good_job/pull/28) ([bensheldon](https://github.com/bensheldon))
|
144
206
|
|
145
207
|
## [v0.5.0](https://github.com/bensheldon/good_job/tree/v0.5.0) (2020-07-13)
|
data/README.md
CHANGED
@@ -6,11 +6,24 @@ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
|
|
6
6
|
|
7
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
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
|
-
- **Backed by Postgres.** Relies upon Postgres integrity
|
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
|
+
<details>
|
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
|
+
|
14
27
|
## Installation
|
15
28
|
|
16
29
|
Add this line to your application's Gemfile:
|
@@ -79,29 +92,36 @@ $ bundle install
|
|
79
92
|
good_job start
|
80
93
|
|
81
94
|
Options:
|
82
|
-
[--max-threads=N]
|
83
|
-
[--queues=queue1,queue2(;queue3,queue4:5)] # Queues to work from. Separate multiple queues with commas; separate isolated execution pools with semicolons and threads with colons (default: *)
|
84
|
-
[--poll-interval=N]
|
95
|
+
[--max-threads=N] # Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)
|
96
|
+
[--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: *)
|
97
|
+
[--poll-interval=N] # Interval between polls for available jobs in seconds (default: 1)
|
85
98
|
|
86
99
|
Start job worker
|
87
100
|
```
|
88
101
|
|
89
|
-
1. Optimize execution to reduce congestion and execution latency.
|
102
|
+
1. Optimize execution to reduce congestion and execution latency.
|
103
|
+
|
104
|
+
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:
|
90
105
|
|
91
106
|
- Multiple execution pools within a single process:
|
92
107
|
|
93
108
|
```bash
|
94
|
-
$ bundle exec good_job --queues
|
109
|
+
$ bundle exec good_job --queues=transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;* --max-threads=5
|
95
110
|
```
|
96
111
|
|
97
|
-
This configuration will result in a single process with
|
112
|
+
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 (`:`)
|
113
|
+
|
114
|
+
- `transactional_messages:2`: execute jobs enqueued on `transactional_messages` with up to 2 threads.
|
115
|
+
- `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
|
116
|
+
- `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
|
117
|
+
- `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
|
98
118
|
|
99
119
|
For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
|
100
120
|
|
101
121
|
Configuration can be injected by environment variables too:
|
102
122
|
|
103
123
|
```bash
|
104
|
-
$ GOOD_JOB_QUEUES="
|
124
|
+
$ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
|
105
125
|
```
|
106
126
|
|
107
127
|
- Multiple processes; for example, on Heroku:
|
@@ -277,15 +297,20 @@ Depending on your application configuration, you may need to take additional ste
|
|
277
297
|
# config/puma.rb
|
278
298
|
|
279
299
|
before_fork do
|
280
|
-
GoodJob
|
300
|
+
GoodJob.shutdown
|
281
301
|
end
|
282
302
|
|
283
303
|
on_worker_boot do
|
284
|
-
GoodJob
|
304
|
+
GoodJob.restart
|
285
305
|
end
|
286
306
|
|
287
307
|
on_worker_shutdown do
|
288
|
-
GoodJob
|
308
|
+
GoodJob.shutdown
|
309
|
+
end
|
310
|
+
|
311
|
+
MAIN_PID = Process.pid
|
312
|
+
at_exit do
|
313
|
+
GoodJob.shutdown if Process.pid == MAIN_PID
|
289
314
|
end
|
290
315
|
```
|
291
316
|
|
data/exe/good_job
CHANGED
data/lib/good_job.rb
CHANGED
@@ -10,14 +10,64 @@ require 'good_job/multi_scheduler'
|
|
10
10
|
require 'good_job/adapter'
|
11
11
|
require 'good_job/pg_locks'
|
12
12
|
require 'good_job/performer'
|
13
|
+
require 'good_job/current_execution'
|
14
|
+
require 'good_job/notifier'
|
13
15
|
|
14
16
|
require 'active_job/queue_adapters/good_job_adapter'
|
15
17
|
|
18
|
+
# GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
|
19
|
+
#
|
20
|
+
# +GoodJob+ is the top-level namespace and exposes configuration attributes.
|
16
21
|
module GoodJob
|
17
|
-
|
22
|
+
# @!attribute [rw] logger
|
23
|
+
# @!scope class
|
24
|
+
# The logger used by GoodJob
|
25
|
+
# @return [Logger]
|
26
|
+
mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
27
|
+
|
28
|
+
# @!attribute [rw] preserve_job_records
|
29
|
+
# @!scope class
|
30
|
+
# Whether to preserve job records in the database after they have finished for inspection
|
31
|
+
# @return [Boolean]
|
18
32
|
mattr_accessor :preserve_job_records, default: false
|
33
|
+
|
34
|
+
# @!attribute [rw] reperform_jobs_on_standard_error
|
35
|
+
# @!scope class
|
36
|
+
# Whether to re-perform a job when a type of +StandardError+ is raised and bubbles up to the GoodJob backend
|
37
|
+
# @return [Boolean]
|
19
38
|
mattr_accessor :reperform_jobs_on_standard_error, default: true
|
39
|
+
|
40
|
+
# @!attribute [rw] on_thread_error
|
41
|
+
# @!scope class
|
42
|
+
# Called when a thread raises an error
|
43
|
+
# @example Send errors to Sentry
|
44
|
+
# # config/initializers/good_job.rb
|
45
|
+
#
|
46
|
+
# # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
|
47
|
+
# GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
48
|
+
# @return [#call, nil]
|
20
49
|
mattr_accessor :on_thread_error, default: nil
|
21
50
|
|
51
|
+
# Shuts down all execution pools
|
52
|
+
# @param wait [Boolean] whether to wait for shutdown
|
53
|
+
# @return [void]
|
54
|
+
def self.shutdown(wait: true)
|
55
|
+
Notifier.instances.each { |adapter| adapter.shutdown(wait: wait) }
|
56
|
+
Scheduler.instances.each { |scheduler| scheduler.shutdown(wait: wait) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Tests if execution pools are shut down
|
60
|
+
# @return [Boolean] whether execution pools are shut down
|
61
|
+
def self.shutdown?
|
62
|
+
Notifier.instances.all?(&:shutdown?) && Scheduler.instances.all?(&:shutdown?)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Restarts all execution pools
|
66
|
+
# @return [void]
|
67
|
+
def self.restart
|
68
|
+
Notifier.instances.each(&:restart)
|
69
|
+
Scheduler.instances.each(&:restart)
|
70
|
+
end
|
71
|
+
|
22
72
|
ActiveSupport.run_load_hooks(:good_job, self)
|
23
73
|
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -2,25 +2,27 @@ module GoodJob
|
|
2
2
|
class Adapter
|
3
3
|
EXECUTION_MODES = [:async, :external, :inline].freeze
|
4
4
|
|
5
|
-
def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
|
5
|
+
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, scheduler: nil, notifier: nil, inline: false)
|
6
6
|
if inline && execution_mode.nil?
|
7
7
|
ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
|
8
8
|
execution_mode = :inline
|
9
9
|
end
|
10
10
|
|
11
|
-
configuration = GoodJob::Configuration.new(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(configuration.execution_mode)
|
11
|
+
configuration = GoodJob::Configuration.new(
|
12
|
+
execution_mode: execution_mode,
|
13
|
+
queues: queues,
|
14
|
+
max_threads: max_threads,
|
15
|
+
poll_interval: poll_interval
|
16
|
+
)
|
19
17
|
|
20
18
|
@execution_mode = configuration.execution_mode
|
19
|
+
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
|
21
20
|
|
22
|
-
@
|
23
|
-
|
21
|
+
if @execution_mode == :async # rubocop:disable Style/GuardClause
|
22
|
+
@notifier = notifier || GoodJob::Notifier.new
|
23
|
+
@scheduler = scheduler || GoodJob::Scheduler.from_configuration(configuration)
|
24
|
+
@notifier.recipients << [@scheduler, :create_thread]
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def enqueue(active_job)
|
@@ -42,12 +44,14 @@ module GoodJob
|
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
45
|
-
@scheduler.create_thread
|
47
|
+
executed_locally = execute_async? && @scheduler.create_thread(queue_name: good_job.queue_name)
|
48
|
+
Notifier.notify(queue_name: good_job.queue_name) unless executed_locally
|
46
49
|
|
47
50
|
good_job
|
48
51
|
end
|
49
52
|
|
50
53
|
def shutdown(wait: true)
|
54
|
+
@notifier&.shutdown(wait: wait)
|
51
55
|
@scheduler&.shutdown(wait: wait)
|
52
56
|
end
|
53
57
|
|
data/lib/good_job/cli.rb
CHANGED
@@ -10,16 +10,18 @@ module GoodJob
|
|
10
10
|
desc: "Maximum number of threads to use for working jobs (default: ActiveRecord::Base.connection_pool.size)"
|
11
11
|
method_option :queues,
|
12
12
|
type: :string,
|
13
|
-
banner: "queue1,queue2(;queue3,queue4:5)",
|
14
|
-
desc: "Queues to work from. Separate multiple queues with commas; separate isolated execution pools with semicolons and threads with colons (default: *)"
|
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: *)"
|
15
15
|
method_option :poll_interval,
|
16
16
|
type: :numeric,
|
17
17
|
desc: "Interval between polls for available jobs in seconds (default: 1)"
|
18
18
|
def start
|
19
19
|
set_up_application!
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
notifier = GoodJob::Notifier.new
|
22
|
+
configuration = GoodJob::Configuration.new(options)
|
23
|
+
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
24
|
+
notifier.recipients << [scheduler, :create_thread]
|
23
25
|
|
24
26
|
@stop_good_job_executable = false
|
25
27
|
%w[INT TERM].each do |signal|
|
@@ -28,9 +30,10 @@ module GoodJob
|
|
28
30
|
|
29
31
|
Kernel.loop do
|
30
32
|
sleep 0.1
|
31
|
-
break if @stop_good_job_executable || scheduler.shutdown?
|
33
|
+
break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
|
32
34
|
end
|
33
35
|
|
36
|
+
notifier.shutdown
|
34
37
|
scheduler.shutdown
|
35
38
|
end
|
36
39
|
|
@@ -41,6 +44,7 @@ module GoodJob
|
|
41
44
|
type: :numeric,
|
42
45
|
default: 24 * 60 * 60,
|
43
46
|
desc: "Delete records finished more than this many seconds ago"
|
47
|
+
|
44
48
|
def cleanup_preserved_jobs
|
45
49
|
set_up_application!
|
46
50
|
|
@@ -55,6 +59,10 @@ module GoodJob
|
|
55
59
|
no_commands do
|
56
60
|
def set_up_application!
|
57
61
|
require RAILS_ENVIRONMENT_RB
|
62
|
+
return unless defined?(GOOD_JOB_LOG_TO_STDOUT) && GOOD_JOB_LOG_TO_STDOUT && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, STDOUT)
|
63
|
+
|
64
|
+
GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
65
|
+
GoodJob::LogSubscriber.reset_logger
|
58
66
|
end
|
59
67
|
end
|
60
68
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
# Thread-local attributes for passing values from Instrumentation.
|
5
|
+
# (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
|
6
|
+
|
7
|
+
module CurrentExecution
|
8
|
+
# @!attribute [rw] error_on_retry
|
9
|
+
# @!scope class
|
10
|
+
# Error captured by retry_on
|
11
|
+
# @return [Exception, nil]
|
12
|
+
thread_mattr_accessor :error_on_retry
|
13
|
+
|
14
|
+
# @!attribute [rw] error_on_discard
|
15
|
+
# @!scope class
|
16
|
+
# Error captured by discard_on
|
17
|
+
# @return [Exception, nil]
|
18
|
+
thread_mattr_accessor :error_on_discard
|
19
|
+
|
20
|
+
# Resets attributes
|
21
|
+
# @return [void]
|
22
|
+
def self.reset
|
23
|
+
self.error_on_retry = nil
|
24
|
+
self.error_on_discard = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Integer] Current process ID
|
28
|
+
def self.process_id
|
29
|
+
Process.pid
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [String] Current thread name
|
33
|
+
def self.thread_name
|
34
|
+
(Thread.current.name || Thread.current.object_id).to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -9,6 +9,25 @@ module GoodJob
|
|
9
9
|
|
10
10
|
self.table_name = 'good_jobs'.freeze
|
11
11
|
|
12
|
+
def self.queue_parser(string)
|
13
|
+
string = string.presence || '*'
|
14
|
+
|
15
|
+
if string.first == '-'
|
16
|
+
exclude_queues = true
|
17
|
+
string = string[1..-1]
|
18
|
+
end
|
19
|
+
|
20
|
+
queues = string.split(',').map(&:strip)
|
21
|
+
|
22
|
+
if queues.include?('*')
|
23
|
+
{ all: true }
|
24
|
+
elsif exclude_queues
|
25
|
+
{ exclude: queues }
|
26
|
+
else
|
27
|
+
{ include: queues }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
12
31
|
scope :unfinished, (lambda do
|
13
32
|
if column_names.include?('finished_at')
|
14
33
|
where(finished_at: nil)
|
@@ -21,8 +40,15 @@ module GoodJob
|
|
21
40
|
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
22
41
|
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
23
42
|
scope :queue_string, (lambda do |string|
|
24
|
-
|
25
|
-
|
43
|
+
parsed = queue_parser(string)
|
44
|
+
|
45
|
+
if parsed[:all]
|
46
|
+
all
|
47
|
+
elsif parsed[:exclude]
|
48
|
+
where.not(queue_name: parsed[:exclude]).or where(queue_name: nil)
|
49
|
+
elsif parsed[:include]
|
50
|
+
where(queue_name: parsed[:include])
|
51
|
+
end
|
26
52
|
end)
|
27
53
|
|
28
54
|
def self.perform_with_advisory_lock
|
@@ -47,7 +73,7 @@ module GoodJob
|
|
47
73
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
48
74
|
priority: active_job.priority || DEFAULT_PRIORITY,
|
49
75
|
serialized_params: active_job.serialize,
|
50
|
-
scheduled_at: scheduled_at
|
76
|
+
scheduled_at: scheduled_at,
|
51
77
|
create_with_advisory_lock: create_with_advisory_lock
|
52
78
|
)
|
53
79
|
|
@@ -63,11 +89,11 @@ module GoodJob
|
|
63
89
|
def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
|
64
90
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
65
91
|
|
92
|
+
GoodJob::CurrentExecution.reset
|
66
93
|
result = nil
|
67
94
|
rescued_error = nil
|
68
95
|
error = nil
|
69
96
|
|
70
|
-
ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
|
71
97
|
self.performed_at = Time.current
|
72
98
|
save! unless destroy_after
|
73
99
|
|
@@ -76,18 +102,23 @@ module GoodJob
|
|
76
102
|
)
|
77
103
|
|
78
104
|
begin
|
79
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
|
105
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
80
106
|
result = ActiveJob::Base.execute(params)
|
81
107
|
end
|
82
108
|
rescue StandardError => e
|
83
109
|
rescued_error = e
|
84
110
|
end
|
85
111
|
|
112
|
+
retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
|
113
|
+
GoodJob::CurrentExecution.error_on_discard
|
114
|
+
|
86
115
|
if rescued_error
|
87
116
|
error = rescued_error
|
88
117
|
elsif result.is_a?(Exception)
|
89
118
|
error = result
|
90
119
|
result = nil
|
120
|
+
elsif retry_or_discard_error
|
121
|
+
error = retry_or_discard_error
|
91
122
|
end
|
92
123
|
|
93
124
|
error_message = "#{error.class}: #{error.message}" if error
|
@@ -32,7 +32,7 @@ module GoodJob
|
|
32
32
|
performer_name = event.payload[:performer_name]
|
33
33
|
process_id = event.payload[:process_id]
|
34
34
|
|
35
|
-
|
35
|
+
info(tags: [process_id]) do
|
36
36
|
"GoodJob started scheduler with queues=#{performer_name} max_threads=#{max_threads} poll_interval=#{poll_interval}."
|
37
37
|
end
|
38
38
|
end
|
@@ -40,7 +40,7 @@ module GoodJob
|
|
40
40
|
def scheduler_shutdown_start(event)
|
41
41
|
process_id = event.payload[:process_id]
|
42
42
|
|
43
|
-
|
43
|
+
info(tags: [process_id]) do
|
44
44
|
"GoodJob shutting down scheduler..."
|
45
45
|
end
|
46
46
|
end
|
@@ -48,7 +48,7 @@ module GoodJob
|
|
48
48
|
def scheduler_shutdown(event)
|
49
49
|
process_id = event.payload[:process_id]
|
50
50
|
|
51
|
-
|
51
|
+
info(tags: [process_id]) do
|
52
52
|
"GoodJob scheduler is shut down."
|
53
53
|
end
|
54
54
|
end
|
@@ -56,24 +56,100 @@ module GoodJob
|
|
56
56
|
def scheduler_restart_pools(event)
|
57
57
|
process_id = event.payload[:process_id]
|
58
58
|
|
59
|
-
|
59
|
+
info(tags: [process_id]) do
|
60
60
|
"GoodJob scheduler has restarted."
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
def perform_job(event)
|
65
|
+
good_job = event.payload[:good_job]
|
66
|
+
process_id = event.payload[:process_id]
|
67
|
+
thread_name = event.payload[:thread_name]
|
68
|
+
|
69
|
+
info(tags: [process_id, thread_name]) do
|
70
|
+
"Executed GoodJob #{good_job.id}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def notifier_listen(_event)
|
75
|
+
info do
|
76
|
+
"Notifier subscribed with LISTEN"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def notifier_notified(event)
|
81
|
+
payload = event.payload[:payload]
|
82
|
+
|
83
|
+
debug do
|
84
|
+
"Notifier received payload: #{payload}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def notifier_notify_error(event)
|
89
|
+
error = event.payload[:error]
|
90
|
+
|
91
|
+
error do
|
92
|
+
"Notifier errored: #{error}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def notifier_unlisten(_event)
|
97
|
+
info do
|
98
|
+
"Notifier unsubscribed with UNLISTEN"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
64
102
|
def cleanup_preserved_jobs(event)
|
65
103
|
timestamp = event.payload[:timestamp]
|
66
104
|
deleted_records_count = event.payload[:deleted_records_count]
|
67
105
|
|
68
|
-
|
106
|
+
info do
|
69
107
|
"GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
|
70
108
|
end
|
71
109
|
end
|
72
110
|
|
73
|
-
|
111
|
+
class << self
|
112
|
+
def loggers
|
113
|
+
@_loggers ||= [GoodJob.logger]
|
114
|
+
end
|
115
|
+
|
116
|
+
def logger
|
117
|
+
@_logger ||= begin
|
118
|
+
logger = Logger.new(StringIO.new)
|
119
|
+
loggers.each do |each_logger|
|
120
|
+
logger.extend(ActiveSupport::Logger.broadcast(each_logger))
|
121
|
+
end
|
122
|
+
logger
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def reset_logger
|
127
|
+
@_logger = nil
|
128
|
+
end
|
129
|
+
end
|
74
130
|
|
75
131
|
def logger
|
76
|
-
GoodJob.logger
|
132
|
+
GoodJob::LogSubscriber.logger
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def tag_logger(*tags, &block)
|
138
|
+
tags = tags.dup.unshift("GoodJob").compact
|
139
|
+
|
140
|
+
self.class.loggers.inject(block) do |inner, each_logger|
|
141
|
+
if each_logger.respond_to?(:tagged)
|
142
|
+
tags_for_logger = if each_logger.formatter.current_tags.include?("ActiveJob")
|
143
|
+
["ActiveJob"] + tags
|
144
|
+
else
|
145
|
+
tags
|
146
|
+
end
|
147
|
+
|
148
|
+
proc { each_logger.tagged(*tags_for_logger, &inner) }
|
149
|
+
else
|
150
|
+
inner
|
151
|
+
end
|
152
|
+
end.call
|
77
153
|
end
|
78
154
|
|
79
155
|
%w(info debug warn error fatal unknown).each do |level|
|
@@ -81,30 +157,11 @@ module GoodJob
|
|
81
157
|
def #{level}(progname = nil, tags: [], &block)
|
82
158
|
return unless logger
|
83
159
|
|
84
|
-
|
85
|
-
tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
|
86
|
-
logger.tagged(*tags.compact) do
|
87
|
-
logger.#{level}(progname, &block)
|
88
|
-
end
|
89
|
-
else
|
160
|
+
tag_logger(*tags) do
|
90
161
|
logger.#{level}(progname, &block)
|
91
162
|
end
|
92
163
|
end
|
93
164
|
METHOD
|
94
165
|
end
|
95
|
-
|
96
|
-
def info_and_stdout(progname = nil, tags: [], &block)
|
97
|
-
unless ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
|
98
|
-
tags_string = (['GoodJob'] + tags).map { |t| "[#{t}]" }.join(' ')
|
99
|
-
stdout_message = "#{tags_string} #{yield}"
|
100
|
-
$stdout.puts stdout_message
|
101
|
-
end
|
102
|
-
|
103
|
-
info(progname, tags: [], &block)
|
104
|
-
end
|
105
|
-
|
106
|
-
def thread_name
|
107
|
-
Thread.current.name || Thread.current.object_id
|
108
|
-
end
|
109
166
|
end
|
110
167
|
end
|
@@ -18,8 +18,17 @@ module GoodJob
|
|
18
18
|
schedulers.each { |s| s.restart(wait: wait) }
|
19
19
|
end
|
20
20
|
|
21
|
-
def create_thread
|
22
|
-
|
21
|
+
def create_thread(state = nil)
|
22
|
+
results = []
|
23
|
+
any_true = schedulers.any? do |scheduler|
|
24
|
+
scheduler.create_thread(state).tap { |result| results << result }
|
25
|
+
end
|
26
|
+
|
27
|
+
if any_true
|
28
|
+
true
|
29
|
+
else
|
30
|
+
results.any? { |result| result == false } ? false : nil
|
31
|
+
end
|
23
32
|
end
|
24
33
|
end
|
25
34
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'concurrent/atomic/atomic_boolean'
|
2
|
+
|
3
|
+
module GoodJob # :nodoc:
|
4
|
+
#
|
5
|
+
# Wrapper for Postgres LISTEN/NOTIFY
|
6
|
+
#
|
7
|
+
class Notifier
|
8
|
+
CHANNEL = 'good_job'.freeze
|
9
|
+
POOL_OPTIONS = {
|
10
|
+
min_threads: 0,
|
11
|
+
max_threads: 1,
|
12
|
+
auto_terminate: true,
|
13
|
+
idletime: 60,
|
14
|
+
max_queue: 1,
|
15
|
+
fallback_policy: :discard,
|
16
|
+
}.freeze
|
17
|
+
WAIT_INTERVAL = 1
|
18
|
+
|
19
|
+
# @!attribute [r] instances
|
20
|
+
# @!scope class
|
21
|
+
# @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
|
22
|
+
cattr_reader :instances, default: [], instance_reader: false
|
23
|
+
|
24
|
+
def self.notify(message)
|
25
|
+
connection = ActiveRecord::Base.connection
|
26
|
+
connection.exec_query <<~SQL
|
27
|
+
NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :recipients
|
32
|
+
|
33
|
+
def initialize(*recipients)
|
34
|
+
@recipients = Concurrent::Array.new(recipients)
|
35
|
+
@listening = Concurrent::AtomicBoolean.new(false)
|
36
|
+
|
37
|
+
self.class.instances << self
|
38
|
+
|
39
|
+
create_pool
|
40
|
+
listen
|
41
|
+
end
|
42
|
+
|
43
|
+
def listening?
|
44
|
+
@listening.true?
|
45
|
+
end
|
46
|
+
|
47
|
+
def restart(wait: true)
|
48
|
+
shutdown(wait: wait)
|
49
|
+
create_pool
|
50
|
+
listen
|
51
|
+
end
|
52
|
+
|
53
|
+
def shutdown(wait: true)
|
54
|
+
return unless @pool.running?
|
55
|
+
|
56
|
+
@pool.shutdown
|
57
|
+
@pool.wait_for_termination if wait
|
58
|
+
end
|
59
|
+
|
60
|
+
def shutdown?
|
61
|
+
!@pool.running?
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def create_pool
|
67
|
+
@pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
|
68
|
+
end
|
69
|
+
|
70
|
+
def listen
|
71
|
+
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
|
77
|
+
|
78
|
+
begin
|
79
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
80
|
+
while pool.running?
|
81
|
+
listening.make_true
|
82
|
+
conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
|
83
|
+
listening.make_false
|
84
|
+
next unless channel == CHANNEL
|
85
|
+
|
86
|
+
ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
|
87
|
+
parsed_payload = JSON.parse(payload, symbolize_names: true)
|
88
|
+
recipients.each do |recipient|
|
89
|
+
target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
|
90
|
+
target.send(method_name, parsed_payload)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
listening.make_false
|
94
|
+
end
|
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
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
future.add_observer(self, :listen_observer)
|
109
|
+
future.execute
|
110
|
+
end
|
111
|
+
|
112
|
+
def listen_observer(_time, _result, _thread_error)
|
113
|
+
listen unless shutdown?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/good_job/performer.rb
CHANGED
@@ -2,14 +2,21 @@ module GoodJob
|
|
2
2
|
class Performer
|
3
3
|
attr_reader :name
|
4
4
|
|
5
|
-
def initialize(target, method_name, name: nil)
|
5
|
+
def initialize(target, method_name, name: nil, filter: nil)
|
6
6
|
@target = target
|
7
7
|
@method_name = method_name
|
8
8
|
@name = name
|
9
|
+
@filter = filter
|
9
10
|
end
|
10
11
|
|
11
12
|
def next
|
12
13
|
@target.public_send(@method_name)
|
13
14
|
end
|
15
|
+
|
16
|
+
def next?(state = {})
|
17
|
+
return true unless @filter.respond_to?(:call)
|
18
|
+
|
19
|
+
@filter.call(state)
|
20
|
+
end
|
14
21
|
end
|
15
22
|
end
|
data/lib/good_job/railtie.rb
CHANGED
@@ -4,5 +4,15 @@ module GoodJob
|
|
4
4
|
ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
|
5
5
|
GoodJob::LogSubscriber.attach_to :good_job
|
6
6
|
end
|
7
|
+
|
8
|
+
initializer "good_job.active_job_notifications" do
|
9
|
+
ActiveSupport::Notifications.subscribe "enqueue_retry.active_job" do |event|
|
10
|
+
GoodJob::CurrentExecution.error_on_retry = event.payload[:error]
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveSupport::Notifications.subscribe "discard.active_job" do |event|
|
14
|
+
GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
|
15
|
+
end
|
16
|
+
end
|
7
17
|
end
|
8
18
|
end
|
data/lib/good_job/scheduler.rb
CHANGED
@@ -2,16 +2,25 @@ require "concurrent/executor/thread_pool_executor"
|
|
2
2
|
require "concurrent/timer_task"
|
3
3
|
require "concurrent/utility/processor_counter"
|
4
4
|
|
5
|
-
module GoodJob
|
5
|
+
module GoodJob # :nodoc:
|
6
|
+
#
|
7
|
+
# Schedulers are generic thread execution pools that are responsible for
|
8
|
+
# periodically checking for available execution tasks, executing tasks in a
|
9
|
+
# bounded thread-pool, and efficiently scaling execution threads.
|
10
|
+
#
|
11
|
+
# Schedulers are "generic" in the sense that they delegate task execution
|
12
|
+
# details to a "Performer" object that responds to #next.
|
13
|
+
#
|
6
14
|
class Scheduler
|
15
|
+
# Defaults for instance of Concurrent::TimerTask
|
7
16
|
DEFAULT_TIMER_OPTIONS = {
|
8
17
|
execution_interval: 1,
|
9
18
|
timeout_interval: 1,
|
10
19
|
run_now: true,
|
11
20
|
}.freeze
|
12
21
|
|
22
|
+
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
13
23
|
DEFAULT_POOL_OPTIONS = {
|
14
|
-
name: 'good_job',
|
15
24
|
min_threads: 0,
|
16
25
|
max_threads: Concurrent.processor_count,
|
17
26
|
auto_terminate: true,
|
@@ -20,18 +29,35 @@ module GoodJob
|
|
20
29
|
fallback_policy: :discard,
|
21
30
|
}.freeze
|
22
31
|
|
32
|
+
# @!attribute [r] instances
|
33
|
+
# @!scope class
|
34
|
+
# All instantiated Schedulers in the current process.
|
35
|
+
# @return [array<GoodJob:Scheduler>]
|
23
36
|
cattr_reader :instances, default: [], instance_reader: false
|
24
37
|
|
38
|
+
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
39
|
+
# @param configuration [GoodJob::Configuration]
|
40
|
+
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
25
41
|
def self.from_configuration(configuration)
|
26
42
|
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
27
43
|
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
28
44
|
max_threads = (max_threads || configuration.max_threads).to_i
|
29
45
|
|
30
46
|
job_query = GoodJob::Job.queue_string(queue_string)
|
31
|
-
|
47
|
+
parsed = GoodJob::Job.queue_parser(queue_string)
|
48
|
+
job_filter = proc do |state|
|
49
|
+
if parsed[:exclude]
|
50
|
+
!parsed[:exclude].include? state[:queue_name]
|
51
|
+
elsif parsed[:include]
|
52
|
+
parsed[:include].include? state[:queue_name]
|
53
|
+
else
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
|
32
58
|
|
33
59
|
timer_options = {}
|
34
|
-
timer_options[:execution_interval] = configuration.poll_interval
|
60
|
+
timer_options[:execution_interval] = configuration.poll_interval
|
35
61
|
|
36
62
|
pool_options = {
|
37
63
|
max_threads: max_threads,
|
@@ -47,6 +73,9 @@ module GoodJob
|
|
47
73
|
end
|
48
74
|
end
|
49
75
|
|
76
|
+
# @param performer [GoodJob::Performer]
|
77
|
+
# @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
|
78
|
+
# @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
|
50
79
|
def initialize(performer, timer_options: {}, pool_options: {})
|
51
80
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
52
81
|
|
@@ -56,14 +85,19 @@ module GoodJob
|
|
56
85
|
@pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
|
57
86
|
@timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
|
58
87
|
|
88
|
+
@pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
|
89
|
+
|
59
90
|
create_pools
|
60
91
|
end
|
61
92
|
|
93
|
+
# Shut down the Scheduler.
|
94
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
95
|
+
# @return [void]
|
62
96
|
def shutdown(wait: true)
|
63
97
|
@_shutdown = true
|
64
98
|
|
65
|
-
|
66
|
-
|
99
|
+
instrument("scheduler_shutdown_start", { wait: wait })
|
100
|
+
instrument("scheduler_shutdown", { wait: wait }) do
|
67
101
|
if @timer&.running?
|
68
102
|
@timer.shutdown
|
69
103
|
@timer.wait_for_termination if wait
|
@@ -76,19 +110,32 @@ module GoodJob
|
|
76
110
|
end
|
77
111
|
end
|
78
112
|
|
113
|
+
# True when the Scheduler is shutdown.
|
114
|
+
# @return [true, false, nil]
|
79
115
|
def shutdown?
|
80
116
|
@_shutdown
|
81
117
|
end
|
82
118
|
|
119
|
+
# Restart the Scheduler. When shutdown, start; or shutdown and start.
|
120
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
121
|
+
# @return [void]
|
83
122
|
def restart(wait: true)
|
84
|
-
|
123
|
+
instrument("scheduler_restart_pools") do
|
85
124
|
shutdown(wait: wait) unless shutdown?
|
86
125
|
create_pools
|
126
|
+
@_shutdown = false
|
87
127
|
end
|
88
128
|
end
|
89
129
|
|
90
|
-
|
91
|
-
|
130
|
+
# Triggers a Performer execution, if an execution thread is available.
|
131
|
+
# @param state [nil, Object] Allows Performer#next? to accept or reject the execution
|
132
|
+
# @return [nil, Boolean] if the thread was created
|
133
|
+
def create_thread(state = nil)
|
134
|
+
return nil unless @pool.running? && @pool.ready_worker_count.positive?
|
135
|
+
|
136
|
+
if state
|
137
|
+
return false unless @performer.next?(state)
|
138
|
+
end
|
92
139
|
|
93
140
|
future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
|
94
141
|
output = nil
|
@@ -97,35 +144,32 @@ module GoodJob
|
|
97
144
|
end
|
98
145
|
future.add_observer(self, :task_observer)
|
99
146
|
future.execute
|
147
|
+
|
148
|
+
true
|
100
149
|
end
|
101
150
|
|
151
|
+
# Invoked on completion of TimerTask task.
|
152
|
+
# @!visibility private
|
153
|
+
# @return [void]
|
102
154
|
def timer_observer(time, executed_task, thread_error)
|
103
155
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
104
|
-
|
156
|
+
instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
|
105
157
|
end
|
106
158
|
|
159
|
+
# Invoked on completion of ThreadPoolExecutor task
|
160
|
+
# @!visibility private
|
161
|
+
# @return [void]
|
107
162
|
def task_observer(time, output, thread_error)
|
108
163
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
109
|
-
|
164
|
+
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
110
165
|
create_thread if output
|
111
166
|
end
|
112
167
|
|
113
|
-
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
114
|
-
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
115
|
-
def ready_worker_count
|
116
|
-
synchronize do
|
117
|
-
workers_still_to_be_created = @max_length - @pool.length
|
118
|
-
workers_created_but_waiting = @ready.length
|
119
|
-
|
120
|
-
workers_still_to_be_created + workers_created_but_waiting
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
168
|
private
|
126
169
|
|
170
|
+
# @return [void]
|
127
171
|
def create_pools
|
128
|
-
|
172
|
+
instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
|
129
173
|
@pool = ThreadPoolExecutor.new(@pool_options)
|
130
174
|
next unless @timer_options[:execution_interval].positive?
|
131
175
|
|
@@ -135,12 +179,29 @@ module GoodJob
|
|
135
179
|
end
|
136
180
|
end
|
137
181
|
|
138
|
-
def
|
139
|
-
|
182
|
+
def instrument(name, payload = {}, &block)
|
183
|
+
payload = payload.reverse_merge({
|
184
|
+
scheduler: self,
|
185
|
+
process_id: GoodJob::CurrentExecution.process_id,
|
186
|
+
thread_name: GoodJob::CurrentExecution.thread_name,
|
187
|
+
})
|
188
|
+
|
189
|
+
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
140
190
|
end
|
191
|
+
end
|
141
192
|
|
142
|
-
|
143
|
-
|
193
|
+
# Slightly customized sub-class of Concurrent::ThreadPoolExecutor
|
194
|
+
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
195
|
+
# Number of idle or potential threads available to execute tasks
|
196
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
197
|
+
# @return [Integer]
|
198
|
+
def ready_worker_count
|
199
|
+
synchronize do
|
200
|
+
workers_still_to_be_created = @max_length - @pool.length
|
201
|
+
workers_created_but_waiting = @ready.length
|
202
|
+
|
203
|
+
workers_still_to_be_created + workers_created_but_waiting
|
204
|
+
end
|
144
205
|
end
|
145
206
|
end
|
146
207
|
end
|
data/lib/good_job/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.1
|
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-
|
11
|
+
date: 2020-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -151,7 +151,7 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name: pry
|
154
|
+
name: pry-rails
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|
@@ -178,6 +178,20 @@ dependencies:
|
|
178
178
|
- - ">="
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rbtrace
|
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'
|
181
195
|
- !ruby/object:Gem::Dependency
|
182
196
|
name: rspec-rails
|
183
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -248,6 +262,34 @@ dependencies:
|
|
248
262
|
- - ">="
|
249
263
|
- !ruby/object:Gem::Version
|
250
264
|
version: '0'
|
265
|
+
- !ruby/object:Gem::Dependency
|
266
|
+
name: sigdump
|
267
|
+
requirement: !ruby/object:Gem::Requirement
|
268
|
+
requirements:
|
269
|
+
- - ">="
|
270
|
+
- !ruby/object:Gem::Version
|
271
|
+
version: '0'
|
272
|
+
type: :development
|
273
|
+
prerelease: false
|
274
|
+
version_requirements: !ruby/object:Gem::Requirement
|
275
|
+
requirements:
|
276
|
+
- - ">="
|
277
|
+
- !ruby/object:Gem::Version
|
278
|
+
version: '0'
|
279
|
+
- !ruby/object:Gem::Dependency
|
280
|
+
name: yard
|
281
|
+
requirement: !ruby/object:Gem::Requirement
|
282
|
+
requirements:
|
283
|
+
- - ">="
|
284
|
+
- !ruby/object:Gem::Version
|
285
|
+
version: '0'
|
286
|
+
type: :development
|
287
|
+
prerelease: false
|
288
|
+
version_requirements: !ruby/object:Gem::Requirement
|
289
|
+
requirements:
|
290
|
+
- - ">="
|
291
|
+
- !ruby/object:Gem::Version
|
292
|
+
version: '0'
|
251
293
|
description: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
252
294
|
email:
|
253
295
|
- bensheldon@gmail.com
|
@@ -270,10 +312,12 @@ files:
|
|
270
312
|
- lib/good_job/adapter.rb
|
271
313
|
- lib/good_job/cli.rb
|
272
314
|
- lib/good_job/configuration.rb
|
315
|
+
- lib/good_job/current_execution.rb
|
273
316
|
- lib/good_job/job.rb
|
274
317
|
- lib/good_job/lockable.rb
|
275
318
|
- lib/good_job/log_subscriber.rb
|
276
319
|
- lib/good_job/multi_scheduler.rb
|
320
|
+
- lib/good_job/notifier.rb
|
277
321
|
- lib/good_job/performer.rb
|
278
322
|
- lib/good_job/pg_locks.rb
|
279
323
|
- lib/good_job/railtie.rb
|