good_job 1.9.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00e281a3f0c203b1401da29f633584c6bb1bb050139413c1c3762fddce8d0555
4
- data.tar.gz: c7d9e2e3e2e401d0d8872f66890be0901b33cb8d774860d4885b334d215248e8
3
+ metadata.gz: 94f195fb2a3c51544d58216d662caaf7fac989ec7cc5eb1fcb57b25a5e303a9d
4
+ data.tar.gz: 20a2b3ffcc844df543809df6bc143cdc1f42885bea28ffb8ec33c02bdf5e7d5b
5
5
  SHA512:
6
- metadata.gz: d3aa584ac5c42dfeae93596168195652a7d030c38e197467204ef1c1a03f5ff4187d9c3d45e5549d4c1eae739ec5859e0a664dc74c1d1a685fc8547357682f27
7
- data.tar.gz: e420f7e40d16ef19f3392da7fd249887e94b805af976a740f76a237131dd2afcc05b4e29c616b63738d44070470a0c32ceefca808707afeab8fc7e6543d3b623
6
+ metadata.gz: b2bb0c8fab29421100400e55959ac2ff95d63e0eec7876277b61d4913eb7f045e660d68ad4638d729e0cb42bbce4f18aea13a3c9e9daffd489659b72404f8d02
7
+ data.tar.gz: 2d8a691a8dae96a8154faa3b86e14a0fd25fd3fedaa35a3e5cedb69ac27bc1926eae4a84fcd9dc73f7f91e15eea9295be929346d711651d656c1425adf96b635
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.9.2](https://github.com/bensheldon/good_job/tree/v1.9.2) (2021-05-10)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.9.1...v1.9.2)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Run Scheduler\#warm\_cache operation in threadpool executor [\#242](https://github.com/bensheldon/good_job/pull/242) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ **Closed issues:**
12
+
13
+ - Jobs not visible in dashboard [\#245](https://github.com/bensheldon/good_job/issues/245)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Use GoodJob::Job::ExecutionResult object instead of job execution returning an ordered array [\#241](https://github.com/bensheldon/good_job/pull/241) ([bensheldon](https://github.com/bensheldon))
18
+ - Update development dependencies [\#240](https://github.com/bensheldon/good_job/pull/240) ([bensheldon](https://github.com/bensheldon))
19
+
3
20
  ## [v1.9.1](https://github.com/bensheldon/good_job/tree/v1.9.1) (2021-04-19)
4
21
 
5
22
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.9.0...v1.9.1)
data/lib/good_job.rb CHANGED
@@ -85,6 +85,11 @@ module GoodJob
85
85
  # When forking processes you should shut down these background threads before forking, and restart them after forking.
86
86
  # For example, you should use +shutdown+ and +restart+ when using async execution mode with Puma.
87
87
  # See the {file:README.md#executing-jobs-async--in-process} for more explanation and examples.
88
+ # @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish
89
+ # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
90
+ # * +-1+, the scheduler will wait until the shutdown is complete.
91
+ # * +0+, the scheduler will immediately shutdown and stop any active tasks.
92
+ # * +1..+, the scheduler will wait that many seconds before stopping any remaining active tasks.
88
93
  # @param wait [Boolean] whether to wait for shutdown
89
94
  # @return [void]
90
95
  def self.shutdown(timeout: -1, wait: nil)
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
 
@@ -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,7 +1,8 @@
1
1
  module GoodJob
2
- #
3
- # Represents a request to perform an +ActiveJob+ job.
4
- #
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
5
6
  class Job < Object.const_get(GoodJob.active_record_parent_class)
6
7
  include Lockable
7
8
 
@@ -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.
@@ -134,29 +136,26 @@ module GoodJob
134
136
 
135
137
  # Finds the next eligible Job, acquire an advisory lock related to it, and
136
138
  # executes the job.
137
- # @return [Array<(GoodJob::Job, Object, Exception)>, nil]
139
+ # @return [ExecutionResult, nil]
138
140
  # If a job was executed, returns an array with the {Job} record, the
139
141
  # return value for the job's +#perform+ method, and the exception the job
140
142
  # raised, if any (if the job raised, then the second array entry will be
141
143
  # +nil+). If there were no jobs to execute, returns +nil+.
142
144
  def self.perform_with_advisory_lock
143
- good_job = nil
144
- result = nil
145
- error = nil
146
-
147
145
  unfinished.priority_ordered.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
148
146
  good_job = good_jobs.first
149
147
  # TODO: Determine why some records are fetched without an advisory lock at all
150
148
  break unless good_job&.executable?
151
149
 
152
- result, error = good_job.perform
150
+ good_job.perform
153
151
  end
154
-
155
- [good_job, result, error] if good_job
156
152
  end
157
153
 
158
154
  # Fetches the scheduled execution time of the next eligible Job(s).
159
- # @return [Array<(DateTime)>]
155
+ # @param after [DateTime]
156
+ # @param limit [Integer]
157
+ # @param now_limit [Integer, nil]
158
+ # @return [Array<DateTime>]
160
159
  def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil)
161
160
  query = advisory_unlocked.unfinished.schedule_ordered
162
161
 
@@ -182,7 +181,6 @@ module GoodJob
182
181
  # @return [Job]
183
182
  # The new {Job} instance representing the queued ActiveJob job.
184
183
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
185
- good_job = nil
186
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|
187
185
  good_job = GoodJob::Job.new(
188
186
  queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
@@ -196,49 +194,37 @@ module GoodJob
196
194
 
197
195
  good_job.save!
198
196
  active_job.provider_job_id = good_job.id
199
- end
200
197
 
201
- good_job
198
+ good_job
199
+ end
202
200
  end
203
201
 
204
202
  # Execute the ActiveJob job this {Job} represents.
205
- # @return [Array<(Object, Exception)>]
203
+ # @return [ExecutionResult]
206
204
  # An array of the return value of the job's +#perform+ method and the
207
205
  # exception raised by the job, if any. If the job completed successfully,
208
206
  # the second array entry (the exception) will be +nil+ and vice versa.
209
207
  def perform
210
208
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
211
209
 
212
- GoodJob::CurrentExecution.reset
213
-
214
210
  self.performed_at = Time.current
215
211
  save! if GoodJob.preserve_job_records
216
212
 
217
- result, unhandled_error = execute
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
213
+ result = execute
229
214
 
215
+ job_error = result.handled_error || result.unhandled_error
230
216
  self.error = "#{job_error.class}: #{job_error.message}" if job_error
231
217
 
232
- if unhandled_error && GoodJob.retry_on_unhandled_error
218
+ if result.unhandled_error && GoodJob.retry_on_unhandled_error
233
219
  save!
234
- 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)
235
221
  self.finished_at = Time.current
236
222
  save!
237
223
  else
238
224
  destroy!
239
225
  end
240
226
 
241
- [result, job_error]
227
+ result
242
228
  end
243
229
 
244
230
  # Tests whether this job is safe to be executed by this thread.
@@ -249,16 +235,26 @@ module GoodJob
249
235
 
250
236
  private
251
237
 
238
+ # @return [GoodJob::ExecutionResult]
252
239
  def execute
253
240
  params = serialized_params.merge(
254
241
  "provider_job_id" => id
255
242
  )
256
243
 
244
+ GoodJob::CurrentExecution.reset
257
245
  ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
258
- [ActiveJob::Base.execute(params), nil]
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)
259
257
  end
