good_job 1.8.0 → 1.9.4

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.
@@ -24,7 +24,7 @@ module GoodJob
24
24
  end
25
25
 
26
26
  # Perform the next eligible job
27
- # @return [nil, Object] Returns job result or +nil+ if no job was found
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<(Time, Timestamp)>, nil]
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
@@ -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 = ActiveRecord::Base.connection.postgresql_version >= 120000
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
- ActiveRecord::Base.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).any?
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
- def sanitize_sql_for_conditions(*args)
249
- # Made public in Rails 5.2
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
97
- def notifier_listen(_event)
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
- # @macro notification_responder
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
- # @macro notification_responder
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
- # @macro notification_responder
122
- def notifier_unlisten(_event)
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
- # @macro notification_responder
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
 
@@ -44,7 +53,7 @@ module GoodJob
44
53
 
45
54
  if results.any?
46
55
  true
47
- elsif results.any? { |result| result == false }
56
+ elsif results.any?(false)
48
57
  false
49
58
  else # rubocop:disable Style/EmptyElse
50
59
  nil
@@ -30,13 +30,13 @@ module GoodJob # :nodoc:
30
30
  # @!attribute [r] instances
31
31
  # @!scope class
32
32
  # List of all instantiated Notifiers in the current process.
33
- # @return [Array<GoodJob:Adapter>]
33
+ # @return [Array<GoodJob::Notifier>, nil]
34
34
  cattr_reader :instances, default: [], instance_reader: false
35
35
 
36
36
  # Send a message via Postgres NOTIFY
37
37
  # @param message [#to_json]
38
38
  def self.notify(message)
39
- connection = ActiveRecord::Base.connection
39
+ connection = Job.connection
40
40
  connection.exec_query <<~SQL.squish
41
41
  NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
42
42
  SQL
@@ -64,18 +64,19 @@ module GoodJob # :nodoc:
64
64
  end
65
65
 
66
66
  # Tests whether the notifier is running.
67
+ # @!method running?
67
68
  # @return [true, false, nil]
68
69
  delegate :running?, to: :executor, allow_nil: true
69
70
 
70
71
  # Tests whether the scheduler is shutdown.
72
+ # @!method shutdown?
71
73
  # @return [true, false, nil]
72
74
  delegate :shutdown?, to: :executor, allow_nil: true
73
75
 
74
76
  # Shut down the notifier.
75
77
  # This stops the background LISTENing thread.
76
78
  # Use {#shutdown?} to determine whether threads have stopped.
77
- # @param timeout [nil, Numeric] Seconds to wait for active threads.
78
- #
79
+ # @param timeout [Numeric, nil] Seconds to wait for active threads.
79
80
  # * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
80
81
  # * +-1+, the scheduler will wait until the shutdown is complete.
81
82
  # * +0+, the scheduler will immediately shutdown and stop any threads.
@@ -159,8 +160,8 @@ module GoodJob # :nodoc:
159
160
  end
160
161
 
161
162
  def with_listen_connection
162
- ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
163
- ActiveRecord::Base.connection_pool.remove(conn)
163
+ ar_conn = Job.connection_pool.checkout.tap do |conn|
164
+ Job.connection_pool.remove(conn)
164
165
  end
165
166
  pg_conn = ar_conn.raw_connection
166
167
  raise AdapterCannotListenError unless pg_conn.respond_to? :wait_for_notify
@@ -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 [Array<GoodJob:Poller>]
19
+ # @return [Array<GoodJob::Poller>, nil]
20
20
  cattr_reader :instances, default: [], instance_reader: false
21
21
 
22
22
  # Creates GoodJob::Poller from a GoodJob::Configuration instance.
@@ -30,8 +30,8 @@ module GoodJob # :nodoc:
30
30
  # @return [Array<#call, Array(Object, Symbol)>]
31
31
  attr_reader :recipients
32
32
 
33
- # @param recipients [Array<#call, Array(Object, Symbol)>]
34
- # @param poll_interval [Hash] number of seconds between polls
33
+ # @param recipients [Array<Proc, #call, Array(Object, Symbol)>]
34
+ # @param poll_interval [Integer, nil] number of seconds between polls
35
35
  def initialize(*recipients, poll_interval: nil)
36
36
  @recipients = Concurrent::Array.new(recipients)
37
37
 
@@ -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.
@@ -73,7 +72,7 @@ module GoodJob # :nodoc:
73
72
 
74
73
  # Restart the poller.
75
74
  # When shutdown, start; or shutdown and start.
76
- # @param timeout [nil, Numeric] Seconds to wait; shares same values as {#shutdown}.
75
+ # @param timeout [Numeric, nil] Seconds to wait; shares same values as {#shutdown}.
77
76
  # @return [void]
78
77
  def restart(timeout: -1)
79
78
  shutdown(timeout: timeout) if running?
@@ -82,16 +81,21 @@ module GoodJob # :nodoc:
82
81
 
83
82
  # Invoked on completion of TimerTask task.
84
83
  # @!visibility private
