good_job 1.6.0 → 1.9.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 +95 -9
- data/README.md +27 -17
- data/exe/good_job +3 -1
- data/lib/active_job/queue_adapters/good_job_adapter.rb +0 -4
- data/lib/good_job.rb +43 -7
- data/lib/good_job/adapter.rb +57 -22
- data/lib/good_job/cli.rb +26 -7
- data/lib/good_job/configuration.rb +62 -39
- data/lib/good_job/job.rb +26 -1
- data/lib/good_job/job_performer.rb +11 -0
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/multi_scheduler.rb +12 -7
- data/lib/good_job/notifier.rb +44 -35
- data/lib/good_job/poller.rb +32 -21
- data/lib/good_job/railtie.rb +4 -0
- data/lib/good_job/scheduler.rb +156 -51
- data/lib/good_job/version.rb +1 -1
- metadata +3 -17
data/lib/good_job/poller.rb
CHANGED
@@ -16,7 +16,7 @@ module GoodJob # :nodoc:
|
|
16
16
|
# @!attribute [r] instances
|
17
17
|
# @!scope class
|
18
18
|
# List of all instantiated Pollers in the current process.
|
19
|
-
# @return [
|
19
|
+
# @return [Array<GoodJob::Poller>]
|
20
20
|
cattr_reader :instances, default: [], instance_reader: false
|
21
21
|
|
22
22
|
# Creates GoodJob::Poller from a GoodJob::Configuration instance.
|
@@ -40,35 +40,44 @@ module GoodJob # :nodoc:
|
|
40
40
|
|
41
41
|
self.class.instances << self
|
42
42
|
|
43
|
-
|
43
|
+
create_timer
|
44
44
|
end
|
45
45
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
46
|
+
# Tests whether the timer is running.
|
47
|
+
# @return [true, false, nil]
|
48
|
+
delegate :running?, to: :timer, allow_nil: true
|
49
|
+
|
50
|
+
# Tests whether the timer is shutdown.
|
51
|
+
# @return [true, false, nil]
|
52
|
+
delegate :shutdown?, to: :timer, allow_nil: true
|
53
|
+
|
54
|
+
# Shut down the notifier.
|
49
55
|
# Use {#shutdown?} to determine whether threads have stopped.
|
50
|
-
# @param
|
56
|
+
# @param timeout [nil, Numeric] Seconds to wait for active threads.
|
57
|
+
#
|
58
|
+
# * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
|
59
|
+
# * +-1+, the scheduler will wait until the shutdown is complete.
|
60
|
+
# * +0+, the scheduler will immediately shutdown and stop any threads.
|
61
|
+
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
51
62
|
# @return [void]
|
52
|
-
def shutdown(
|
53
|
-
return
|
63
|
+
def shutdown(timeout: -1)
|
64
|
+
return if timer.nil? || timer.shutdown?
|
54
65
|
|
55
|
-
|
56
|
-
@timer.wait_for_termination if wait
|
57
|
-
end
|
66
|
+
timer.shutdown if timer.running?
|
58
67
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
68
|
+
if timer.shuttingdown? && timeout # rubocop:disable Style/GuardClause
|
69
|
+
timer_wait = timeout.negative? ? nil : timeout
|
70
|
+
timer.kill unless timer.wait_for_termination(timer_wait)
|
71
|
+
end
|
63
72
|
end
|
64
73
|
|
65
74
|
# Restart the poller.
|
66
75
|
# When shutdown, start; or shutdown and start.
|
67
|
-
# @param
|
76
|
+
# @param timeout [nil, Numeric] Seconds to wait; shares same values as {#shutdown}.
|
68
77
|
# @return [void]
|
69
|
-
def restart(
|
70
|
-
shutdown(
|
71
|
-
|
78
|
+
def restart(timeout: -1)
|
79
|
+
shutdown(timeout: timeout) if running?
|
80
|
+
create_timer
|
72
81
|
end
|
73
82
|
|
74
83
|
# Invoked on completion of TimerTask task.
|
@@ -76,12 +85,14 @@ module GoodJob # :nodoc:
|
|
76
85
|
# @return [void]
|
77
86
|
def timer_observer(time, executed_task, thread_error)
|
78
87
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
79
|
-
instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
|
88
|
+
ActiveSupport::Notifications.instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
|
80
89
|
end
|
81
90
|
|
82
91
|
private
|
83
92
|
|
84
|
-
|
93
|
+
attr_reader :timer
|
94
|
+
|
95
|
+
def create_timer
|
85
96
|
return if @timer_options[:execution_interval] <= 0
|
86
97
|
|
87
98
|
@timer = Concurrent::TimerTask.new(@timer_options) do
|
data/lib/good_job/railtie.rb
CHANGED
data/lib/good_job/scheduler.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "concurrent/executor/thread_pool_executor"
|
2
|
-
require "concurrent/
|
2
|
+
require "concurrent/executor/timer_set"
|
3
|
+
require "concurrent/scheduled_task"
|
3
4
|
require "concurrent/utility/processor_counter"
|
4
5
|
|
5
6
|
module GoodJob # :nodoc:
|
@@ -15,33 +16,39 @@ module GoodJob # :nodoc:
|
|
15
16
|
#
|
16
17
|
class Scheduler
|
17
18
|
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
18
|
-
# The thread pool is where work is performed.
|
19
|
-
|
19
|
+
# The thread pool executor is where work is performed.
|
20
|
+
DEFAULT_EXECUTOR_OPTIONS = {
|
20
21
|
name: name,
|
21
22
|
min_threads: 0,
|
22
23
|
max_threads: Configuration::DEFAULT_MAX_THREADS,
|
23
24
|
auto_terminate: true,
|
24
25
|
idletime: 60,
|
25
|
-
max_queue:
|
26
|
+
max_queue: Configuration::DEFAULT_MAX_THREADS,
|
26
27
|
fallback_policy: :discard,
|
27
28
|
}.freeze
|
28
29
|
|
29
30
|
# @!attribute [r] instances
|
30
31
|
# @!scope class
|
31
32
|
# List of all instantiated Schedulers in the current process.
|
32
|
-
# @return [
|
33
|
+
# @return [Array<GoodJob::Scheduler>]
|
33
34
|
cattr_reader :instances, default: [], instance_reader: false
|
34
35
|
|
35
36
|
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
36
37
|
# @param configuration [GoodJob::Configuration]
|
38
|
+
# @param warm_cache_on_initialize [Boolean]
|
37
39
|
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
38
|
-
def self.from_configuration(configuration)
|
40
|
+
def self.from_configuration(configuration, warm_cache_on_initialize: true)
|
39
41
|
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
40
42
|
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
41
43
|
max_threads = (max_threads || configuration.max_threads).to_i
|
42
44
|
|
43
45
|
job_performer = GoodJob::JobPerformer.new(queue_string)
|
44
|
-
GoodJob::Scheduler.new(
|
46
|
+
GoodJob::Scheduler.new(
|
47
|
+
job_performer,
|
48
|
+
max_threads: max_threads,
|
49
|
+
max_cache: configuration.max_cache,
|
50
|
+
warm_cache_on_initialize: warm_cache_on_initialize
|
51
|
+
)
|
45
52
|
end
|
46
53
|
|
47
54
|
if schedulers.size > 1
|
@@ -53,74 +60,108 @@ module GoodJob # :nodoc:
|
|
53
60
|
|
54
61
|
# @param performer [GoodJob::JobPerformer]
|
55
62
|
# @param max_threads [Numeric, nil] number of seconds between polls for jobs
|
56
|
-
|
63
|
+
# @param max_cache [Numeric, nil] maximum number of scheduled jobs to cache in memory
|
64
|
+
# @param warm_cache_on_initialize [Boolean] whether to warm the cache immediately
|
65
|
+
def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: true)
|
57
66
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
58
67
|
|
59
68
|
self.class.instances << self
|
60
69
|
|
61
70
|
@performer = performer
|
62
71
|
|
63
|
-
@
|
64
|
-
@
|
65
|
-
|
72
|
+
@max_cache = max_cache || 0
|
73
|
+
@executor_options = DEFAULT_EXECUTOR_OPTIONS.dup
|
74
|
+
if max_threads.present?
|
75
|
+
@executor_options[:max_threads] = max_threads
|
76
|
+
@executor_options[:max_queue] = max_threads
|
77
|
+
end
|
78
|
+
@executor_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@executor_options[:max_threads]})"
|
66
79
|
|
67
|
-
|
80
|
+
create_executor
|
81
|
+
warm_cache if warm_cache_on_initialize
|
68
82
|
end
|
69
83
|
|
84
|
+
# Tests whether the scheduler is running.
|
85
|
+
# @return [true, false, nil]
|
86
|
+
delegate :running?, to: :executor, allow_nil: true
|
87
|
+
|
88
|
+
# Tests whether the scheduler is shutdown.
|
89
|
+
# @return [true, false, nil]
|
90
|
+
delegate :shutdown?, to: :executor, allow_nil: true
|
91
|
+
|
70
92
|
# Shut down the scheduler.
|
71
|
-
# This stops all threads in the pool.
|
72
|
-
# If +wait+ is +true+, the scheduler will wait for any active tasks to finish.
|
73
|
-
# If +wait+ is +false+, this method will return immediately even though threads may still be running.
|
93
|
+
# This stops all threads in the thread pool.
|
74
94
|
# Use {#shutdown?} to determine whether threads have stopped.
|
75
|
-
# @param
|
95
|
+
# @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish
|
96
|
+
#
|
97
|
+
# * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
|
98
|
+
# * +-1+, the scheduler will wait until the shutdown is complete.
|
99
|
+
# * +0+, the scheduler will immediately shutdown and stop any active tasks.
|
100
|
+
# * A positive number will wait that many seconds before stopping any remaining active tasks.
|
76
101
|
# @return [void]
|
77
|
-
def shutdown(
|
78
|
-
return
|
79
|
-
|
80
|
-
instrument("scheduler_shutdown_start", { wait: wait })
|
81
|
-
instrument("scheduler_shutdown", { wait: wait }) do
|
82
|
-
@pool.shutdown
|
83
|
-
@pool.wait_for_termination if wait
|
84
|
-
# TODO: Should be killed if wait is not true
|
85
|
-
end
|
86
|
-
end
|
102
|
+
def shutdown(timeout: -1)
|
103
|
+
return if executor.nil? || executor.shutdown?
|
87
104
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
105
|
+
instrument("scheduler_shutdown_start", { timeout: timeout })
|
106
|
+
instrument("scheduler_shutdown", { timeout: timeout }) do
|
107
|
+
if executor.running?
|
108
|
+
@timer_set.shutdown
|
109
|
+
executor.shutdown
|
110
|
+
end
|
111
|
+
|
112
|
+
if executor.shuttingdown? && timeout
|
113
|
+
executor_wait = timeout.negative? ? nil : timeout
|
114
|
+
executor.kill unless executor.wait_for_termination(executor_wait)
|
115
|
+
end
|
116
|
+
end
|
92
117
|
end
|
93
118
|
|
94
119
|
# Restart the Scheduler.
|
95
120
|
# When shutdown, start; or shutdown and start.
|
96
|
-
# @param
|
121
|
+
# @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish; shares same values as {#shutdown}.
|
97
122
|
# @return [void]
|
98
|
-
def restart(
|
123
|
+
def restart(timeout: -1)
|
99
124
|
instrument("scheduler_restart_pools") do
|
100
|
-
shutdown(
|
101
|
-
|
125
|
+
shutdown(timeout: timeout) if running?
|
126
|
+
create_executor
|
127
|
+
warm_cache
|
102
128
|
end
|
103
129
|
end
|
104
130
|
|
105
131
|
# Wakes a thread to allow the performer to execute a task.
|
106
|
-
# @param state [nil, Object] Contextual information for the performer. See {
|
132
|
+
# @param state [nil, Object] Contextual information for the performer. See {JobPerformer#next?}.
|
107
133
|
# @return [nil, Boolean] Whether work was started.
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
134
|
+
#
|
135
|
+
# * +nil+ if the scheduler is unable to take new work, for example if the thread pool is shut down or at capacity.
|
136
|
+
# * +true+ if the performer started executing work.
|
137
|
+
# * +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
|
111
138
|
def create_thread(state = nil)
|
112
|
-
return nil unless
|
113
|
-
return false if state && !@performer.next?(state)
|
139
|
+
return nil unless executor.running?
|
114
140
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
141
|
+
if state
|
142
|
+
return false unless performer.next?(state)
|
143
|
+
|
144
|
+
if state[:scheduled_at]
|
145
|
+
scheduled_at = if state[:scheduled_at].is_a? String
|
146
|
+
Time.zone.parse state[:scheduled_at]
|
147
|
+
else
|
148
|
+
state[:scheduled_at]
|
149
|
+
end
|
150
|
+
delay = [(scheduled_at - Time.current).to_f, 0].max
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
delay ||= 0
|
155
|
+
run_now = delay <= 0.01
|
156
|
+
if run_now
|
157
|
+
return nil unless executor.ready_worker_count.positive?
|
158
|
+
elsif @max_cache.positive?
|
159
|
+
return nil unless remaining_cache_count.positive?
|
119
160
|
end
|
120
|
-
future.add_observer(self, :task_observer)
|
121
|
-
future.execute
|
122
161
|
|
123
|
-
|
162
|
+
create_task(delay)
|
163
|
+
|
164
|
+
run_now ? true : nil
|
124
165
|
end
|
125
166
|
|
126
167
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -129,17 +170,57 @@ module GoodJob # :nodoc:
|
|
129
170
|
def task_observer(time, output, thread_error)
|
130
171
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
131
172
|
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
132
|
-
|
173
|
+
create_task if output
|
174
|
+
end
|
175
|
+
|
176
|
+
# Information about the Scheduler
|
177
|
+
# @return [Hash]
|
178
|
+
def stats
|
179
|
+
{
|
180
|
+
name: performer.name,
|
181
|
+
max_threads: @executor_options[:max_threads],
|
182
|
+
active_threads: @executor_options[:max_threads] - executor.ready_worker_count,
|
183
|
+
available_threads: executor.ready_worker_count,
|
184
|
+
max_cache: @max_cache,
|
185
|
+
active_cache: cache_count,
|
186
|
+
available_cache: remaining_cache_count,
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
# Preload existing runnable and future-scheduled jobs
|
191
|
+
# @return [void]
|
192
|
+
def warm_cache
|
193
|
+
return if @max_cache.zero?
|
194
|
+
|
195
|
+
performer.next_at(
|
196
|
+
limit: @max_cache,
|
197
|
+
now_limit: @executor_options[:max_threads]
|
198
|
+
).each do |scheduled_at|
|
199
|
+
create_thread({ scheduled_at: scheduled_at })
|
200
|
+
end
|
133
201
|
end
|
134
202
|
|
135
203
|
private
|
136
204
|
|
137
|
-
|
138
|
-
|
139
|
-
|
205
|
+
attr_reader :performer, :executor, :timer_set
|
206
|
+
|
207
|
+
def create_executor
|
208
|
+
instrument("scheduler_create_pool", { performer_name: performer.name, max_threads: @executor_options[:max_threads] }) do
|
209
|
+
@timer_set = TimerSet.new
|
210
|
+
@executor = ThreadPoolExecutor.new(@executor_options)
|
140
211
|
end
|
141
212
|
end
|
142
213
|
|
214
|
+
def create_task(delay = 0)
|
215
|
+
future = Concurrent::ScheduledTask.new(delay, args: [performer], executor: executor, timer_set: timer_set) do |thr_performer|
|
216
|
+
output = nil
|
217
|
+
Rails.application.executor.wrap { output = thr_performer.next }
|
218
|
+
output
|
219
|
+
end
|
220
|
+
future.add_observer(self, :task_observer)
|
221
|
+
future.execute
|
222
|
+
end
|
223
|
+
|
143
224
|
def instrument(name, payload = {}, &block)
|
144
225
|
payload = payload.reverse_merge({
|
145
226
|
scheduler: self,
|
@@ -150,6 +231,14 @@ module GoodJob # :nodoc:
|
|
150
231
|
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
151
232
|
end
|
152
233
|
|
234
|
+
def cache_count
|
235
|
+
timer_set.length
|
236
|
+
end
|
237
|
+
|
238
|
+
def remaining_cache_count
|
239
|
+
@max_cache - cache_count
|
240
|
+
end
|
241
|
+
|
153
242
|
# Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
|
154
243
|
# @private
|
155
244
|
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
@@ -168,5 +257,21 @@ module GoodJob # :nodoc:
|
|
168
257
|
end
|
169
258
|
end
|
170
259
|
end
|
260
|
+
|
261
|
+
# Custom sub-class of +Concurrent::TimerSet+ for additional behavior.
|
262
|
+
# @private
|
263
|
+
class TimerSet < Concurrent::TimerSet
|
264
|
+
# Number of scheduled jobs in the queue
|
265
|
+
# @return [Integer]
|
266
|
+
def length
|
267
|
+
@queue.length
|
268
|
+
end
|
269
|
+
|
270
|
+
# Clear the queue
|
271
|
+
# @return [void]
|
272
|
+
def reset
|
273
|
+
synchronize { @queue.clear }
|
274
|
+
end
|
275
|
+
end
|
171
276
|
end
|
172
277
|
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.9.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: 2021-
|
11
|
+
date: 2021-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -94,20 +94,6 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '2.0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: appraisal
|
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'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: capybara
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -400,7 +386,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
400
386
|
- !ruby/object:Gem::Version
|
401
387
|
version: '0'
|
402
388
|
requirements: []
|
403
|
-
rubygems_version: 3.2.
|
389
|
+
rubygems_version: 3.2.13
|
404
390
|
signing_key:
|
405
391
|
specification_version: 4
|
406
392
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|