good_job 1.4.1 → 1.8.0
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 +88 -7
- data/README.md +61 -15
- data/exe/good_job +1 -0
- data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -3
- data/lib/good_job.rb +34 -7
- data/lib/good_job/adapter.rb +57 -33
- data/lib/good_job/cli.rb +25 -4
- data/lib/good_job/configuration.rb +76 -20
- data/lib/good_job/daemon.rb +59 -0
- data/lib/good_job/job.rb +25 -0
- data/lib/good_job/job_performer.rb +74 -0
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/multi_scheduler.rb +11 -6
- data/lib/good_job/notifier.rb +41 -32
- data/lib/good_job/poller.rb +31 -20
- data/lib/good_job/railtie.rb +10 -2
- data/lib/good_job/scheduler.rb +141 -66
- data/lib/good_job/version.rb +1 -1
- metadata +5 -4
- data/lib/good_job/performer.rb +0 -60
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.
|
@@ -81,7 +90,9 @@ module GoodJob # :nodoc:
|
|
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
@@ -1,8 +1,12 @@
|
|
1
1
|
module GoodJob
|
2
2
|
# Ruby on Rails integration.
|
3
3
|
class Railtie < ::Rails::Railtie
|
4
|
-
|
5
|
-
|
4
|
+
config.good_job = ActiveSupport::OrderedOptions.new
|
5
|
+
|
6
|
+
initializer "good_job.logger" do |_app|
|
7
|
+
ActiveSupport.on_load(:good_job) do
|
8
|
+
self.logger = ::Rails.logger
|
9
|
+
end
|
6
10
|
GoodJob::LogSubscriber.attach_to :good_job
|
7
11
|
end
|
8
12
|
|
@@ -15,5 +19,9 @@ module GoodJob
|
|
15
19
|
GoodJob::CurrentExecution.error_on_discard = event.payload[:error]
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
23
|
+
config.after_initialize do
|
24
|
+
GoodJob::Scheduler.instances.each(&:warm_cache)
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
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:
|
@@ -8,52 +9,46 @@ module GoodJob # :nodoc:
|
|
8
9
|
# periodically checking for available tasks, executing tasks within a thread,
|
9
10
|
# and efficiently scaling active threads.
|
10
11
|
#
|
11
|
-
# Every scheduler has a single {
|
12
|
+
# Every scheduler has a single {JobPerformer} that will execute tasks.
|
12
13
|
# The scheduler is responsible for calling its performer efficiently across threads managed by an instance of +Concurrent::ThreadPoolExecutor+.
|
13
14
|
# If a performer does not have work, the thread will go to sleep.
|
14
15
|
# The scheduler maintains an instance of +Concurrent::TimerTask+, which wakes sleeping threads and causes them to check whether the performer has new work.
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
true
|
52
|
-
end
|
53
|
-
end
|
54
|
-
job_performer = GoodJob::Performer.new(job_query, :perform_with_advisory_lock, name: queue_string, filter: job_filter)
|
55
|
-
|
56
|
-
GoodJob::Scheduler.new(job_performer, max_threads: max_threads)
|
45
|
+
job_performer = GoodJob::JobPerformer.new(queue_string)
|
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
|
+
)
|
57
52
|
end
|
58
53
|
|
59
54
|
if schedulers.size > 1
|
@@ -63,76 +58,110 @@ module GoodJob # :nodoc:
|
|
63
58
|
end
|
64
59
|
end
|
65
60
|
|
66
|
-
# @param performer [GoodJob::
|
61
|
+
# @param performer [GoodJob::JobPerformer]
|
67
62
|
# @param max_threads [Numeric, nil] number of seconds between polls for jobs
|
68
|
-
|
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)
|
69
66
|
raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
|
70
67
|
|
71
68
|
self.class.instances << self
|
72
69
|
|
73
70
|
@performer = performer
|
74
71
|
|
75
|
-
@
|
76
|
-
@
|
77
|
-
|
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]})"
|
78
79
|
|
79
|
-
|
80
|
+
create_executor
|
81
|
+
warm_cache if warm_cache_on_initialize
|
80
82
|
end
|
81
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
|
+
|
82
92
|
# Shut down the scheduler.
|
83
|
-
# This stops all threads in the pool.
|
84
|
-
# If +wait+ is +true+, the scheduler will wait for any active tasks to finish.
|
85
|
-
# 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.
|
86
94
|
# Use {#shutdown?} to determine whether threads have stopped.
|
87
|
-
# @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.
|
88
101
|
# @return [void]
|
89
|
-
def shutdown(
|
90
|
-
return
|
91
|
-
|
92
|
-
instrument("scheduler_shutdown_start", { wait: wait })
|
93
|
-
instrument("scheduler_shutdown", { wait: wait }) do
|
94
|
-
@pool.shutdown
|
95
|
-
@pool.wait_for_termination if wait
|
96
|
-
# TODO: Should be killed if wait is not true
|
97
|
-
end
|
98
|
-
end
|
102
|
+
def shutdown(timeout: -1)
|
103
|
+
return if executor.nil? || executor.shutdown?
|
99
104
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
104
117
|
end
|
105
118
|
|
106
119
|
# Restart the Scheduler.
|
107
120
|
# When shutdown, start; or shutdown and start.
|
108
|
-
# @param
|
121
|
+
# @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish; shares same values as {#shutdown}.
|
109
122
|
# @return [void]
|
110
|
-
def restart(
|
123
|
+
def restart(timeout: -1)
|
111
124
|
instrument("scheduler_restart_pools") do
|
112
|
-
shutdown(
|
113
|
-
|
125
|
+
shutdown(timeout: timeout) if running?
|
126
|
+
create_executor
|
127
|
+
warm_cache
|
114
128
|
end
|
115
129
|
end
|
116
130
|
|
117
131
|
# Wakes a thread to allow the performer to execute a task.
|
118
|
-
# @param state [nil, Object] Contextual information for the performer. See {
|
132
|
+
# @param state [nil, Object] Contextual information for the performer. See {JobPerformer#next?}.
|
119
133
|
# @return [nil, Boolean] Whether work was started.
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
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.
|
123
138
|
def create_thread(state = nil)
|
124
|
-
return nil unless
|
125
|
-
return false if state && !@performer.next?(state)
|
139
|
+
return nil unless executor.running?
|
126
140
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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?
|
131
160
|
end
|
132
|
-
future.add_observer(self, :task_observer)
|
133
|
-
future.execute
|
134
161
|
|
135
|
-
|
162
|
+
create_task(delay)
|
163
|
+
|
164
|
+
run_now ? true : nil
|
136
165
|
end
|
137
166
|
|
138
167
|
# Invoked on completion of ThreadPoolExecutor task
|
@@ -141,17 +170,55 @@ module GoodJob # :nodoc:
|
|
141
170
|
def task_observer(time, output, thread_error)
|
142
171
|
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
143
172
|
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
144
|
-
|
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
|
+
def warm_cache
|
191
|
+
return if @max_cache.zero?
|
192
|
+
|
193
|
+
performer.next_at(
|
194
|
+
limit: @max_cache,
|
195
|
+
now_limit: @executor_options[:max_threads]
|
196
|
+
).each do |scheduled_at|
|
197
|
+
create_thread({ scheduled_at: scheduled_at })
|
198
|
+
end
|
145
199
|
end
|
146
200
|
|
147
201
|
private
|
148
202
|
|
149
|
-
|
150
|
-
|
151
|
-
|
203
|
+
attr_reader :performer, :executor, :timer_set
|
204
|
+
|
205
|
+
def create_executor
|
206
|
+
instrument("scheduler_create_pool", { performer_name: performer.name, max_threads: @executor_options[:max_threads] }) do
|
207
|
+
@timer_set = Concurrent::TimerSet.new
|
208
|
+
@executor = ThreadPoolExecutor.new(@executor_options)
|
152
209
|
end
|
153
210
|
end
|
154
211
|
|
212
|
+
def create_task(delay = 0)
|
213
|
+
future = Concurrent::ScheduledTask.new(delay, args: [performer], executor: executor, timer_set: timer_set) do |thr_performer|
|
214
|
+
output = nil
|
215
|
+
Rails.application.executor.wrap { output = thr_performer.next }
|
216
|
+
output
|
217
|
+
end
|
218
|
+
future.add_observer(self, :task_observer)
|
219
|
+
future.execute
|
220
|
+
end
|
221
|
+
|
155
222
|
def instrument(name, payload = {}, &block)
|
156
223
|
payload = payload.reverse_merge({
|
157
224
|
scheduler: self,
|
@@ -162,6 +229,14 @@ module GoodJob # :nodoc:
|
|
162
229
|
ActiveSupport::Notifications.instrument("#{name}.good_job", payload, &block)
|
163
230
|
end
|
164
231
|
|
232
|
+
def cache_count
|
233
|
+
timer_set.instance_variable_get(:@queue).length
|
234
|
+
end
|
235
|
+
|
236
|
+
def remaining_cache_count
|
237
|
+
@max_cache - cache_count
|
238
|
+
end
|
239
|
+
|
165
240
|
# Custom sub-class of +Concurrent::ThreadPoolExecutor+ to add additional worker status.
|
166
241
|
# @private
|
167
242
|
class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
|
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.8.0
|
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-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -357,12 +357,13 @@ files:
|
|
357
357
|
- lib/good_job/cli.rb
|
358
358
|
- lib/good_job/configuration.rb
|
359
359
|
- lib/good_job/current_execution.rb
|
360
|
+
- lib/good_job/daemon.rb
|
360
361
|
- lib/good_job/job.rb
|
362
|
+
- lib/good_job/job_performer.rb
|
361
363
|
- lib/good_job/lockable.rb
|
362
364
|
- lib/good_job/log_subscriber.rb
|
363
365
|
- lib/good_job/multi_scheduler.rb
|
364
366
|
- lib/good_job/notifier.rb
|
365
|
-
- lib/good_job/performer.rb
|
366
367
|
- lib/good_job/poller.rb
|
367
368
|
- lib/good_job/railtie.rb
|
368
369
|
- lib/good_job/scheduler.rb
|
@@ -399,7 +400,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
399
400
|
- !ruby/object:Gem::Version
|
400
401
|
version: '0'
|
401
402
|
requirements: []
|
402
|
-
rubygems_version: 3.
|
403
|
+
rubygems_version: 3.1.4
|
403
404
|
signing_key:
|
404
405
|
specification_version: 4
|
405
406
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|