260
- rescue StandardError => e
261
- [nil, e]
262
258
  end
263
259
  end
264
260
  end
@@ -75,7 +75,6 @@ module GoodJob # :nodoc:
75
75
  # This stops the background LISTENing thread.
76
76
  # Use {#shutdown?} to determine whether threads have stopped.
77
77
  # @param timeout [nil, Numeric] Seconds to wait for active threads.
78
- #
79
78
  # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
80
79
  # * +-1+, the scheduler will wait until the shutdown is complete.
81
80
  # * +0+, the scheduler will immediately shutdown and stop any threads.
@@ -54,7 +54,6 @@ module GoodJob # :nodoc:
54
54
  # Shut down the notifier.
55
55
  # Use {#shutdown?} to determine whether threads have stopped.
56
56
  # @param timeout [nil, Numeric] Seconds to wait for active threads.
57
- #
58
57
  # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
59
58
  # * +-1+, the scheduler will wait until the shutdown is complete.
60
59
  # * +0+, the scheduler will immediately shutdown and stop any threads.
@@ -37,7 +37,7 @@ module GoodJob # :nodoc:
37
37
  # @param configuration [GoodJob::Configuration]
38
38
  # @param warm_cache_on_initialize [Boolean]
39
39
  # @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
40
- def self.from_configuration(configuration, warm_cache_on_initialize: true)
40
+ def self.from_configuration(configuration, warm_cache_on_initialize: false)
41
41
  schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
