good_job 2.7.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,10 @@ module GoodJob
18
18
  DEFAULT_MAX_CACHE = 10000
19
19
  # Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs} and {GoodJob.cleanup_preserved_jobs}
20
20
  DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60
21
+ # Default number of jobs to execute between preserved job cleanup runs
22
+ DEFAULT_CLEANUP_INTERVAL_JOBS = nil
23
+ # Default number of seconds to wait between preserved job cleanup runs
24
+ DEFAULT_CLEANUP_INTERVAL_SECONDS = nil
21
25
  # Default to always wait for jobs to finish for {Adapter#shutdown}
22
26
  DEFAULT_SHUTDOWN_TIMEOUT = -1
23
27
  # Default to not running cron
@@ -50,24 +54,22 @@ module GoodJob
50
54
  # for more details on possible values.
51
55
  # @return [Symbol]
52
56
  def execution_mode
53
- @_execution_mode ||= begin
54
- mode = if GoodJob::CLI.within_exe?
55
- :external
56
- else
57
- options[:execution_mode] ||
58
- rails_config[:execution_mode] ||
59
- env['GOOD_JOB_EXECUTION_MODE']
60
- end
61
-
62
- if mode
63
- mode.to_sym
64
- elsif Rails.env.development?
65
- :async
66
- elsif Rails.env.test?
67
- :inline
68
- else
69
- :external
70
- end
57
+ mode = if GoodJob::CLI.within_exe?
58
+ :external
59
+ else
60
+ options[:execution_mode] ||
61
+ rails_config[:execution_mode] ||
62
+ env['GOOD_JOB_EXECUTION_MODE']
63
+ end
64
+
65
+ if mode
66
+ mode.to_sym
67
+ elsif Rails.env.development?
68
+ :async
69
+ elsif Rails.env.test?
70
+ :inline
71
+ else
72
+ :external
71
73
  end
72
74
  end
73
75
 
@@ -181,6 +183,28 @@ module GoodJob
181
183
  ).to_i
182
184
  end
183
185
 
186
+ # Number of jobs a {Scheduler} will execute before cleaning up preserved jobs.
187
+ # @return [Integer, nil]
188
+ def cleanup_interval_jobs
189
+ value = (
190
+ rails_config[:cleanup_interval_jobs] ||
191
+ env['GOOD_JOB_CLEANUP_INTERVAL_JOBS'] ||
192
+ DEFAULT_CLEANUP_INTERVAL_JOBS
193
+ )
194
+ value.present? ? value.to_i : nil
195
+ end
196
+
197
+ # Number of seconds a {Scheduler} will wait before cleaning up preserved jobs.
198
+ # @return [Integer, nil]
199
+ def cleanup_interval_seconds
200
+ value = (
201
+ rails_config[:cleanup_interval_seconds] ||
202
+ env['GOOD_JOB_CLEANUP_INTERVAL_SECONDS'] ||
203
+ DEFAULT_CLEANUP_INTERVAL_SECONDS
204
+ )
205
+ value.present? ? value.to_i : nil
206
+ end
207
+
184
208
  # Tests whether to daemonize the process.
185
209
  # @return [Boolean]
186
210
  def daemonize?
@@ -21,7 +21,7 @@ module GoodJob # :nodoc:
21
21
  def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
22
22
  return if thread_error.is_a? Concurrent::CancelledOperationError
23
23
 
24
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
24
+ GoodJob._on_thread_error(thread_error) if thread_error
25
25
  end
26
26
 
27
27
  # Execution configuration to be scheduled
@@ -68,7 +68,7 @@ module GoodJob
68
68
 
69
69
  # @return [Integer] Current process ID
70
70
  def self.process_id
71
- Process.pid
71
+ ::Process.pid
72
72
  end
73
73
 
74
74
  # @return [String] Current thread name
@@ -17,7 +17,7 @@ module GoodJob
17
17
  # @return [void]
18
18
  def daemonize
19
19
  check_pid
20
- Process.daemon
20
+ ::Process.daemon
21
21
  write_pid
22
22
  end
23
23
 
@@ -25,7 +25,7 @@ module GoodJob
25
25
 
