good_job 1.9.1 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
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