good_job 1.0.2 → 1.1.3
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 +75 -8
- data/README.md +227 -30
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -12
- data/lib/good_job.rb +7 -2
- data/lib/good_job/adapter.rb +30 -16
- data/lib/good_job/cli.rb +18 -48
- data/lib/good_job/configuration.rb +55 -0
- data/lib/good_job/current_execution.rb +25 -0
- data/lib/good_job/job.rb +49 -26
- data/lib/good_job/log_subscriber.rb +110 -0
- data/lib/good_job/multi_scheduler.rb +25 -0
- data/lib/good_job/performer.rb +4 -1
- data/lib/good_job/railtie.rb +11 -0
- data/lib/good_job/scheduler.rb +116 -21
- data/lib/good_job/version.rb +1 -1
- metadata +51 -6
- data/lib/good_job/logging.rb +0 -70
@@ -0,0 +1,25 @@
|
|
1
|
+
module GoodJob
|
2
|
+
class MultiScheduler
|
3
|
+
attr_reader :schedulers
|
4
|
+
|
5
|
+
def initialize(schedulers)
|
6
|
+
@schedulers = schedulers
|
7
|
+
end
|
8
|
+
|
9
|
+
def shutdown(wait: true)
|
10
|
+
schedulers.each { |s| s.shutdown(wait: wait) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def shutdown?
|
14
|
+
schedulers.all?(&:shutdown?)
|
15
|
+
end
|
16
|
+
|
17
|
+
def restart(wait: true)
|
18
|
+
schedulers.each { |s| s.restart(wait: wait) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_thread
|
22
|
+
schedulers.all?(&:create_thread)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/good_job/performer.rb
CHANGED
data/lib/good_job/railtie.rb
CHANGED
@@ -2,6 +2,17 @@ module GoodJob
|
|
2
2
|
class Railtie < ::Rails::Railtie
|
3
3
|
initializer "good_job.logger" do
|
4
4
|
ActiveSupport.on_load(:good_job) { self.logger = ::Rails.logger }
|
5
|
+
GoodJob::LogSubscriber.attach_to :good_job
|
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
|
5
16
|
end
|
6
17
|
end
|
7
18
|
end
|
data/lib/good_job/scheduler.rb
CHANGED
@@ -2,14 +2,22 @@ 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
|
+
# Schedulers are generic thread execution pools that are responsible for
|
7
|
+
# periodically checking for available execution tasks, executing tasks in a
|
8
|
+
# bounded thread-pool, and efficiently scaling execution threads.
|
9
|
+
#
|
10
|
+
# Schedulers are "generic" in the sense that they delegate task execution
|
11
|
+
# details to a "Performer" object that responds to #next.
|
6
12
|
class Scheduler
|
13
|
+
# Defaults for instance of Concurrent::TimerTask
|
7
14
|
DEFAULT_TIMER_OPTIONS = {
|
8
15
|
execution_interval: 1,
|
9
16
|
timeout_interval: 1,
|
10
17
|
run_now: true,
|
11
18
|
}.freeze
|
12
19
|
|
20
|
+
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
13
21
|
DEFAULT_POOL_OPTIONS = {
|
14
22
|
name: 'good_job',
|
15
23
|
min_threads: 0,
|
@@ -20,42 +28,93 @@ module GoodJob
|
|
20
28
|
fallback_policy: :discard,
|
21
29
|
}.freeze
|
22
30
|
|
31
|
+
# @!attribute [r] instances
|
32
|
+
# @!scope class
|
33
|
+
# All instantiated Schedulers in the current process.
|
34
|
+
# @return [array<GoodJob:Scheduler>]
|
35
|
+
cattr_reader :instances, default: [], instance_reader: false
|
36
|
+
|
37
|
+
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
38
|
+
# @param configuration [GoodJob::Configuration]
|
39
|
+
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
40
|
+
def self.from_configuration(configuration)
|
41
|
+
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
42
|
+
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
43
|
+
max_threads = (max_threads || configuration.max_threads).to_i
|
44
|
+
|
45
|
+
job_query = GoodJob::Job.queue_string(queue_string)
|
46
|
+
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string)
|
47
|
+
|
48
|
+
timer_options = {}
|
49
|
+
timer_options[:execution_interval] = configuration.poll_interval if configuration.poll_interval.positive?
|
50
|
+
|
51
|
+
pool_options = {
|
52
|
+
max_threads: max_threads,
|
53
|
+
}
|
54
|
+
|
55
|
+
GoodJob::Scheduler.new(job_performer, timer_options: timer_options, pool_options: pool_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
if schedulers.size > 1
|
59
|
+
GoodJob::MultiScheduler.new(schedulers)
|
60
|
+
else
|
61
|
+
schedulers.first
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param performer [GoodJob::Performer]
|
66
|
+
# @param timer_options [Hash] Options to instantiate a Concurrent::TimerTask
|
67
|
+
# @param pool_options [Hash] Options to instantiate a Concurrent::ThreadPoolExecutor
|
23
68
|
def initialize(performer, timer_options: {}, pool_options: {})
|
24
69
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
25
70
|
|
71
|
+
self.class.instances << self
|
72
|
+
|
26
73
|
@performer = performer
|
27
|
-
@
|
28
|
-
@
|
29
|
-
create_thread
|
30
|
-
end
|
31
|
-
@timer.add_observer(self, :timer_observer)
|
32
|
-
@timer.execute
|
33
|
-
end
|
74
|
+
@pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
|
75
|
+
@timer_options = DEFAULT_TIMER_OPTIONS.merge(timer_options)
|
34
76
|
|
35
|
-
|
77
|
+
create_pools
|
36
78
|
end
|
37
79
|
|
80
|
+
# Shut down the Scheduler.
|
81
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
82
|
+
# @return [void]
|
38
83
|
def shutdown(wait: true)
|
39
84
|
@_shutdown = true
|
40
85
|
|
41
|
-
ActiveSupport::Notifications.instrument("
|
42
|
-
ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait }) do
|
43
|
-
if @timer
|
86
|
+
ActiveSupport::Notifications.instrument("scheduler_shutdown_start.good_job", { wait: wait, process_id: process_id })
|
87
|
+
ActiveSupport::Notifications.instrument("scheduler_shutdown.good_job", { wait: wait, process_id: process_id }) do
|
88
|
+
if @timer&.running?
|
44
89
|
@timer.shutdown
|
45
90
|
@timer.wait_for_termination if wait
|
46
91
|
end
|
47
92
|
|
48
|
-
if @pool
|
93
|
+
if @pool&.running?
|
49
94
|
@pool.shutdown
|
50
95
|
@pool.wait_for_termination if wait
|
51
96
|
end
|
52
97
|
end
|
53
98
|
end
|
54
99
|
|
100
|
+
# True when the Scheduler is shutdown.
|
101
|
+
# @return [true, false, nil]
|
55
102
|
def shutdown?
|
56
103
|
@_shutdown
|
57
104
|
end
|
58
105
|
|
106
|
+
# Restart the Scheduler. When shutdown, start; or shutdown and start.
|
107
|
+
# @param wait [Boolean] Wait for actively executing jobs to finish
|
108
|
+
# @return [void]
|
109
|
+
def restart(wait: true)
|
110
|
+
ActiveSupport::Notifications.instrument("scheduler_restart_pools.good_job", { process_id: process_id }) do
|
111
|
+
shutdown(wait: wait) unless shutdown?
|
112
|
+
create_pools
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Triggers the execution the Performer, if an execution thread is available.
|
117
|
+
# @return [Boolean]
|
59
118
|
def create_thread
|
60
119
|
return false unless @pool.ready_worker_count.positive?
|
61
120
|
|
@@ -66,26 +125,62 @@ module GoodJob
|
|
66
125
|
end
|
67
126
|
future.add_observer(self, :task_observer)
|
68
127
|
future.execute
|
128
|
+
true
|
69
129
|
end
|
70
130
|
|
131
|
+
# Invoked on completion of TimerTask task.
|
132
|
+
# @!visibility private
|
133
|
+
# @return [void]
|
71
134
|
def timer_observer(time, executed_task, thread_error)
|
135
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
72
136
|
ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
|
73
137
|
end
|
74
138
|
|
139
|
+
# Invoked on completion of ThreadPoolExecutor task
|
140
|
+
# @!visibility private
|
141
|
+
# @return [void]
|
75
142
|
def task_observer(time, output, thread_error)
|
143
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
76
144
|
ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
|
77
145
|
create_thread if output
|
78
146
|
end
|
79
147
|
|
80
|
-
|
81
|
-
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
82
|
-
def ready_worker_count
|
83
|
-
synchronize do
|
84
|
-
workers_still_to_be_created = @max_length - @pool.length
|
85
|
-
workers_created_but_waiting = @ready.length
|
148
|
+
private
|
86
149
|
|
87
|
-
|
88
|
-
|
150
|
+
# @return [void]
|
151
|
+
def create_pools
|
152
|
+
ActiveSupport::Notifications.instrument("scheduler_create_pools.good_job", { performer_name: @performer.name, max_threads: @pool_options[:max_threads], poll_interval: @timer_options[:execution_interval], process_id: process_id }) do
|
153
|
+
@pool = ThreadPoolExecutor.new(@pool_options)
|
154
|
+
next unless @timer_options[:execution_interval].positive?
|
155
|
+
|
156
|
+
@timer = Concurrent::TimerTask.new(@timer_options) { create_thread }
|
157
|
+
@timer.add_observer(self, :timer_observer)
|
158
|
+
@timer.execute
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [Integer] Current process ID
|
163
|
+
def process_id
|
164
|
+
Process.pid
|
165
|
+
end
|
166
|
+
|
167
|
+
# @return [String] Current thread name
|
168
|
+
def thread_name
|
169
|
+
(Thread.current.name || Thread.current.object_id).to_s
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Slightly customized sub-class of Concurrent::ThreadPoolExecutor
|
174
|
+
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
175
|
+
# Number of idle or potential threads available to execute tasks
|
176
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
|
177
|
+
# @return [Integer]
|
178
|
+
def ready_worker_count
|
179
|
+
synchronize do
|
180
|
+
workers_still_to_be_created = @max_length - @pool.length
|
181
|
+
workers_created_but_waiting = @ready.length
|
182
|
+
|
183
|
+
workers_still_to_be_created + workers_created_but_waiting
|
89
184
|
end
|
90
185
|
end
|
91
186
|
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.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: dotenv
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: foreman
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,6 +164,20 @@ dependencies:
|
|
150
164
|
- - ">="
|
151
165
|
- !ruby/object:Gem::Version
|
152
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: puma
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
153
181
|
- !ruby/object:Gem::Dependency
|
154
182
|
name: rspec-rails
|
155
183
|
requirement: !ruby/object:Gem::Requirement
|
@@ -220,6 +248,20 @@ dependencies:
|
|
220
248
|
- - ">="
|
221
249
|
- !ruby/object:Gem::Version
|
222
250
|
version: '0'
|
251
|
+
- !ruby/object:Gem::Dependency
|
252
|
+
name: yard
|
253
|
+
requirement: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - ">="
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: '0'
|
258
|
+
type: :development
|
259
|
+
prerelease: false
|
260
|
+
version_requirements: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - ">="
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: '0'
|
223
265
|
description: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
224
266
|
email:
|
225
267
|
- bensheldon@gmail.com
|
@@ -241,9 +283,12 @@ files:
|
|
241
283
|
- lib/good_job.rb
|
242
284
|
- lib/good_job/adapter.rb
|
243
285
|
- lib/good_job/cli.rb
|
286
|
+
- lib/good_job/configuration.rb
|
287
|
+
- lib/good_job/current_execution.rb
|
244
288
|
- lib/good_job/job.rb
|
245
289
|
- lib/good_job/lockable.rb
|
246
|
-
- lib/good_job/
|
290
|
+
- lib/good_job/log_subscriber.rb
|
291
|
+
- lib/good_job/multi_scheduler.rb
|
247
292
|
- lib/good_job/performer.rb
|
248
293
|
- lib/good_job/pg_locks.rb
|
249
294
|
- lib/good_job/railtie.rb
|
@@ -258,7 +303,7 @@ metadata:
|
|
258
303
|
documentation_uri: https://rdoc.info/github/bensheldon/good_job
|
259
304
|
homepage_uri: https://github.com/bensheldon/good_job
|
260
305
|
source_code_uri: https://github.com/bensheldon/good_job
|
261
|
-
post_install_message:
|
306
|
+
post_install_message:
|
262
307
|
rdoc_options:
|
263
308
|
- "--title"
|
264
309
|
- GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
@@ -281,7 +326,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
281
326
|
version: '0'
|
282
327
|
requirements: []
|
283
328
|
rubygems_version: 3.0.3
|
284
|
-
signing_key:
|
329
|
+
signing_key:
|
285
330
|
specification_version: 4
|
286
331
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
287
332
|
test_files: []
|
data/lib/good_job/logging.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
module GoodJob
|
2
|
-
module Logging
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
included do
|
6
|
-
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
7
|
-
|
8
|
-
def self.tag_logger(*tags)
|
9
|
-
if logger.respond_to?(:tagged)
|
10
|
-
tags.unshift "GoodJob" unless logger.formatter.current_tags.include?("GoodJob")
|
11
|
-
logger.tagged(*tags) { yield }
|
12
|
-
else
|
13
|
-
yield
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class LogSubscriber < ActiveSupport::LogSubscriber
|
19
|
-
def create(event)
|
20
|
-
good_job = event.payload[:good_job]
|
21
|
-
|
22
|
-
info do
|
23
|
-
"Created GoodJob resource with id #{good_job.id}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def timer_task_finished(event)
|
28
|
-
exception = event.payload[:error]
|
29
|
-
return unless exception
|
30
|
-
|
31
|
-
error do
|
32
|
-
"ERROR: #{exception}\n #{exception.backtrace}"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def job_finished(event)
|
37
|
-
exception = event.payload[:error]
|
38
|
-
return unless exception
|
39
|
-
|
40
|
-
error do
|
41
|
-
"ERROR: #{exception}\n #{exception.backtrace}"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def scheduler_start_shutdown(_event)
|
46
|
-
info do
|
47
|
-
"Shutting down scheduler..."
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def scheduler_shutdown(_event)
|
52
|
-
info do
|
53
|
-
"Scheduler is shut down."
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def logger
|
60
|
-
GoodJob.logger
|
61
|
-
end
|
62
|
-
|
63
|
-
def thread_name
|
64
|
-
Thread.current.name || Thread.current.object_id
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
GoodJob::Logging::LogSubscriber.attach_to :good_job
|