26
26
  # @return [void]
27
27
  def write_pid
28
- File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) }
28
+ File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(::Process.pid.to_s) }
29
29
  at_exit { File.delete(pidfile) if File.exist?(pidfile) }
30
30
  rescue Errno::EEXIST
31
31
  check_pid
@@ -55,7 +55,7 @@ module GoodJob
55
55
  pid = ::File.read(pidfile).to_i
56
56
  return :dead if pid.zero?
57
57
 
58
- Process.kill(0, pid) # check process status
58
+ ::Process.kill(0, pid) # check process status
59
59
  :running
60
60
  rescue Errno::ESRCH
61
61
  :dead
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # ActiveRecord model that represents an +ActiveJob+ job.
4
- # Parent class can be configured with +GoodJob.active_record_parent_class+.
5
- # @!parse
6
- # class Execution < ActiveRecord::Base; end
7
- class Execution < Object.const_get(GoodJob.active_record_parent_class)
4
+ class Execution < BaseRecord
8
5
  include Lockable
9
6
  include Filterable
10
7
 
@@ -54,16 +51,6 @@ module GoodJob
54
51
  end
55
52
  end
56
53
 
57
- def self._migration_pending_warning
58
- ActiveSupport::Deprecation.warn(<<~DEPRECATION)
59
- GoodJob has pending database migrations. To create the migration files, run:
60
- rails generate good_job:update
61
- To apply the migration files, run:
62
- rails db:migrate
63
- DEPRECATION
64
- nil
65
- end
66
-
67
54
  # Get Jobs with given ActiveJob ID
68
55
  # @!method active_job_id
69
56
  # @!scope class
@@ -224,7 +211,7 @@ module GoodJob
224
211
  if @cron_at_index
225
212
  execution_args[:cron_at] = CurrentThread.cron_at
226
213
  else
227
- _migration_pending_warning
214
+ migration_pending_warning!
228
215
  end
229
216
  elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
230
217
  execution_args[:cron_key] = CurrentThread.execution.cron_key
@@ -57,6 +57,12 @@ module GoodJob
57
57
  job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
58
58
  end
59
59
 
60
+ # Delete expired preserved jobs
61
+ # @return [void]
62
+ def cleanup
63
+ GoodJob.cleanup_preserved_jobs
64
+ end
65
+
60
66
  private
61
67
 
62
68
  attr_reader :queue_string
@@ -215,7 +215,9 @@ module GoodJob
215
215
  SQL
216
216
  end
217
217
 
218
- binds = [[nil, key]]
218
+ binds = [
219
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
220
+ ]
219
221
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
220
222
  end
221
223
 
@@ -229,7 +231,9 @@ module GoodJob
229
231
  query = <<~SQL.squish
230
232
  SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked
231
233
  SQL
232
- binds = [[nil, key]]
234
+ binds = [
235
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
236
+ ]
233
237
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
234
238
  end
235
239
 
@@ -279,7 +283,10 @@ module GoodJob
279
283
  AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
280
284
  AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
281
285
  SQL
282
- binds = [[nil, key], [nil, key]]
286
+ binds = [
287
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
288
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
289
+ ]
283
290
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
284
291
  end
285
292
 
@@ -303,7 +310,10 @@ module GoodJob
303
310
  AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
304
311
  AND pg_locks.pid = pg_backend_pid()
305
312
  SQL
306
- binds = [[nil, key], [nil, key]]
313
+ binds = [
314
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
315
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
316
+ ]
307
317
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
308
318
  end
309
319
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob # :nodoc:
4
+ class Notifier # :nodoc:
5
+ # Extends the Notifier to register the process in the database.
6
+ module ProcessRegistration
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ set_callback :listen, :after, :register_process
11
+ set_callback :unlisten, :after, :deregister_process
12
+ end
13
+
14
+ # Registers the current process.
15
+ def register_process
16
+ GoodJob::Process.with_connection(connection) do
17
+ next unless Process.migrated?
18
+
19
+ GoodJob::Process.cleanup
20
+ @process = GoodJob::Process.register
21
+ end
22
+ end
23
+
24
+ # Deregisters the current process.
25
+ def deregister_process
26
+ GoodJob::Process.with_connection(connection) do
27
+ next unless Process.migrated?
28
+
29
+ @process&.deregister
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
2
3
  require 'concurrent/atomic/atomic_boolean'
