good_job 1.1.2 → 1.2.2
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 +80 -0
- data/README.md +22 -4
- data/exe/good_job +1 -0
- data/lib/good_job.rb +51 -1
- data/lib/good_job/adapter.rb +18 -12
- data/lib/good_job/cli.rb +11 -3
- data/lib/good_job/current_execution.rb +37 -0
- data/lib/good_job/job.rb +37 -18
- data/lib/good_job/lockable.rb +8 -10
- data/lib/good_job/log_subscriber.rb +84 -27
- data/lib/good_job/multi_scheduler.rb +11 -2
- data/lib/good_job/notifier.rb +127 -0
- data/lib/good_job/performer.rb +8 -1
- data/lib/good_job/railtie.rb +10 -0
- data/lib/good_job/scheduler.rb +90 -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: b5125c48d26036d60649d0448f064aebcbe95fd90ac5e2eeb22e504761f7cdd1
|
4
|
+
data.tar.gz: f6cd3d0bcbfbbc639d0724557c7dbad61e9aef717545916b57a11c4413dcfe3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eeb551e271b3a47aa903d64e566a010c87a632fd064835928d4986342405f458e02c5f88451c034ee2c73225274d54583ed963431e08e80232bcd3d87f76d500
|
7
|
+
data.tar.gz: f0c70c47ad3279bb41440ed27408511bdebc064524cf0933db066a0420bae829b1145b7713bc644febe0bc21e3010334f1c641863674110df878dd4245d17b7a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,84 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.2.2](https://github.com/bensheldon/good_job/tree/v1.2.2) (2020-08-26)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.1...v1.2.2)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Run Github Action tests against Ruby 2.5, 2.6, 2.7 [\#100](https://github.com/bensheldon/good_job/issues/100)
|
10
|
+
|
11
|
+
**Fixed bugs:**
|
12
|
+
|
13
|
+
- Freezes puma on code change [\#95](https://github.com/bensheldon/good_job/issues/95)
|
14
|
+
- Ruby 2.7 keyword arguments warning [\#93](https://github.com/bensheldon/good_job/issues/93)
|
15
|
+
|
16
|
+
**Closed issues:**
|
17
|
+
|
18
|
+
- Add test for `rails g good\_job:install` [\#57](https://github.com/bensheldon/good_job/issues/57)
|
19
|
+
|
20
|
+
**Merged pull requests:**
|
21
|
+
|
22
|
+
- Use more ActiveRecord in Lockable and not connection.execute [\#102](https://github.com/bensheldon/good_job/pull/102) ([bensheldon](https://github.com/bensheldon))
|
23
|
+
- 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))
|
24
|
+
- Return to using executor.wrap around Scheduler execution task [\#99](https://github.com/bensheldon/good_job/pull/99) ([bensheldon](https://github.com/bensheldon))
|
25
|
+
- Fix Ruby 2.7 keyword arguments warning [\#98](https://github.com/bensheldon/good_job/pull/98) ([arku](https://github.com/arku))
|
26
|
+
- Remove executor/reloader for less interlocking [\#97](https://github.com/bensheldon/good_job/pull/97) ([sj26](https://github.com/sj26))
|
27
|
+
- Name the thread pools [\#96](https://github.com/bensheldon/good_job/pull/96) ([sj26](https://github.com/sj26))
|
28
|
+
- Add test for `rails g good\_job:install` [\#94](https://github.com/bensheldon/good_job/pull/94) ([arku](https://github.com/arku))
|
29
|
+
|
30
|
+
## [v1.2.1](https://github.com/bensheldon/good_job/tree/v1.2.1) (2020-08-21)
|
31
|
+
|
32
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.2.0...v1.2.1)
|
33
|
+
|
34
|
+
**Fixed bugs:**
|
35
|
+
|
36
|
+
- undefined method `thread\_mattr\_accessor' when not requiring the Sprockets Railstie [\#85](https://github.com/bensheldon/good_job/issues/85)
|
37
|
+
|
38
|
+
**Closed issues:**
|
39
|
+
|
40
|
+
- Document comparison of GoodJob with other backends [\#51](https://github.com/bensheldon/good_job/issues/51)
|
41
|
+
|
42
|
+
**Merged pull requests:**
|
43
|
+
|
44
|
+
- Explicitly require thread\_mattr\_accessor from ActiveSupport [\#86](https://github.com/bensheldon/good_job/pull/86) ([bensheldon](https://github.com/bensheldon))
|
45
|
+
- Add comparison of other backends to Readme [\#84](https://github.com/bensheldon/good_job/pull/84) ([bensheldon](https://github.com/bensheldon))
|
46
|
+
|
47
|
+
## [v1.2.0](https://github.com/bensheldon/good_job/tree/v1.2.0) (2020-08-20)
|
48
|
+
|
49
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.4...v1.2.0)
|
50
|
+
|
51
|
+
**Merged pull requests:**
|
52
|
+
|
53
|
+
- Document GoodJob module [\#83](https://github.com/bensheldon/good_job/pull/83) ([bensheldon](https://github.com/bensheldon))
|
54
|
+
|
55
|
+
## [v1.1.4](https://github.com/bensheldon/good_job/tree/v1.1.4) (2020-08-19)
|
56
|
+
|
57
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.3...v1.1.4)
|
58
|
+
|
59
|
+
**Implemented enhancements:**
|
60
|
+
|
61
|
+
- Explicitly name threads for easier debugging [\#64](https://github.com/bensheldon/good_job/issues/64)
|
62
|
+
- Investigate Listen/Notify as alternative to polling [\#54](https://github.com/bensheldon/good_job/issues/54)
|
63
|
+
|
64
|
+
**Merged pull requests:**
|
65
|
+
|
66
|
+
- Add Postgres LISTEN/NOTIFY support [\#82](https://github.com/bensheldon/good_job/pull/82) ([bensheldon](https://github.com/bensheldon))
|
67
|
+
- 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))
|
68
|
+
- Fully name scheduler threadpools and thread names; refactor CLI STDOUT [\#80](https://github.com/bensheldon/good_job/pull/80) ([bensheldon](https://github.com/bensheldon))
|
69
|
+
|
70
|
+
## [v1.1.3](https://github.com/bensheldon/good_job/tree/v1.1.3) (2020-08-14)
|
71
|
+
|
72
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.2...v1.1.3)
|
73
|
+
|
74
|
+
**Fixed bugs:**
|
75
|
+
|
76
|
+
- Job exceptions not properly attached to good\_jobs record [\#72](https://github.com/bensheldon/good_job/issues/72)
|
77
|
+
|
78
|
+
**Merged pull requests:**
|
79
|
+
|
80
|
+
- Document GoodJob::Scheduler with Yard [\#78](https://github.com/bensheldon/good_job/pull/78) ([bensheldon](https://github.com/bensheldon))
|
81
|
+
|
3
82
|
## [v1.1.2](https://github.com/bensheldon/good_job/tree/v1.1.2) (2020-08-13)
|
4
83
|
|
5
84
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.1.1...v1.1.2)
|
@@ -22,6 +101,7 @@
|
|
22
101
|
|
23
102
|
**Merged pull requests:**
|
24
103
|
|
104
|
+
- Capture errors via instrumentation from retry\_on and discard\_on [\#79](https://github.com/bensheldon/good_job/pull/79) ([bensheldon](https://github.com/bensheldon))
|
25
105
|
- Allow instantiation of multiple schedulers via --queues [\#76](https://github.com/bensheldon/good_job/pull/76) ([bensheldon](https://github.com/bensheldon))
|
26
106
|
- Extract options parsing to Configuration object [\#74](https://github.com/bensheldon/good_job/pull/74) ([bensheldon](https://github.com/bensheldon))
|
27
107
|
|
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:
|
@@ -284,15 +297,20 @@ Depending on your application configuration, you may need to take additional ste
|
|
284
297
|
# config/puma.rb
|
285
298
|
|
286
299
|
before_fork do
|
287
|
-
GoodJob
|
300
|
+
GoodJob.shutdown
|
288
301
|
end
|
289
302
|
|
290
303
|
on_worker_boot do
|
291
|
-
GoodJob
|
304
|
+
GoodJob.restart
|
292
305
|
end
|
293
306
|
|
294
307
|
on_worker_shutdown do
|
295
|
-
GoodJob
|
308
|
+
GoodJob.shutdown
|
309
|
+
end
|
310
|
+
|
311
|
+
MAIN_PID = Process.pid
|
312
|
+
at_exit do
|
313
|
+
GoodJob.shutdown if Process.pid == MAIN_PID
|
296
314
|
end
|
297
315
|
```
|
298
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,29 @@ 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
|
-
|
11
|
+
configuration = GoodJob::Configuration.new(
|
12
|
+
{
|
13
|
+
execution_mode: execution_mode,
|
14
|
+
queues: queues,
|
15
|
+
max_threads: max_threads,
|
16
|
+
poll_interval: poll_interval,
|
17
|
+
}
|
18
|
+
)
|
19
19
|
|
20
20
|
@execution_mode = configuration.execution_mode
|
21
|
+
raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(@execution_mode)
|
21
22
|
|
22
|
-
@
|
23
|
-
|
23
|
+
if @execution_mode == :async # rubocop:disable Style/GuardClause
|
24
|
+
@notifier = notifier || GoodJob::Notifier.new
|
25
|
+
@scheduler = scheduler || GoodJob::Scheduler.from_configuration(configuration)
|
26
|
+
@notifier.recipients << [@scheduler, :create_thread]
|
27
|
+
end
|
24
28
|
end
|
25
29
|
|
26
30
|
def enqueue(active_job)
|
@@ -42,12 +46,14 @@ module GoodJob
|
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
|
-
@scheduler.create_thread
|
49
|
+
executed_locally = execute_async? && @scheduler.create_thread(queue_name: good_job.queue_name)
|
50
|
+
Notifier.notify(queue_name: good_job.queue_name) unless executed_locally
|
46
51
|
|
47
52
|
good_job
|
48
53
|
end
|
49
54
|
|
50
55
|
def shutdown(wait: true)
|
56
|
+
@notifier&.shutdown(wait: wait)
|
51
57
|
@scheduler&.shutdown(wait: wait)
|
52
58
|
end
|
53
59
|
|
data/lib/good_job/cli.rb
CHANGED
@@ -18,8 +18,10 @@ module GoodJob
|
|
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,20 +40,14 @@ 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
|
-
|
26
|
-
if
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
return if queue_names_without_all.size.zero?
|
33
|
-
|
34
|
-
if exclude_queues
|
35
|
-
where.not(queue_name: queue_names_without_all).or where(queue_name: nil)
|
36
|
-
else
|
37
|
-
where(queue_name: queue_names_without_all)
|
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])
|
38
51
|
end
|
39
52
|
end)
|
40
53
|
|
@@ -45,7 +58,8 @@ module GoodJob
|
|
45
58
|
|
46
59
|
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
47
60
|
good_job = good_jobs.first
|
48
|
-
|
61
|
+
# TODO: Determine why some records are fetched without an advisory lock at all
|
62
|
+
break unless good_job&.owns_advisory_lock?
|
49
63
|
|
50
64
|
result, error = good_job.perform
|
51
65
|
end
|
@@ -60,7 +74,7 @@ module GoodJob
|
|
60
74
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
61
75
|
priority: active_job.priority || DEFAULT_PRIORITY,
|
62
76
|
serialized_params: active_job.serialize,
|
63
|
-
scheduled_at: scheduled_at
|
77
|
+
scheduled_at: scheduled_at,
|
64
78
|
create_with_advisory_lock: create_with_advisory_lock
|
65
79
|
)
|
66
80
|
|
@@ -76,11 +90,11 @@ module GoodJob
|
|
76
90
|
def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
|
77
91
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
78
92
|
|
93
|
+
GoodJob::CurrentExecution.reset
|
79
94
|
result = nil
|
80
95
|
rescued_error = nil
|
81
96
|
error = nil
|
82
97
|
|
83
|
-
ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
|
84
98
|
self.performed_at = Time.current
|
85
99
|
save! unless destroy_after
|
86
100
|
|
@@ -89,18 +103,23 @@ module GoodJob
|
|
89
103
|
)
|
90
104
|
|
91
105
|
begin
|
92
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
|
106
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
93
107
|
result = ActiveJob::Base.execute(params)
|
94
108
|
end
|
95
109
|
rescue StandardError => e
|
96
110
|
rescued_error = e
|
97
111
|
end
|
98
112
|
|
113
|
+
retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
|
114
|
+
GoodJob::CurrentExecution.error_on_discard
|
115
|
+
|
99
116
|
if rescued_error
|
100
117
|
error = rescued_error
|
101
118
|
elsif result.is_a?(Exception)
|
102
119
|
error = result
|
103
120
|
result = nil
|
121
|
+
elsif retry_or_discard_error
|
122
|
+
error = retry_or_discard_error
|
104
123
|
end
|
105
124
|
|
106
125
|
error_message = "#{error.class}: #{error.message}" if error
|
data/lib/good_job/lockable.rb
CHANGED
@@ -55,19 +55,17 @@ module GoodJob
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def advisory_lock
|
58
|
-
|
59
|
-
|
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.
|
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
|
-
|
67
|
-
|
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.
|
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)).
|
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)).
|
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!
|
@@ -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,127 @@
|
|
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
|
+
name: name,
|
11
|
+
min_threads: 0,
|
12
|
+
max_threads: 1,
|
13
|
+
auto_terminate: true,
|
14
|
+
idletime: 60,
|
15
|
+
max_queue: 1,
|
16
|
+
fallback_policy: :discard,
|
17
|
+
}.freeze
|
18
|
+
WAIT_INTERVAL = 1
|
19
|
+
|
20
|
+
# @!attribute [r] instances
|
21
|
+
# @!scope class
|
22
|
+
# @return [array<GoodJob:Adapter>] the instances of +GoodJob::Notifier+
|
23
|
+
cattr_reader :instances, default: [], instance_reader: false
|
24
|
+
|
25
|
+
def self.notify(message)
|
26
|
+
connection = ActiveRecord::Base.connection
|
27
|
+
connection.exec_query <<~SQL
|
28
|
+
NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
|
29
|
+
SQL
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :recipients
|
33
|
+
|
34
|
+
def initialize(*recipients)
|
35
|
+
@recipients = Concurrent::Array.new(recipients)
|
36
|
+
@listening = Concurrent::AtomicBoolean.new(false)
|
37
|
+
|
38
|
+
self.class.instances << self
|
39
|
+
|
40
|
+
create_pool
|
41
|
+
listen
|
42
|
+
end
|
43
|
+
|
44
|
+
def listening?
|
45
|
+
@listening.true?
|
46
|
+
end
|
47
|
+
|
48
|
+
def restart(wait: true)
|
49
|
+
shutdown(wait: wait)
|
50
|
+
create_pool
|
51
|
+
listen
|
52
|
+
end
|
53
|
+
|
54
|
+
def shutdown(wait: true)
|
55
|
+
return unless @pool.running?
|
56
|
+
|
57
|
+
@pool.shutdown
|
58
|
+
@pool.wait_for_termination if wait
|
59
|
+
end
|
60
|
+
|
61
|
+
def shutdown?
|
62
|
+
!@pool.running?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def create_pool
|
68
|
+
@pool = Concurrent::ThreadPoolExecutor.new(POOL_OPTIONS)
|
69
|
+
end
|
70
|
+
|
71
|
+
def listen
|
72
|
+
future = Concurrent::Future.new(args: [@recipients, @pool, @listening], executor: @pool) do |recipients, pool, listening|
|
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
|
78
|
+
|
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
|
+
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
|
+
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
|
+
|
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
|
126
|
+
end
|
127
|
+
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,26 @@ 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:
|
24
|
+
name: name,
|
15
25
|
min_threads: 0,
|
16
26
|
max_threads: Concurrent.processor_count,
|
17
27
|
auto_terminate: true,
|
@@ -20,18 +30,35 @@ module GoodJob
|
|
20
30
|
fallback_policy: :discard,
|
21
31
|
}.freeze
|
22
32
|
|
33
|
+
# @!attribute [r] instances
|
34
|
+
# @!scope class
|
35
|
+
# All instantiated Schedulers in the current process.
|
36
|
+
# @return [array<GoodJob:Scheduler>]
|
23
37
|
cattr_reader :instances, default: [], instance_reader: false
|
24
38
|
|
39
|
+
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
40
|
+
# @param configuration [GoodJob::Configuration]
|
41
|
+
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
25
42
|
def self.from_configuration(configuration)
|
26
43
|
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
27
44
|
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
28
45
|
max_threads = (max_threads || configuration.max_threads).to_i
|
29
46
|
|
30
47
|
job_query = GoodJob::Job.queue_string(queue_string)
|
31
|
-
|
48
|
+
parsed = GoodJob::Job.queue_parser(queue_string)
|
49
|
+
job_filter = proc do |state|
|
50
|
+
if parsed[:exclude]
|
51
|
+
!parsed[:exclude].include? state[:queue_name]
|
52
|
+
elsif parsed[:include]
|
53
|
+
parsed[:include].include? state[:queue_name]
|
54
|
+
else
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
|
32
59
|
|
33
60
|
timer_options = {}
|
34
|
-
timer_options[:execution_interval] = configuration.poll_interval
|
61
|
+
timer_options[:execution_interval] = configuration.poll_interval
|
35
62
|
|
36
63
|
pool_options = {
|
37
64
|
max_threads: max_threads,
|
@@ -47,6 +74,9 @@ module GoodJob
|
|
47
74
|
end
|
48
75
|
end
|
49
76
|
|
77
|
+
# @param performer [GoodJob::Performer]
|
78
|
+
# @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
|
79
|
+
# @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
|
50
80
|
def initialize(performer, timer_options: {}, pool_options: {})
|
51
81
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
52
82
|
|
@@ -56,14 +86,19 @@ module GoodJob
|
|
56
86
|
@pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
|
57
87
|
@timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
|
58
88
|
|
89
|
+
@pool_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@pool_options[:max_threads]} poll_interval=#{@timer_options[:execution_interval]})"
|
90
|
+
|
59
91
|
create_pools
|
60
92
|
end
|
61
93
|
|
94
|
+
# Shut down the Scheduler.
|
95
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
96
|
+
# @return [void]
|
62
97
|
def shutdown(wait: true)
|
63
98
|
@_shutdown = true
|
64
99
|
|
65
|
-
|
66
|
-
|
100
|
+
instrument("scheduler_shutdown_start", { wait: wait })
|
101
|
+
instrument("scheduler_shutdown", { wait: wait }) do
|
67
102
|
if @timer&.running?
|
68
103
|
@timer.shutdown
|
69
104
|
@timer.wait_for_termination if wait
|
@@ -76,19 +111,32 @@ module GoodJob
|
|
76
111
|
end
|
77
112
|
end
|
78
113
|
|
114
|
+
# True when the Scheduler is shutdown.
|
115
|
+
# @return [true, false, nil]
|
79
116
|
def shutdown?
|
80
117
|
@_shutdown
|
81
118
|
end
|
82
119
|
|
120
|
+
# Restart the Scheduler. When shutdown, start; or shutdown and start.
|
121
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
122
|
+
# @return [void]
|
83
123
|
def restart(wait: true)
|
84
|
-
|
124
|
+
instrument("scheduler_restart_pools") do
|
85
125
|
shutdown(wait: wait) unless shutdown?
|
86
126
|
create_pools
|
127
|
+
@_shutdown = false
|
87
128
|
end
|
88
129
|
end
|
89
130
|
|
90
|
-
|
91
|
-
|
131
|
+
# Triggers a Performer execution, if an execution thread is available.
|
132
|
+
# @param state [nil, Object] Allows Performer#next? to accept or reject the execution
|
133
|
+
# @return [nil, Boolean] if the thread was created
|
134
|
+
def create_thread(state = nil)
|
135
|
+
return nil unless @pool.running? && @pool.ready_worker_count.positive?
|
136
|
+
|
137
|
+
if state
|
138
|
+
return false unless @performer.next?(state)
|
139
|
+
end
|
92
140
|
|
93
141
|
future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
|
94
142
|
output = nil
|
@@ -97,35 +145,32 @@ module GoodJob
|
|
97
145
|
end
|
98
146
|
future.add_observer(self, :task_observer)
|
99
147
|
future.execute
|
148
|
+
|
149
|
+
true
|
100
150
|
end
|
101
151
|
|
152
|
+
# Invoked on completion of TimerTask task.
|
153
|
+
# @!visibility private
|
154
|
+
# @return [void]
|
102
155
|
def timer_observer(time, executed_task, thread_error)
|
103
156
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
104
|
-
|
157
|
+
instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
|
105
158
|
end
|
106
159
|
|
160
|
+
# Invoked on completion of ThreadPoolExecutor task
|
161
|
+
# @!visibility private
|
162
|
+
# @return [void]
|
107
163
|
def task_observer(time, output, thread_error)
|
108
164
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
109
|
-
|
165
|
+
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
110
166
|
create_thread if output
|
111
167
|
end
|
112
168
|
|
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
169
|
private
|
126
170
|
|
171
|
+
# @return [void]
|
127
172
|
def create_pools
|
128
|
-
|
173
|
+
instrument("scheduler_create_pools", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval] }) do
|
129
174
|
@pool = ThreadPoolExecutor.new(@pool_options)
|
130
175
|
next unless @timer_options[:execution_interval].positive?
|
131
176
|
|
@@ -135,12 +180,29 @@ module GoodJob
|
|
135
180
|
end
|
136
181
|
end
|
137
182
|
|
138
|
-
def
|
139
|
-
|
183
|
+
def instrument(name, payload = {}, &block)
|
184
|
+
payload = payload.reverse_merge({
|
185
|
+
scheduler: self,
|
186
|
+
process_id: GoodJob::CurrentExecution.process_id,
|
187
|
+
thread_name: GoodJob::CurrentExecution.thread_name,
|
188
|
+
})
|
189
|
+
|
190
|
+
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
140
191
|
end
|
192
|
+
end
|
141
193
|
|
142
|
-
|
143
|
-
|
194
|
+
# Slightly customized sub-class of Concurrent::ThreadPoolExecutor
|
195
|
+
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
196
|
+
# Number of idle or potential threads available to execute tasks
|
197
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
198
|
+
# @return [Integer]
|
199
|
+
def ready_worker_count
|
200
|
+
synchronize do
|
201
|
+
workers_still_to_be_created = @max_length - @pool.length
|
202
|
+
workers_created_but_waiting = @ready.length
|
203
|
+
|
204
|
+
workers_still_to_be_created + workers_created_but_waiting
|
205
|
+
end
|
144
206
|
end
|
145
207
|
end
|
146
208
|
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.2
|
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-27 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
|