good_job 1.7.0 → 1.9.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 +104 -9
- data/README.md +26 -18
- data/exe/good_job +3 -2
- data/lib/active_job/queue_adapters/good_job_adapter.rb +0 -4
- data/lib/good_job.rb +48 -7
- data/lib/good_job/adapter.rb +50 -18
- data/lib/good_job/cli.rb +23 -8
- data/lib/good_job/configuration.rb +46 -40
- data/lib/good_job/execution_result.rb +20 -0
- data/lib/good_job/job.rb +35 -37
- data/lib/good_job/job_performer.rb +4 -3
- data/lib/good_job/lockable.rb +2 -2
- data/lib/good_job/multi_scheduler.rb +12 -7
- data/lib/good_job/notifier.rb +43 -35
- data/lib/good_job/poller.rb +31 -21
- data/lib/good_job/scheduler.rb +119 -71
- data/lib/good_job/version.rb +1 -1
- metadata +4 -17
data/lib/good_job/cli.rb
CHANGED
@@ -15,9 +15,21 @@ module GoodJob
|
|
15
15
|
# Requiring this loads the application's configuration and classes.
|
16
16
|
RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
class << self
|
19
|
+
# Whether the CLI is running from the executable
|
20
|
+
# @return [Boolean, nil]
|
21
|
+
attr_accessor :within_exe
|
22
|
+
alias within_exe? within_exe
|
23
|
+
|
24
|
+
# Whether to log to STDOUT
|
25
|
+
# @return [Boolean, nil]
|
26
|
+
attr_accessor :log_to_stdout
|
27
|
+
alias log_to_stdout? log_to_stdout
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
def exit_on_failure?
|
31
|
+
true
|
32
|
+
end
|
21
33
|
end
|
22
34
|
|
23
35
|
# @!macro thor.desc
|
@@ -53,6 +65,10 @@ module GoodJob
|
|
53
65
|
type: :numeric,
|
54
66
|
banner: 'COUNT',
|
55
67
|
desc: "Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000)"
|
68
|
+
method_option :shutdown_timeout,
|
69
|
+
type: :numeric,
|
70
|
+
banner: 'SECONDS',
|
71
|
+
desc: "Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))"
|
56
72
|
method_option :daemonize,
|
57
73
|
type: :boolean,
|
58
74
|
desc: "Run as a background daemon (default: false)"
|
@@ -67,7 +83,7 @@ module GoodJob
|
|
67
83
|
|
68
84
|
notifier = GoodJob::Notifier.new
|
69
85
|
poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
70
|
-
scheduler = GoodJob::Scheduler.from_configuration(configuration)
|
86
|
+
scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
71
87
|
notifier.recipients << [scheduler, :create_thread]
|
72
88
|
poller.recipients << [scheduler, :create_thread]
|
73
89
|
|
@@ -81,9 +97,8 @@ module GoodJob
|
|
81
97
|
break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown?
|
82
98
|
end
|
83
99
|
|
84
|
-
notifier
|
85
|
-
|
86
|
-
scheduler.shutdown
|
100
|
+
executors = [notifier, poller, scheduler]
|
101
|
+
GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
|
87
102
|
end
|
88
103
|
|
89
104
|
default_task :start
|
@@ -133,7 +148,7 @@ module GoodJob
|
|
133
148
|
# Rails or from the application can be set up here.
|
134
149
|
def set_up_application!
|
135
150
|
require RAILS_ENVIRONMENT_RB
|
136
|
-
return unless
|
151
|
+
return unless GoodJob::CLI.log_to_stdout? && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, $stdout)
|
137
152
|
|
138
153
|
GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
|
139
154
|
GoodJob::LogSubscriber.reset_logger
|
@@ -5,6 +5,8 @@ module GoodJob
|
|
5
5
|
# set options to get the final values for each option.
|
6
6
|
#
|
7
7
|
class Configuration
|
8
|
+
# Valid execution modes.
|
9
|
+
EXECUTION_MODES = [:async, :async_server, :external, :inline].freeze
|
8
10
|
# Default number of threads to use per {Scheduler}
|
9
11
|
DEFAULT_MAX_THREADS = 5
|
10
12
|
# Default number of seconds between polls for jobs
|
@@ -13,6 +15,8 @@ module GoodJob
|
|
13
15
|
DEFAULT_MAX_CACHE = 10000
|
14
16
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs}
|
15
17
|
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
|
18
|
+
# Default to always wait for jobs to finish for {Adapter#shutdown}
|
19
|
+
DEFAULT_SHUTDOWN_TIMEOUT = -1
|
16
20
|
|
17
21
|
# The options that were explicitly set when initializing +Configuration+.
|
18
22
|
# @return [Hash]
|
@@ -33,40 +37,30 @@ module GoodJob
|
|
33
37
|
@env = env
|
34
38
|
end
|
35
39
|
|
40
|
+
def validate!
|
41
|
+
raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
|
42
|
+
end
|
43
|
+
|
36
44
|
# Specifies how and where jobs should be executed. See {Adapter#initialize}
|
37
45
|
# for more details on possible values.
|
38
|
-
#
|
39
|
-
# When running inside a Rails app, you may want to use
|
40
|
-
# {#rails_execution_mode}, which takes the current Rails environment into
|
41
|
-
# account when determining the final value.
|
42
|
-
#
|
43
|
-
# @param default [Symbol]
|
44
|
-
# Value to use if none was specified in the configuration.
|
45
46
|
# @return [Symbol]
|
46
|
-
def execution_mode
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
else
|
56
|
-
default
|
57
|
-
end
|
58
|
-
end
|
47
|
+
def execution_mode
|
48
|
+
@_execution_mode ||= begin
|
49
|
+
mode = if GoodJob::CLI.within_exe?
|
50
|
+
:external
|
51
|
+
else
|
52
|
+
options[:execution_mode] ||
|
53
|
+
rails_config[:execution_mode] ||
|
54
|
+
env['GOOD_JOB_EXECUTION_MODE']
|
55
|
+
end
|
59
56
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
:inline
|
68
|
-
else
|
69
|
-
:external
|
57
|
+
if mode
|
58
|
+
mode.to_sym
|
59
|
+
elsif Rails.env.development? || Rails.env.test?
|
60
|
+
:inline
|
61
|
+
else
|
62
|
+
:external
|
63
|
+
end
|
70
64
|
end
|
71
65
|
end
|
72
66
|
|
@@ -77,10 +71,10 @@ module GoodJob
|
|
77
71
|
def max_threads
|
78
72
|
(
|
79
73
|
options[:max_threads] ||
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
74
|
+
rails_config[:max_threads] ||
|
75
|
+
env['GOOD_JOB_MAX_THREADS'] ||
|
76
|
+
env['RAILS_MAX_THREADS'] ||
|
77
|
+
DEFAULT_MAX_THREADS
|
84
78
|
).to_i
|
85
79
|
end
|
86
80
|
|
@@ -103,9 +97,9 @@ module GoodJob
|
|
103
97
|
def poll_interval
|
104
98
|
(
|
105
99
|
options[:poll_interval] ||
|
106
|
-
|
107
|
-
|
108
|
-
|
100
|
+
rails_config[:poll_interval] ||
|
101
|
+
env['GOOD_JOB_POLL_INTERVAL'] ||
|
102
|
+
DEFAULT_POLL_INTERVAL
|
109
103
|
).to_i
|
110
104
|
end
|
111
105
|
|
@@ -122,15 +116,27 @@ module GoodJob
|
|
122
116
|
).to_i
|
123
117
|
end
|
124
118
|
|
119
|
+
# The number of seconds to wait for jobs to finish when shutting down
|
120
|
+
# before stopping the thread. +-1+ is forever.
|
121
|
+
# @return [Numeric]
|
122
|
+
def shutdown_timeout
|
123
|
+
(
|
124
|
+
options[:shutdown_timeout] ||
|
125
|
+
rails_config[:shutdown_timeout] ||
|
126
|
+
env['GOOD_JOB_SHUTDOWN_TIMEOUT'] ||
|
127
|
+
DEFAULT_SHUTDOWN_TIMEOUT
|
128
|
+
).to_f
|
129
|
+
end
|
130
|
+
|
125
131
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
126
132
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
127
133
|
# @return [Integer]
|
128
134
|
def cleanup_preserved_jobs_before_seconds_ago
|
129
135
|
(
|
130
136
|
options[:before_seconds_ago] ||
|
131
|
-
|
132
|
-
|
133
|
-
|
137
|
+
rails_config[:cleanup_preserved_jobs_before_seconds_ago] ||
|
138
|
+
env['GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO'] ||
|
139
|
+
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO
|
134
140
|
).to_i
|
135
141
|
end
|
136
142
|
|
@@ -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.
|
@@ -15,8 +16,11 @@ module GoodJob
|
|
15
16
|
|
16
17
|
self.table_name = 'good_jobs'.freeze
|
17
18
|
|
19
|
+
attr_readonly :serialized_params
|
20
|
+
|
18
21
|
# Parse a string representing a group of queues into a more readable data
|
19
22
|
# structure.
|
23
|
+
# @param string [String] Queue string
|
20
24
|
# @return [Hash]
|
21
25
|
# How to match a given queue. It can have the following keys and values:
|
22
26
|
# - +{ all: true }+ indicates that all queues match.
|
@@ -132,29 +136,26 @@ module GoodJob
|
|
132
136
|
|
133
137
|
# Finds the next eligible Job, acquire an advisory lock related to it, and
|
134
138
|
# executes the job.
|
135
|
-
# @return [
|
139
|
+
# @return [ExecutionResult, nil]
|
136
140
|
# If a job was executed, returns an array with the {Job} record, the
|
137
141
|
# return value for the job's +#perform+ method, and the exception the job
|
138
142
|
# raised, if any (if the job raised, then the second array entry will be
|
139
143
|
# +nil+). If there were no jobs to execute, returns +nil+.
|
140
144
|
def self.perform_with_advisory_lock
|
141
|
-
good_job = nil
|
142
|
-
result = nil
|
143
|
-
error = nil
|
144
|
-
|
145
145
|
unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
|
146
146
|
good_job = good_jobs.first
|
147
147
|
# TODO: Determine why some records are fetched without an advisory lock at all
|
148
148
|
break unless good_job&.executable?
|
149
149
|
|
150
|
-
|
150
|
+
good_job.perform
|
151
151
|
end
|
152
|
-
|
153
|
-
[good_job, result, error] if good_job
|
154
152
|
end
|
155
153
|
|
156
154
|
# Fetches the scheduled execution time of the next eligible Job(s).
|
157
|
-
# @
|
155
|
+
# @param after [DateTime]
|
156
|
+
# @param limit [Integer]
|
157
|
+
# @param now_limit [Integer, nil]
|
158
|
+
# @return [Array<DateTime>]
|
158
159
|
def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
|
159
160
|
query = advisory_unlocked.unfinished.schedule_ordered
|
160
161
|
|
@@ -180,7 +181,6 @@ module GoodJob
|
|
180
181
|
# @return [Job]
|
181
182
|
# The new {Job} instance representing the queued ActiveJob job.
|
182
183
|
def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
|
183
|
-
good_job = nil
|
184
184
|
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|
|
185
185
|
good_job = GoodJob::Job.new(
|
186
186
|
queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
|
@@ -194,49 +194,37 @@ module GoodJob
|
|
194
194
|
|
195
195
|
good_job.save!
|
196
196
|
active_job.provider_job_id = good_job.id
|
197
|
-
end
|
198
197
|
|
199
|
-
|
198
|
+
good_job
|
199
|
+
end
|
200
200
|
end
|
201
201
|
|
202
202
|
# Execute the ActiveJob job this {Job} represents.
|
203
|
-
# @return [
|
203
|
+
# @return [ExecutionResult]
|
204
204
|
# An array of the return value of the job's +#perform+ method and the
|
205
205
|
# exception raised by the job, if any. If the job completed successfully,
|
206
206
|
# the second array entry (the exception) will be +nil+ and vice versa.
|
207
207
|
def perform
|
208
208
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
209
209
|
|
210
|
-
GoodJob::CurrentExecution.reset
|
211
|
-
|
212
210
|
self.performed_at = Time.current
|
213
211
|
save! if GoodJob.preserve_job_records
|
214
212
|
|
215
|
-
result
|
216
|
-
|
217
|
-
result_error = nil
|
218
|
-
if result.is_a?(Exception)
|
219
|
-
result_error = result
|
220
|
-
result = nil
|
221
|
-
end
|
222
|
-
|
223
|
-
job_error = unhandled_error ||
|
224
|
-
result_error ||
|
225
|
-
GoodJob::CurrentExecution.error_on_retry ||
|
226
|
-
GoodJob::CurrentExecution.error_on_discard
|
213
|
+
result = execute
|
227
214
|
|
215
|
+
job_error = result.handled_error || result.unhandled_error
|
228
216
|
self.error = "#{job_error.class}: #{job_error.message}" if job_error
|
229
217
|
|
230
|
-
if unhandled_error && GoodJob.retry_on_unhandled_error
|
218
|
+
if result.unhandled_error && GoodJob.retry_on_unhandled_error
|
231
219
|
save!
|
232
|
-
elsif GoodJob.preserve_job_records == true || (unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
220
|
+
elsif GoodJob.preserve_job_records == true || (result.unhandled_error && GoodJob.preserve_job_records == :on_unhandled_error)
|
233
221
|
self.finished_at = Time.current
|
234
222
|
save!
|
235
223
|
else
|
236
224
|
destroy!
|
237
225
|
end
|
238
226
|
|
239
|
-
|
227
|
+
result
|
240
228
|
end
|
241
229
|
|
242
230
|
# Tests whether this job is safe to be executed by this thread.
|
@@ -247,16 +235,26 @@ module GoodJob
|
|
247
235
|
|
248
236
|
private
|
249
237
|
|
238
|
+
# @return [GoodJob::ExecutionResult]
|
250
239
|
def execute
|
251
240
|
params = serialized_params.merge(
|
252
241
|
"provider_job_id" => id
|
253
242
|
)
|
254
243
|
|
244
|
+
GoodJob::CurrentExecution.reset
|
255
245
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
256
|
-
|
246
|
+
value = ActiveJob::Base.execute(params)
|
247
|
+
|
248
|
+
if value.is_a?(Exception)
|
249
|
+
handled_error = value
|
250
|
+
value = nil
|
251
|
+
end
|
252
|
+
handled_error ||= GoodJob::CurrentExecution.error_on_retry || GoodJob::CurrentExecution.error_on_discard
|
253
|
+
|
254
|
+
ExecutionResult.new(value: value, handled_error: handled_error)
|
255
|
+
rescue StandardError => e
|
256
|
+
ExecutionResult.new(value: nil, unhandled_error: e)
|
257
257
|
end
|
258
|
-
rescue StandardError => e
|
259
|
-
[nil, e]
|
260
258
|
end
|
261
259
|
end
|
262
260
|
end
|
@@ -51,9 +51,10 @@ module GoodJob
|
|
51
51
|
end
|
52
52
|
|
53
53
|
# The Returns timestamps of when next tasks may be available.
|
54
|
-
# @param
|
55
|
-
# @param
|
56
|
-
# @
|
54
|
+
# @param after [DateTime, Time, nil] future jobs scheduled after this time
|
55
|
+
# @param limit [Integer] number of future timestamps to return
|
56
|
+
# @param now_limit [Integer] number of past timestamps to return
|
57
|
+
# @return [Array<(Time, DateTime)>, nil]
|
57
58
|
def next_at(after: nil, limit: nil, now_limit: nil)
|
58
59
|
job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
|
59
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
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module GoodJob
|
2
2
|
# Delegates the interface of a single {Scheduler} to multiple Schedulers.
|
3
3
|
class MultiScheduler
|
4
|
-
# @return [
|
4
|
+
# @return [Array<Scheduler>] List of the scheduler delegates
|
5
5
|
attr_reader :schedulers
|
6
6
|
|
7
7
|
def initialize(schedulers)
|
8
8
|
@schedulers = schedulers
|
9
9
|
end
|
10
10
|
|
11
|
-
# Delegates to {Scheduler#
|
12
|
-
def
|
13
|
-
schedulers.
|
11
|
+
# Delegates to {Scheduler#running?}.
|
12
|
+
def running?
|
13
|
+
schedulers.all?(&:running?)
|
14
14
|
end
|
15
15
|
|
16
16
|
# Delegates to {Scheduler#shutdown?}.
|
@@ -18,9 +18,14 @@ module GoodJob
|
|
18
18
|
schedulers.all?(&:shutdown?)
|
19
19
|
end
|
20
20
|
|
21
|
+
# Delegates to {Scheduler#shutdown}.
|
22
|
+
def shutdown(timeout: -1)
|
23
|
+
GoodJob._shutdown_all(schedulers, timeout: timeout)
|
24
|
+
end
|
25
|
+
|
21
26
|
# Delegates to {Scheduler#restart}.
|
22
|
-
def restart(
|
23
|
-
schedulers
|
27
|
+
def restart(timeout: -1)
|
28
|
+
GoodJob._shutdown_all(schedulers, :restart, timeout: timeout)
|
24
29
|
end
|
25
30
|
|
26
31
|
# Delegates to {Scheduler#create_thread}.
|
@@ -39,7 +44,7 @@ module GoodJob
|
|
39
44
|
|
40
45
|
if results.any?
|
41
46
|
true
|
42
|
-
elsif results.any?
|
47
|
+
elsif results.any?(false)
|
43
48
|
false
|
44
49
|
else # rubocop:disable Style/EmptyElse
|
45
50
|
nil
|