3
4
 
4
5
  module GoodJob # :nodoc:
@@ -10,6 +11,11 @@ module GoodJob # :nodoc:
10
11
  # When a message is received, the notifier passes the message to each of its recipients.
11
12
  #
12
13
  class Notifier
14
+ include ActiveSupport::Callbacks
15
+ define_callbacks :listen, :unlisten
16
+
17
+ include Notifier::ProcessRegistration
18
+
13
19
  # Raised if the Database adapter does not implement LISTEN.
14
20
  AdapterCannotListenError = Class.new(StandardError)
15
21
 
@@ -43,6 +49,12 @@ module GoodJob # :nodoc:
43
49
  # @return [Array<GoodJob::Notifier>, nil]
44
50
  cattr_reader :instances, default: [], instance_reader: false
45
51
 
52
+ # @!attribute [rw] connection
53
+ # @!scope class
54
+ # ActiveRecord Connection that has been established for the Notifier.
55
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter, nil]
56
+ thread_cattr_accessor :connection
57
+
46
58
  # Send a message via Postgres NOTIFY
47
59
  # @param message [#to_json]
48
60
  def self.notify(message)
@@ -120,7 +132,7 @@ module GoodJob # :nodoc:
120
132
  return if thread_error.is_a? AdapterCannotListenError
121
133
 
122
134
  if thread_error
123
- GoodJob.on_thread_error.call(thread_error) if GoodJob.on_thread_error.respond_to?(:call)
135
+ GoodJob._on_thread_error(thread_error)
124
136
  ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: thread_error })
125
137
 
126
138
  connection_error = CONNECTION_ERRORS.any? do |error_string|
@@ -146,30 +158,36 @@ module GoodJob # :nodoc:
146
158
 
147
159
  def listen(delay: 0)