42
42
  queue_string, max_threads = queue_string_and_max_threads.split(':')
43
43
  max_threads = (max_threads || configuration.max_threads).to_i
@@ -61,8 +61,8 @@ module GoodJob # :nodoc:
61
61
  # @param performer [GoodJob::JobPerformer]
62
62
  # @param max_threads [Numeric, nil] number of seconds between polls for jobs
63
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)
64
+ # @param warm_cache_on_initialize [Boolean] whether to warm the cache immediately, or manually by calling +warm_cache+
65
+ def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: false)
66
66
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
67
67
 
68
68
  self.class.instances << self
@@ -93,7 +93,6 @@ module GoodJob # :nodoc:
93
93
  # This stops all threads in the thread pool.
94
94
  # Use {#shutdown?} to determine whether threads have stopped.
95
95
  # @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish
96
- #
97
96
  # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
98
97
  # * +-1+, the scheduler will wait until the shutdown is complete.
99
98
  # * +0+, the scheduler will immediately shutdown and stop any active tasks.
@@ -192,12 +191,24 @@ module GoodJob # :nodoc:
192
191
  def warm_cache
193
192
  return if @max_cache.zero?
194
193
 
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 })
194
+ future = Concurrent::Future.new(args: [self, @performer], executor: executor) do |thr_scheduler, thr_performer|
195
+ Rails.application.executor.wrap do
196
+ thr_performer.next_at(
197
+ limit: @max_cache,
198
+ now_limit: @executor_options[:max_threads]
199
+ ).each do |scheduled_at|
200
+ thr_scheduler.create_thread({ scheduled_at: scheduled_at })
201
+ end
202
+ end
203
+ end
204
+
205
+ observer = lambda do |_time, _output, thread_error|
206
+ GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
207
+ create_task # If cache-warming exhausts the threads, ensure there isn't an executable task remaining
200
208
  end
209
+ future.add_observer(observer, :call)
210
+
211
+ future.execute
201
212
  end
202
213
 
203
214
  private
@@ -213,9 +224,9 @@ module GoodJob # :nodoc:
213
224
 
214
225
  def create_task(delay = 0)
215
226
  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
227
+ Rails.application.executor.wrap do
228
+ thr_performer.next
229
+ end
219
230
  end
220
231
  future.add_observer(self, :task_observer)
221
232
  future.execute
@@ -1,4 +1,4 @@
1
1
  module GoodJob
2
2
  # GoodJob gem version.
3
- VERSION = '1.9.1'.freeze
3
+ VERSION = '1.9.2'.freeze
4
4
  end
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.9.1
4
+ version: 1.9.2
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-04-19 00:00:00.000000000 Z
11
+ date: 2021-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -344,6 +344,7 @@ files:
344
344
  - lib/good_job/configuration.rb
345
345
  - lib/good_job/current_execution.rb
346
346
  - lib/good_job/daemon.rb
347
+ - lib/good_job/execution_result.rb
347
348
  - lib/good_job/job.rb
348
349
  - lib/good_job/job_performer.rb
349
350
  - lib/good_job/lockable.rb