84
+ # @param time [Integer]
85
+ # @param executed_task [Object, nil]
86
+ # @param thread_error [Exception, nil]
85
87
  # @return [void]
86
88
  def timer_observer(time, executed_task, thread_error)
87
89
  GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
88
- instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
90
+ ActiveSupport::Notifications.instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
89
91
  end
90
92
 
91
93
  private
92
94
 
95
+ # @return [Concurrent::TimerTask]
93
96
  attr_reader :timer
94
97
 
98
+ # @return [void]
95
99
  def create_timer
96
100
  return if @timer_options[:execution_interval] <= 0
97
101
 
@@ -30,14 +30,14 @@ module GoodJob # :nodoc:
30
30
  # @!attribute [r] instances
31
31
  # @!scope class
32
32
  # List of all instantiated Schedulers in the current process.
33
- # @return [Array<GoodJob:Scheduler>]
33
+ # @return [Array<GoodJob::Scheduler>, nil]
34
34
  cattr_reader :instances, default: [], instance_reader: false
35
35
 
36
36
  # Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
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
@@ -82,18 +82,17 @@ module GoodJob # :nodoc:
82
82
  end
83
83
 
84
84
  # Tests whether the scheduler is running.
85
- # @return [true, false, nil]
85
+ # @return [Boolean, nil]
86
86
  delegate :running?, to: :executor, allow_nil: true
87
87
 
88
88
  # Tests whether the scheduler is shutdown.
89
- # @return [true, false, nil]
89
+ # @return [Boolean, nil]
90
90
  delegate :shutdown?, to: :executor, allow_nil: true
91
91
 
92
92
  # Shut down the scheduler.
93
93
  # This stops all threads in the thread pool.
94
94
  # Use {#shutdown?} to determine whether threads have stopped.
95
- # @param timeout [nil, Numeric] Seconds to wait for actively executing jobs to finish
96
- #
95
+ # @param timeout [Numeric, nil] Seconds to wait for actively executing jobs to finish
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.
@@ -129,8 +128,8 @@ module GoodJob # :nodoc:
129
128
  end
130
129
 
131
130
  # Wakes a thread to allow the performer to execute a task.
132
- # @param state [nil, Object] Contextual information for the performer. See {JobPerformer#next?}.
133
- # @return [nil, Boolean] Whether work was started.
131
+ # @param state [Hash, nil] Contextual information for the performer. See {JobPerformer#next?}.
132
+ # @return [Boolean, nil] Whether work was started.
134
133
  #
135
134
  # * +nil+ if the scheduler is unable to take new work, for example if the thread pool is shut down or at capacity.
136
135
  # * +true+ if the performer started executing work.
@@ -187,38 +186,58 @@ module GoodJob # :nodoc:
187
186
  }
188
187
  end
189
188
 
189
+ # Preload existing runnable and future-scheduled jobs
190
+ # @return [void]
190
191
  def warm_cache
191
192
  return if @max_cache.zero?
192
193
 
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 })
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
198
208
  end
209
+ future.add_observer(observer, :call)
210
+
211
+ future.execute
199
212
  end
200
213
 
201
214
  private
202
215
 
203
216
  attr_reader :performer, :executor, :timer_set
204
217
 
218
+ # @return [void]
205
219
  def create_executor
206
220
  instrument("scheduler_create_pool", { performer_name: performer.name, max_threads: @executor_options[:max_threads] }) do
207
- @timer_set = Concurrent::TimerSet.new
221
+ @timer_set = TimerSet.new
208
222
  @executor = ThreadPoolExecutor.new(@executor_options)
209
223
  end
210
224
  end
211
225
 
226
+ # @param delay [Integer]
227
+ # @return [void]
212
228
  def create_task(delay = 0)
213
229
  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
230
+ Rails.application.executor.wrap do
231
+ thr_performer.next
232
+ end
217
233
  end
218
234
  future.add_observer(self, :task_observer)
219
235
  future.execute
220
236
  end
221
237
 
238
+ # @param name [String]
239
+ # @param payload [Hash]
240
+ # @return [void]
222
241
  def instrument(name, payload = {}, &block)
223
242
  payload = payload.reverse_merge({
224
243
  scheduler: self,
@@ -230,7 +249,7 @@ module GoodJob # :nodoc:
230
249
  end
231
250
 
232
251
  def cache_count
233
- timer_set.instance_variable_get(:@queue).length
252
+ timer_set.length
234
253
  end
235
254
 
236
255
  def remaining_cache_count
@@ -255,5 +274,21 @@ module GoodJob # :nodoc:
255
274
  end
256
275
  end
257
276
  end
277
+
278
+ # Custom sub-class of +Concurrent::TimerSet+ for additional behavior.
279
+ # @private
280
+ class TimerSet < Concurrent::TimerSet
281
+ # Number of scheduled jobs in the queue
282
+ # @return [Integer]
283
+ def length
284
+ @queue.length
285
+ end
286
+
287
+ # Clear the queue
288
+ # @return [void]
289
+ def reset
290
+ synchronize { @queue.clear }
291
+ end
292
+ end
258
293
  end
259
294
  end