148
160
  future = Concurrent::ScheduledTask.new(delay, args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
149
- with_listen_connection do |conn|
150
- ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
151
- conn.async_exec("LISTEN #{CHANNEL}").clear
152
- end
161
+ with_connection do
162
+ begin
163
+ run_callbacks :listen do
164
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
165
+ connection.execute("LISTEN #{CHANNEL}")
166
+ end
167
+ thr_listening.make_true
168
+ end
153
169
 
154
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
155
- thr_listening.make_true
156
- while thr_executor.running?
157
- conn.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
158
- next unless channel == CHANNEL
159
-
160
- ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
161
- parsed_payload = JSON.parse(payload, symbolize_names: true)
162
- thr_recipients.each do |recipient|
163
- target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
164
- target.send(method_name, parsed_payload)
170
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
171
+ while thr_executor.running?
172
+ wait_for_notify do |channel, payload|
173
+ next unless channel == CHANNEL
174
+
175
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
176
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
177
+ thr_recipients.each do |recipient|
178
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
179
+ target.send(method_name, parsed_payload)
180
+ end
165
181
  end
166
182
  end
167
183
  end
168
184
  end
169
185
  ensure
170
- thr_listening.make_false
171
- ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
172
- conn.async_exec("UNLISTEN *").clear
186
+ run_callbacks :unlisten do
187
+ thr_listening.make_false
188
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
189
+ connection.execute("UNLISTEN *")
190
+ end
173
191
  end
174
192
  end
175
193
  end
@@ -178,17 +196,27 @@ module GoodJob # :nodoc:
178
196
  future.execute
179
197
  end
180
198
 
181
- def with_listen_connection
182
- ar_conn = Execution.connection_pool.checkout.tap do |conn|
199
+ def with_connection
200
+ self.connection = Execution.connection_pool.checkout.tap do |conn|
183
201
  Execution.connection_pool.remove(conn)
184
202
  end
185
- pg_conn = ar_conn.raw_connection
186
- raise AdapterCannotListenError unless pg_conn.respond_to? :wait_for_notify
203
+ connection.execute("SET application_name = #{connection.quote(self.class.name)}")
187
204
 
188
- pg_conn.async_exec("SET application_name = #{pg_conn.escape_identifier(self.class.name)}").clear
189
- yield pg_conn
205
+ yield
190
206
  ensure
191
- ar_conn&.disconnect!
207
+ connection&.disconnect!
208
+ self.connection = nil
209
+ end
210
+
211
+ def wait_for_notify
212
+ raw_connection = connection.raw_connection
213
+ if raw_connection.respond_to?(:wait_for_notify)
214
+ raw_connection.wait_for_notify(WAIT_INTERVAL) do |channel, _pid, payload|
215
+ yield(channel, payload)
216
+ end
217
+ else
218
+ sleep WAIT_INTERVAL
219
+ end
192
220
  end
193
221
  end
194
222
  end
@@ -91,7 +91,7 @@ module GoodJob # :nodoc:
91
91
  # @param thread_error [Exception, nil]
92
92
  # @return [void]
93
93
  def timer_observer(time, executed_task, thread_error)
94
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
94
+ GoodJob._on_thread_error(thread_error) if thread_error
95
95
  ActiveSupport::Notifications.instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
96
96
  end
97
97
 
@@ -7,7 +7,7 @@ module GoodJob
7
7
  def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
8
8
  return if thread_error.is_a? Concurrent::CancelledOperationError
9
9
 
10
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
10
+ GoodJob._on_thread_error(thread_error) if thread_error
11
11
  end
12
12
 
13
13
  def initialize(port:)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ require 'socket'
3
+
4
+ module GoodJob # :nodoc:
5
+ # ActiveRecord model that represents an GoodJob process (either async or CLI).
6
+ class Process < BaseRecord
7
+ include AssignableConnection
8
+ include Lockable
9
+
10
+ self.table_name = 'good_job_processes'
11
+
12
+ cattr_reader :mutex, default: Mutex.new
13
+ cattr_accessor :_current_id, default: nil
14
+ cattr_accessor :_pid, default: nil
15
+
16
+ # Processes that are active and locked.
17
+ # @!method active
18
+ # @!scope class
19
+ # @return [ActiveRecord::Relation]
20
+ scope :active, -> { advisory_locked }
21
+
22
+ # Processes that are inactive and unlocked (e.g. SIGKILLed)
23
+ # @!method active
24
+ # @!scope class
25
+ # @return [ActiveRecord::Relation]
26
+ scope :inactive, -> { advisory_unlocked }
27
+
28
+ # Whether the +good_job_processes+ table exsists.
29
+ # @return [Boolean]
30
+ def self.migrated?
31
+ return true if connection.table_exists?(table_name)
32
+
33
+ migration_pending_warning!
34
+ false
35
+ end
36
+
37
+ # UUID that is unique to the current process and changes when forked.
38
+ # @return [String]
39
+ def self.current_id
40
+ mutex.synchronize do
41
+ if _current_id.nil? || _pid != ::Process.pid
42
+ self._current_id = SecureRandom.uuid
43
+ self._pid = ::Process.pid
44
+ end
45
+ _current_id
46
+ end
47
+ end
48
+
49
+ # Hash representing metadata about the current process.
50
+ # @return [Hash]
51
+ def self.current_state
52
+ {
53
+ id: current_id,
54
+ hostname: Socket.gethostname,
55
+ pid: ::Process.pid,
56
+ proctitle: $PROGRAM_NAME,
57
+ schedulers: GoodJob::Scheduler.instances.map(&:name),
58
+ }
59
+ end
60
+
61
+ # Deletes all inactive process records.
62
+ def self.cleanup
63
+ inactive.delete_all
64
+ end
65
+
66
+ # Registers the current process in the database
67
+ # @return [GoodJob::Process]
68
+ def self.register
69
+ create(id: current_id, state: current_state, create_with_advisory_lock: true)
70
+ rescue ActiveRecord::RecordNotUnique
71
+ nil
72
+ end
73
+
74
+ # Unregisters the instance.
75
+ def deregister
76
+ return unless owns_advisory_lock?
77
+
78
+ destroy!
79
+ advisory_unlock
80
+ end
81
+ end
82
+ end
@@ -7,7 +7,7 @@ module GoodJob
7
7
 
8
8
  initializer "good_job.logger" do |_app|
9
9
  ActiveSupport.on_load(:good_job) do
10
- self.logger = ::Rails.logger
10
+ self.logger = ::Rails.logger if GoodJob.logger == GoodJob::DEFAULT_LOGGER
11
11
  end
12
12
  GoodJob::LogSubscriber.attach_to :good_job
13
13
  end
@@ -22,9 +22,21 @@ module GoodJob
22
22
  end
23
23
  end
24
24
 
25
- config.after_initialize do
26
- GoodJob::Scheduler.instances.each(&:warm_cache)
27
- GoodJob::CronManager.instances.each(&:start)
25
+ initializer 'good_job.rails_config' do
26
+ config.after_initialize do
27
+ rails_config = Rails.application.config.good_job
28
+
29
+ GoodJob.logger = rails_config[:logger] if rails_config.key?(:logger)
30
+ GoodJob.on_thread_error = rails_config[:on_thread_error] if rails_config.key?(:on_thread_error)
31
+ GoodJob.preserve_job_records = rails_config[:preserve_job_records] if rails_config.key?(:preserve_job_records)
32
+ GoodJob.retry_on_unhandled_error = rails_config[:retry_on_unhandled_error] if rails_config.key?(:retry_on_unhandled_error)
33
+ end
34
+ end
35
+
36
+ initializer "good_job.start_async" do
37
+ config.after_initialize do
38
+ GoodJob::Adapter.instances.each(&:start_async)
39
+ end
28
40
  end
29
41
  end
30
42
  end
@@ -48,7 +48,9 @@ module GoodJob # :nodoc:
48
48
  job_performer,
49
49
  max_threads: max_threads,
50
50
  max_cache: configuration.max_cache,
51
- warm_cache_on_initialize: warm_cache_on_initialize
51
+ warm_cache_on_initialize: warm_cache_on_initialize,
52
+ cleanup_interval_seconds: configuration.cleanup_interval_seconds,
53
+ cleanup_interval_jobs: configuration.cleanup_interval_jobs
52
54
  )
