good_job 1.9.0 → 1.9.5
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 +79 -0
- data/README.md +2 -0
- data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +7 -0
- data/engine/app/controllers/good_job/assets_controller.rb +29 -0
- data/engine/app/controllers/good_job/dashboards_controller.rb +7 -5
- data/engine/app/views/good_job/dashboards/index.html.erb +1 -1
- data/engine/app/views/layouts/good_job/base.html.erb +8 -12
- data/engine/app/views/shared/_chart.erb +3 -2
- data/engine/config/routes.rb +8 -0
- data/lib/good_job.rb +26 -8
- data/lib/good_job/adapter.rb +15 -11
- data/lib/good_job/cli.rb +1 -1
- data/lib/good_job/configuration.rb +3 -3
- data/lib/good_job/current_execution.rb +0 -1
- data/lib/good_job/daemon.rb +6 -0
- data/lib/good_job/execution_result.rb +20 -0
- data/lib/good_job/job.rb +47 -37
- data/lib/good_job/job_performer.rb +2 -2
- data/lib/good_job/lockable.rb +4 -7
- data/lib/good_job/log_subscriber.rb +15 -14
- data/lib/good_job/multi_scheduler.rb +9 -0
- data/lib/good_job/notifier.rb +7 -6
- data/lib/good_job/poller.rb +9 -5
- data/lib/good_job/scheduler.rb +35 -18
- data/lib/good_job/version.rb +1 -1
- metadata +6 -4
- data/engine/app/assets/vendor/bootstrap/bootstrap-native.js +0 -1662
- data/engine/app/assets/vendor/bootstrap/bootstrap.css +0 -10258
data/lib/good_job/cli.rb
CHANGED
@@ -83,7 +83,7 @@ module GoodJob
|
|
83
83
|
|
84
84
|
notifier = GoodJob::Notifier.new
|
85
85
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
86
|
-
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
86
|
+
scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
87
87
|
notifier.recipients << [scheduler, :create_thread]
|
88
88
|
poller.recipients << [scheduler, :create_thread]
|
89
89
|
|
@@ -84,9 +84,9 @@ module GoodJob
|
|
84
84
|
# on the format of this string.
|
85
85
|
# @return [String]
|
86
86
|
def queue_string
|
87
|
-
options[:queues] ||
|
88
|
-
rails_config[:queues] ||
|
89
|
-
env['GOOD_JOB_QUEUES'] ||
|
87
|
+
options[:queues].presence ||
|
88
|
+
rails_config[:queues].presence ||
|
89
|
+
env['GOOD_JOB_QUEUES'].presence ||
|
90
90
|
'*'
|
91
91
|
end
|
92
92
|
|
@@ -3,7 +3,6 @@ require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
|
3
3
|
module GoodJob
|
4
4
|
# Thread-local attributes for passing values from Instrumentation.
|
5
5
|
# (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
|
6
|
-
|
7
6
|
module CurrentExecution
|
8
7
|
# @!attribute [rw] error_on_retry
|
9
8
|
# @!scope class
|
data/lib/good_job/daemon.rb
CHANGED
@@ -13,6 +13,7 @@ module GoodJob
|
|
13
13
|
end
|
14
14
|
|
15
15
|
# Daemonizes the current process and writes out a pidfile.
|
16
|
+
# @return [void]
|
16
17
|
def daemonize
|
17
18
|
check_pid
|
18
19
|
Process.daemon
|
@@ -21,6 +22,7 @@ module GoodJob
|
|
21
22
|
|
22
23
|
private
|
23
24
|
|
25
|
+
# @return [void]
|
24
26
|
def write_pid
|
25
27
|
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
|
26
28
|
at_exit { File.delete(pidfile) if File.exist?(pidfile) }
|
@@ -29,10 +31,12 @@ module GoodJob
|
|
29
31
|
retry
|
30
32
|
end
|
31
33
|
|
34
|
+
# @return [void]
|
32
35
|
def delete_pid
|
33
36
|
File.delete(pidfile) if File.exist?(pidfile)
|
34
37
|
end
|
35
38
|
|
39
|
+
# @return [void]
|
36
40
|
def check_pid
|
37
41
|
case pid_status(pidfile)
|
38
42
|
when :running, :not_owned
|
@@ -42,6 +46,8 @@ module GoodJob
|
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
49
|
+
# @param pidfile [Pathname, String]
|
50
|
+
# @return [Symbol]
|
45
51
|
def pid_status(pidfile)
|
46
52
|
return :exited unless File.exist?(pidfile)
|
47
53
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GoodJob
|
2
|
+
# Stores the results of job execution
|
3
|
+
class ExecutionResult
|
4
|
+
# @return [Object, nil]
|
5
|
+
attr_reader :value
|
6
|
+
# @return [Exception, nil]
|
7
|
+
attr_reader :handled_error
|
8
|
+
# @return [Exception, nil]
|
9
|
+
attr_reader :unhandled_error
|
10
|
+
|
11
|
+
# @param value [Object, nil]
|
12
|
+
# @param handled_error [Exception, nil]
|
13
|
+
# @param unhandled_error [Exception, nil]
|
14
|
+
def initialize(value:, handled_error: nil, unhandled_error: nil)
|
15
|
+
@value = value
|
16
|
+
@handled_error = handled_error
|
17
|
+
@unhandled_error = unhandled_error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/good_job/job.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
module GoodJob
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
class Job < ActiveRecord::Base
|
2
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
3
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
4
|
+
# @!parse
|
5
|
+
# class Job < ActiveRecord::Base; end
|
6
|
+
class Job < Object.const_get(GoodJob.active_record_parent_class)
|
6
7
|
include Lockable
|
7
8
|
|
8
9
|
# Raised if something attempts to execute a previously completed Job again.
|
@@ -19,6 +20,7 @@ module GoodJob
|
|
19
20
|
|
20
21
|
# Parse a string representing a group of queues into a more readable data
|
21
22
|
# structure.
|
23
|
+
# @param string [String] Queue string
|
22
24
|
# @return [Hash]
|
23
25
|
# How to match a given queue. It can have the following keys and values:
|
24
26
|
# - +{ all: true }+ indicates that all queues match.
|
@@ -48,6 +50,14 @@ module GoodJob
|
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
53
|
+
# Get Jobs with given class name
|
54
|
+
# @!method with_job_class
|
55
|
+
# @!scope class
|
56
|
+
# @param string [String]
|
57
|
+
# Job class name
|
58
|
+
# @return [ActiveRecord::Relation]
|
59
|
+
scope :with_job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
60
|
+
|
51
61
|
# Get Jobs that have not yet been completed.
|
52
62
|
# @!method unfinished
|
53
63
|
# @!scope class
|
@@ -92,6 +102,12 @@ module GoodJob
|
|
92
102
|
# @return [ActiveRecord::Relation]
|
93
103
|
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
94
104
|
|
105
|
+
# Get Jobs that started but not finished yet.
|
106
|
+
# @!method running
|
107
|
+
# @!scope class
|
108
|
+
# @return [ActiveRecord::Relation]
|
109
|
+
scope :running, -> { where.not(performed_at: nil).where(finished_at: nil) }
|
110
|
+
|
95
111
|
# Get Jobs on queues that match the given queue string.
|
96
112
|
# @!method queue_string(string)
|
97
113
|
# @!scope class
|
@@ -134,29 +150,26 @@ module GoodJob
|
|
134
150
|
|
135
151
|
# Finds the next eligible Job, acquire an advisory lock related to it, and
|
136
152
|
# executes the job.
|
137
|
-
# @return [
|
153
|
+
# @return [ExecutionResult, nil]
|
138
154
|
# If a job was executed, returns an array with the {Job} record, the
|
139
155
|
# return value for the job's +#perform+ method, and the exception the job
|
140
156
|
# raised, if any (if the job raised, then the second array entry will be
|
141
157
|
# +nil+). If there were no jobs to execute, returns +nil+.
|
142
158
|
def self.perform_with_advisory_lock
|
143
|
-
good_job = nil
|
144
|
-
result = nil
|
145
|
-
error = nil
|
146
|
-
|
147
159
|
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
148
160
|
good_job = good_jobs.first
|
149
161
|
# TODO: Determine why some records are fetched without an advisory lock at all
|
150
162
|
break unless good_job&.executable?
|
151
163
|
|
152
|
-
|
164
|
+
good_job.perform
|
153
165
|
end
|
154
|
-
|
155
|
-
[good_job, result, error] if good_job
|
156
166
|
end
|
157
167
|
|
158
168
|
# Fetches the scheduled execution time of the next eligible Job(s).
|
159
|
-
# @
|
169
|
+
# @param after [DateTime]
|
170
|
+
# @param limit [Integer]
|
171
|
+
# @param now_limit [Integer, nil]
|
172
|
+
# @return [Array<DateTime>]
|
160
173
|
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
161
174
|
query = advisory_unlocked.unfinished.schedule_ordered
|
162
175
|
|
@@ -182,7 +195,6 @@ module GoodJob
|
|
182
195
|
# @return [Job]
|
183
196
|
# The new {Job} instance representing the queued ActiveJob job.
|
184
197
|
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
185
|
-
good_job = nil
|
186
198
|
ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
|
187
199
|
good_job = GoodJob::Job.new(
|
188
200
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
@@ -196,49 +208,37 @@ module GoodJob
|
|
196
208
|
|
197
209
|
good_job.save!
|
198
210
|
active_job.provider_job_id = good_job.id
|
199
|
-
end
|
200
211
|
|
201
|
-
|
212
|
+
good_job
|
213
|
+
end
|
202
214
|
end
|
203
215
|
|
204
216
|
# Execute the ActiveJob job this {Job} represents.
|
205
|
-
# @return [
|
217
|
+
# @return [ExecutionResult]
|
206
218
|
# An array of the return value of the job's +#perform+ method and the
|
207
219
|
# exception raised by the job, if any. If the job completed successfully,
|
208
220
|
# the second array entry (the exception) will be +nil+ and vice versa.
|
209
221
|
def perform
|
210
222
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
211
223
|
|
212
|
-
GoodJob::CurrentExecution.reset
|
213
|
-
|
214
224
|
self.performed_at = Time.current
|
215
225
|
save! if GoodJob.preserve_job_records
|
216
226
|
|
217
|
-
result
|
218
|
-
|
219
|
-
result_error = nil
|
220
|
-
if result.is_a?(Exception)
|
221
|
-
result_error = result
|
222
|
-
result = nil
|
223
|
-
end
|
224
|
-
|
225
|
-
job_error = unhandled_error ||
|
226
|
-
result_error ||
|
227
|
-
GoodJob::CurrentExecution.error_on_retry ||
|
228
|
-
GoodJob::CurrentExecution.error_on_discard
|
227
|
+
result = execute
|
229
228
|
|
229
|
+
job_error = result.handled_error || result.unhandled_error
|
230
230
|
self.error = "#{job_error.class}: #{job_error.message}" if job_error
|
231
231
|
|
232
|
-
if unhandled_error && GoodJob.retry_on_unhandled_error
|
232
|
+
if result.unhandled_error && GoodJob.retry_on_unhandled_error
|
233
233
|
save!
|
234
|
-
elsif GoodJob.preserve_job_records == true || (unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
234
|
+
elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
235
235
|
self.finished_at = Time.current
|
236
236
|
save!
|
237
237
|
else
|
238
238
|
destroy!
|
239
239
|
end
|
240
240
|
|
241
|
-
|
241
|
+
result
|
242
242
|
end
|
243
243
|
|
244
244
|
# Tests whether this job is safe to be executed by this thread.
|
@@ -249,16 +249,26 @@ module GoodJob
|
|
249
249
|
|
250
250
|
private
|
251
251
|
|
252
|
+
# @return [ExecutionResult]
|
252
253
|
def execute
|
253
254
|
params = serialized_params.merge(
|
254
255
|
"provider_job_id" => id
|
255
256
|
)
|
256
257
|
|
258
|
+
GoodJob::CurrentExecution.reset
|
257
259
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
258
|
-
|
260
|
+
value = ActiveJob::Base.execute(params)
|
261
|
+
|
262
|
+
if value.is_a?(Exception)
|
263
|
+
handled_error = value
|
264
|
+
value = nil
|
265
|
+
end
|
266
|
+
handled_error ||= GoodJob::CurrentExecution.error_on_retry || GoodJob::CurrentExecution.error_on_discard
|
267
|
+
|
268
|
+
ExecutionResult.new(value: value, handled_error: handled_error)
|
269
|
+
rescue StandardError => e
|
270
|
+
ExecutionResult.new(value: nil, unhandled_error: e)
|
259
271
|
end
|
260
|
-
rescue StandardError => e
|
261
|
-
[nil, e]
|
262
272
|
end
|
263
273
|
end
|
264
274
|
end
|
@@ -24,7 +24,7 @@ module GoodJob
|
|
24
24
|
end
|
25
25
|
|
26
26
|
# Perform the next eligible job
|
27
|
-
# @return [
|
27
|
+
# @return [Object, nil] Returns job result or +nil+ if no job was found
|
28
28
|
def next
|
29
29
|
job_query.perform_with_advisory_lock
|
30
30
|
end
|
@@ -54,7 +54,7 @@ module GoodJob
|
|
54
54
|
# @param after [DateTime, Time, nil] future jobs scheduled after this time
|
55
55
|
# @param limit [Integer] number of future timestamps to return
|
56
56
|
# @param now_limit [Integer] number of past timestamps to return
|
57
|
-
# @return [Array<
|
57
|
+
# @return [Array<DateTime, Time>, nil]
|
58
58
|
def next_at(after: nil, limit: nil, now_limit: nil)
|
59
59
|
job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
|
60
60
|
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -143,7 +143,7 @@ module GoodJob
|
|
143
143
|
def supports_cte_materialization_specifiers?
|
144
144
|
return @_supports_cte_materialization_specifiers if defined?(@_supports_cte_materialization_specifiers)
|
145
145
|
|
146
|
-
@_supports_cte_materialization_specifiers =
|
146
|
+
@_supports_cte_materialization_specifiers = connection.postgresql_version >= 120000
|
147
147
|
end
|
148
148
|
end
|
149
149
|
|
@@ -158,7 +158,7 @@ module GoodJob
|
|
158
158
|
WHERE pg_try_advisory_lock(('x'||substr(md5($1 || $2::text), 1, 16))::bit(64)::bigint)
|
159
159
|
SQL
|
160
160
|
binds = [[nil, self.class.table_name], [nil, send(self.class.primary_key)]]
|
161
|
-
|
161
|
+
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
|
162
162
|
end
|
163
163
|
|
164
164
|
# Releases an advisory lock on this record if it is locked by this database
|
@@ -245,11 +245,8 @@ module GoodJob
|
|
245
245
|
|
246
246
|
private
|
247
247
|
|
248
|
-
|
249
|
-
|
250
|
-
self.class.send(:sanitize_sql_for_conditions, *args)
|
251
|
-
end
|
252
|
-
|
248
|
+
# @param query [String]
|
249
|
+
# @return [Boolean]
|
253
250
|
def pg_or_jdbc_query(query)
|
254
251
|
if Concurrent.on_jruby?
|
255
252
|
# Replace $1 bind parameters with ?
|
@@ -14,6 +14,7 @@ module GoodJob
|
|
14
14
|
|
15
15
|
# @!macro notification_responder
|
16
16
|
# Responds to the +$0.good_job+ notification.
|
17
|
+
# @param event [ActiveSupport::Notifications::Event]
|
17
18
|
# @return [void]
|
18
19
|
def create(event)
|
19
20
|
# FIXME: This method does not match any good_job notifications.
|
@@ -24,7 +25,7 @@ module GoodJob
|
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
|
-
#
|
28
|
+
# @!macro notification_responder
|
28
29
|
def finished_timer_task(event)
|
29
30
|
exception = event.payload[:error]
|
30
31
|
return unless exception
|
@@ -34,7 +35,7 @@ module GoodJob
|
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
|
-
#
|
38
|
+
# @!macro notification_responder
|
38
39
|
def finished_job_task(event)
|
39
40
|
exception = event.payload[:error]
|
40
41
|
return unless exception
|
@@ -44,7 +45,7 @@ module GoodJob
|
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
47
|
-
#
|
48
|
+
# @!macro notification_responder
|
48
49
|
def scheduler_create_pool(event)
|
49
50
|
max_threads = event.payload[:max_threads]
|
50
51
|
performer_name = event.payload[:performer_name]
|
@@ -55,7 +56,7 @@ module GoodJob
|
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
58
|
-
#
|
59
|
+
# @!macro notification_responder
|
59
60
|
def scheduler_shutdown_start(event)
|
60
61
|
process_id = event.payload[:process_id]
|
61
62
|
|
@@ -64,7 +65,7 @@ module GoodJob
|
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
|
-
#
|
68
|
+
# @!macro notification_responder
|
68
69
|
def scheduler_shutdown(event)
|
69
70
|
process_id = event.payload[:process_id]
|
70
71
|
|
@@ -73,7 +74,7 @@ module GoodJob
|
|
73
74
|
end
|
74
75
|
end
|
75
76
|
|
76
|
-
#
|
77
|
+
# @!macro notification_responder
|
77
78
|
def scheduler_restart_pools(event)
|
78
79
|
process_id = event.payload[:process_id]
|
79
80
|
|
@@ -82,7 +83,7 @@ module GoodJob
|
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
|
-
#
|
86
|
+
# @!macro notification_responder
|
86
87
|
def perform_job(event)
|
87
88
|
good_job = event.payload[:good_job]
|
88
89
|
process_id = event.payload[:process_id]
|
@@ -93,14 +94,14 @@ module GoodJob
|
|
93
94
|
end
|
94
95
|
end
|
95
96
|
|
96
|
-
#
|
97
|
-
def notifier_listen(
|
97
|
+
# @!macro notification_responder
|
98
|
+
def notifier_listen(event) # rubocop:disable Lint/UnusedMethodArgument
|
98
99
|
info do
|
99
100
|
"Notifier subscribed with LISTEN"
|
100
101
|
end
|
101
102
|
end
|
102
103
|
|
103
|
-
#
|
104
|
+
# @!macro notification_responder
|
104
105
|
def notifier_notified(event)
|
105
106
|
payload = event.payload[:payload]
|
106
107
|
|
@@ -109,7 +110,7 @@ module GoodJob
|
|
109
110
|
end
|
110
111
|
end
|
111
112
|
|
112
|
-
#
|
113
|
+
# @!macro notification_responder
|
113
114
|
def notifier_notify_error(event)
|
114
115
|
error = event.payload[:error]
|
115
116
|
|
@@ -118,14 +119,14 @@ module GoodJob
|
|
118
119
|
end
|
119
120
|
end
|
120
121
|
|
121
|
-
#
|
122
|
-
def notifier_unlisten(
|
122
|
+
# @!macro notification_responder
|
123
|
+
def notifier_unlisten(event) # rubocop:disable Lint/UnusedMethodArgument
|
123
124
|
info do
|
124
125
|
"Notifier unsubscribed with UNLISTEN"
|
125
126
|
end
|
126
127
|
end
|
127
128
|
|
128
|
-
#
|
129
|
+
# @!macro notification_responder
|
129
130
|
def cleanup_preserved_jobs(event)
|
130
131
|
timestamp = event.payload[:timestamp]
|
131
132
|
deleted_records_count = event.payload[:deleted_records_count]
|
@@ -4,31 +4,40 @@ module GoodJob
|
|
4
4
|
# @return [Array<Scheduler>] List of the scheduler delegates
|
5
5
|
attr_reader :schedulers
|
6
6
|
|
7
|
+
# @param schedulers [Array<Scheduler>]
|
7
8
|
def initialize(schedulers)
|
8
9
|
@schedulers = schedulers
|
9
10
|
end
|
10
11
|
|
11
12
|
# Delegates to {Scheduler#running?}.
|
13
|
+
# @return [Boolean, nil]
|
12
14
|
def running?
|
13
15
|
schedulers.all?(&:running?)
|
14
16
|
end
|
15
17
|
|
16
18
|
# Delegates to {Scheduler#shutdown?}.
|
19
|
+
# @return [Boolean, nil]
|
17
20
|
def shutdown?
|
18
21
|
schedulers.all?(&:shutdown?)
|
19
22
|
end
|
20
23
|
|
21
24
|
# Delegates to {Scheduler#shutdown}.
|
25
|
+
# @param timeout [Numeric, nil]
|
26
|
+
# @return [void]
|
22
27
|
def shutdown(timeout: -1)
|
23
28
|
GoodJob._shutdown_all(schedulers, timeout: timeout)
|
24
29
|
end
|
25
30
|
|
26
31
|
# Delegates to {Scheduler#restart}.
|
32
|
+
# @param timeout [Numeric, nil]
|
33
|
+
# @return [void]
|
27
34
|
def restart(timeout: -1)
|
28
35
|
GoodJob._shutdown_all(schedulers, :restart, timeout: timeout)
|
29
36
|
end
|
30
37
|
|
31
38
|
# Delegates to {Scheduler#create_thread}.
|
39
|
+
# @param state [Hash]
|
40
|
+
# @return [Boolean, nil]
|
32
41
|
def create_thread(state = nil)
|
33
42
|
results = []
|
34
43
|
|