53
55
  end
54
56
 
@@ -59,11 +61,17 @@ module GoodJob # :nodoc:
59
61
  end
60
62
  end
61
63
 
64
+ # Human readable name of the scheduler that includes configuration values.
65
+ # @return [String]
66
+ attr_reader :name
67
+
62
68
  # @param performer [GoodJob::JobPerformer]
63
69
  # @param max_threads [Numeric, nil] number of seconds between polls for jobs
64
70
  # @param max_cache [Numeric, nil] maximum number of scheduled jobs to cache in memory
65
71
  # @param warm_cache_on_initialize [Boolean] whether to warm the cache immediately, or manually by calling +warm_cache+
66
- def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: false)
72
+ # @param cleanup_interval_seconds [Numeric, nil] number of seconds between cleaning up job records
73
+ # @param cleanup_interval_jobs [Numeric, nil] number of executed jobs between cleaning up job records
74
+ def initialize(performer, max_threads: nil, max_cache: nil, warm_cache_on_initialize: false, cleanup_interval_seconds: nil, cleanup_interval_jobs: nil)
67
75
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
68
76
 
69
77
  self.class.instances << self
@@ -76,8 +84,10 @@ module GoodJob # :nodoc:
76
84
  @executor_options[:max_threads] = max_threads
77
85
  @executor_options[:max_queue] = max_threads
78
86
  end
79
- @executor_options[:name] = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@executor_options[:max_threads]})"
87
+ @name = "GoodJob::Scheduler(queues=#{@performer.name} max_threads=#{@executor_options[:max_threads]})"
88
+ @executor_options[:name] = name
80
89
 
90
+ @cleanup_tracker = CleanupTracker.new(cleanup_interval_seconds: cleanup_interval_seconds, cleanup_interval_jobs: cleanup_interval_jobs)
81
91
  create_executor
82
92
  warm_cache if warm_cache_on_initialize
83
93
  end
@@ -169,10 +179,17 @@ module GoodJob # :nodoc:
169
179
  # @return [void]
170
180
  def task_observer(time, output, thread_error)
171
181
  error = thread_error || (output.is_a?(GoodJob::ExecutionResult) ? output.unhandled_error : nil)
172
- GoodJob.on_thread_error.call(error) if error && GoodJob.on_thread_error.respond_to?(:call)
182
+ GoodJob._on_thread_error(error) if error
173
183
 
174
184
  instrument("finished_job_task", { result: output, error: thread_error, time: time })
175
- create_task if output
185
+ return unless output
186
+
187
+ @cleanup_tracker.increment
188
+ if @cleanup_tracker.cleanup?
189
+ cleanup
190
+ else
191
+ create_task
192
+ end
176
193
  end
177
194
 
178
195
  # Information about the Scheduler
@@ -206,11 +223,29 @@ module GoodJob # :nodoc:
206
223
  end
207
224
 
208
225
  observer = lambda do |_time, _output, thread_error|
209
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
226
+ GoodJob._on_thread_error(thread_error) if thread_error
210
227
  create_task # If cache-warming exhausts the threads, ensure there isn't an executable task remaining
211
228
  end
212
229
  future.add_observer(observer, :call)
230
+ future.execute
231
+ end
232
+
233
+ # Preload existing runnable and future-scheduled jobs
234
+ # @return [void]
235
+ def cleanup
236
+ @cleanup_tracker.reset
237
+
238
+ future = Concurrent::Future.new(args: [self, @performer], executor: executor) do |_thr_scheduler, thr_performer|
239
+ Rails.application.executor.wrap do
240
+ thr_performer.cleanup
241
+ end
242
+ end
213
243
 
244
+ observer = lambda do |_time, _output, thread_error|
245
+ GoodJob._on_thread_error(thread_error) if thread_error
246
+ create_task
247
+ end
248
+ future.add_observer(observer, :call)
214
249
  future.execute
215
250
  end
216
251
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.7.1'
4
+ VERSION = '2.8.0'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -18,6 +18,8 @@ require "good_job/railtie"
18
18
  #
19
19
  # +GoodJob+ is the top-level namespace and exposes configuration attributes.
20
20
  module GoodJob
21
+ DEFAULT_LOGGER = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
22
+
21
23
  # @!attribute [rw] active_record_parent_class
22
24
  # @!scope class
23
25
  # The ActiveRecord parent class inherited by +GoodJob::Execution+ (default: +ActiveRecord::Base+).
@@ -34,7 +36,7 @@ module GoodJob
34
36
  # @return [Logger, nil]
35
37
  # @example Output GoodJob logs to a file:
36
38
  # GoodJob.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
37
- mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
39
+ mattr_accessor :logger, default: DEFAULT_LOGGER
38
40
 
39
41
  # @!attribute [rw] preserve_job_records
40
42
  # @!scope class
@@ -66,6 +68,13 @@ module GoodJob
66
68
  # @return [Proc, nil]
67
69
  mattr_accessor :on_thread_error, default: nil
68
70
 
71
+ # Called with exception when a GoodJob thread raises an exception
72
+ # @param exception [Exception] Exception that was raised
73
+ # @return [void]
74
+ def self._on_thread_error(exception)
75
+ on_thread_error.call(exception) if on_thread_error.respond_to?(:call)
76
+ end
77
+
69
78
  # Stop executing jobs.
70
79
  # GoodJob does its work in pools of background threads.
71
80
  # When forking processes you should shut down these background threads before forking, and restart them after forking.
@@ -122,7 +131,7 @@ module GoodJob
122
131
  # analyze or inspect job performance.
123
132
  # If you are preserving job records this way, use this method regularly to
124
133
  # delete old records and preserve space in your database.
125
- # @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs olders than this will be deleted (default: +86400+).
134
+ # @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs older than this will be deleted (default: +86400+).
126
135
  # @return [Integer] Number of jobs that were deleted.
127
136
  def self.cleanup_preserved_jobs(older_than: nil)
128
137
  older_than ||= GoodJob::Configuration.new({}).cleanup_preserved_jobs_before_seconds_ago