delayed_job_tgmerritt 4.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +208 -0
  3. data/CONTRIBUTING.md +27 -0
  4. data/LICENSE.md +20 -0
  5. data/README.md +424 -0
  6. data/Rakefile +15 -0
  7. data/contrib/delayed_job.monitrc +14 -0
  8. data/contrib/delayed_job_multiple.monitrc +34 -0
  9. data/contrib/delayed_job_rails_4.monitrc +14 -0
  10. data/contrib/delayed_job_rails_4_multiple.monitrc +34 -0
  11. data/delayed_job.gemspec +15 -0
  12. data/lib/delayed/backend/base.rb +159 -0
  13. data/lib/delayed/backend/shared_spec.rb +657 -0
  14. data/lib/delayed/command.rb +170 -0
  15. data/lib/delayed/compatibility.rb +27 -0
  16. data/lib/delayed/deserialization_error.rb +4 -0
  17. data/lib/delayed/exceptions.rb +12 -0
  18. data/lib/delayed/lifecycle.rb +85 -0
  19. data/lib/delayed/message_sending.rb +52 -0
  20. data/lib/delayed/performable_mailer.rb +22 -0
  21. data/lib/delayed/performable_method.rb +41 -0
  22. data/lib/delayed/plugin.rb +15 -0
  23. data/lib/delayed/plugins/clear_locks.rb +15 -0
  24. data/lib/delayed/psych_ext.rb +89 -0
  25. data/lib/delayed/railtie.rb +22 -0
  26. data/lib/delayed/recipes.rb +54 -0
  27. data/lib/delayed/serialization/active_record.rb +17 -0
  28. data/lib/delayed/syck_ext.rb +42 -0
  29. data/lib/delayed/tasks.rb +39 -0
  30. data/lib/delayed/worker.rb +312 -0
  31. data/lib/delayed/yaml_ext.rb +10 -0
  32. data/lib/delayed_job.rb +22 -0
  33. data/lib/generators/delayed_job/delayed_job_generator.rb +11 -0
  34. data/lib/generators/delayed_job/templates/script +5 -0
  35. data/recipes/delayed_job.rb +1 -0
  36. data/spec/autoloaded/clazz.rb +7 -0
  37. data/spec/autoloaded/instance_clazz.rb +6 -0
  38. data/spec/autoloaded/instance_struct.rb +7 -0
  39. data/spec/autoloaded/struct.rb +8 -0
  40. data/spec/daemons.rb +2 -0
  41. data/spec/delayed/backend/test.rb +117 -0
  42. data/spec/delayed/command_spec.rb +180 -0
  43. data/spec/delayed/serialization/test.rb +0 -0
  44. data/spec/helper.rb +85 -0
  45. data/spec/lifecycle_spec.rb +75 -0
  46. data/spec/message_sending_spec.rb +122 -0
  47. data/spec/performable_mailer_spec.rb +43 -0
  48. data/spec/performable_method_spec.rb +111 -0
  49. data/spec/psych_ext_spec.rb +12 -0
  50. data/spec/sample_jobs.rb +111 -0
  51. data/spec/test_backend_spec.rb +13 -0
  52. data/spec/worker_spec.rb +175 -0
  53. data/spec/yaml_ext_spec.rb +48 -0
  54. metadata +144 -0
@@ -0,0 +1,89 @@
1
+ module Delayed
2
+ class PerformableMethod
3
+ # serialize to YAML
4
+ def encode_with(coder)
5
+ coder.map = {
6
+ 'object' => object,
7
+ 'method_name' => method_name,
8
+ 'args' => args
9
+ }
10
+ end
11
+ end
12
+ end
13
+
14
+ module Psych
15
+ def self.load_dj(yaml)
16
+ result = parse(yaml)
17
+ result ? Delayed::PsychExt::ToRuby.create.accept(result) : result
18
+ end
19
+ end
20
+
21
+ module Delayed
22
+ module PsychExt
23
+ class ToRuby < Psych::Visitors::ToRuby
24
+ unless respond_to?(:create)
25
+ def self.create
26
+ new
27
+ end
28
+ end
29
+
30
+ def visit_Psych_Nodes_Mapping(object) # rubocop:disable CyclomaticComplexity, MethodName, PerceivedComplexity
31
+ return revive(Psych.load_tags[object.tag], object) if Psych.load_tags[object.tag]
32
+
33
+ case object.tag
34
+ when /^!ruby\/object/
35
+ result = super
36
+ if defined?(ActiveRecord::Base) && result.is_a?(ActiveRecord::Base)
37
+ klass = result.class
38
+ id = result[klass.primary_key]
39
+ begin
40
+ klass.find(id)
41
+ rescue ActiveRecord::RecordNotFound => error # rubocop:disable BlockNesting
42
+ raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
43
+ end
44
+ else
45
+ result
46
+ end
47
+ when /^!ruby\/ActiveRecord:(.+)$/
48
+ klass = resolve_class(Regexp.last_match[1])
49
+ payload = Hash[*object.children.map { |c| accept c }]
50
+ id = payload['attributes'][klass.primary_key]
51
+ id = id.value if defined?(ActiveRecord::Attribute) && id.is_a?(ActiveRecord::Attribute)
52
+ begin
53
+ klass.unscoped.find(id)
54
+ rescue ActiveRecord::RecordNotFound => error
55
+ raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
56
+ end
57
+ when /^!ruby\/Mongoid:(.+)$/
58
+ klass = resolve_class(Regexp.last_match[1])
59
+ payload = Hash[*object.children.map { |c| accept c }]
60
+ id = payload['attributes']['_id']
61
+ begin
62
+ klass.find(id)
63
+ rescue Mongoid::Errors::DocumentNotFound => error
64
+ raise Delayed::DeserializationError, "Mongoid::Errors::DocumentNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
65
+ end
66
+ when /^!ruby\/DataMapper:(.+)$/
67
+ klass = resolve_class(Regexp.last_match[1])
68
+ payload = Hash[*object.children.map { |c| accept c }]
69
+ begin
70
+ primary_keys = klass.properties.select(&:key?)
71
+ key_names = primary_keys.map { |p| p.name.to_s }
72
+ klass.get!(*key_names.map { |k| payload['attributes'][k] })
73
+ rescue DataMapper::ObjectNotFoundError => error
74
+ raise Delayed::DeserializationError, "DataMapper::ObjectNotFoundError, class: #{klass} (#{error.message})"
75
+ end
76
+ else
77
+ super
78
+ end
79
+ end
80
+
81
+ def resolve_class(klass_name)
82
+ return nil if !klass_name || klass_name.empty?
83
+ klass_name.constantize
84
+ rescue
85
+ super
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,22 @@
1
+ require 'delayed_job'
2
+ require 'rails'
3
+
4
+ module Delayed
5
+ class Railtie < Rails::Railtie
6
+ initializer :after_initialize do
7
+ ActiveSupport.on_load(:action_mailer) do
8
+ ActionMailer::Base.extend(Delayed::DelayMail)
9
+ end
10
+
11
+ Delayed::Worker.logger ||= if defined?(Rails)
12
+ Rails.logger
13
+ elsif defined?(RAILS_DEFAULT_LOGGER)
14
+ RAILS_DEFAULT_LOGGER
15
+ end
16
+ end
17
+
18
+ rake_tasks do
19
+ load 'delayed/tasks.rb'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ # Capistrano Recipes for managing delayed_job
2
+ #
3
+ # Add these callbacks to have the delayed_job process restart when the server
4
+ # is restarted:
5
+ #
6
+ # after "deploy:stop", "delayed_job:stop"
7
+ # after "deploy:start", "delayed_job:start"
8
+ # after "deploy:restart", "delayed_job:restart"
9
+ #
10
+ # If you want to use command line options, for example to start multiple workers,
11
+ # define a Capistrano variable delayed_job_args:
12
+ #
13
+ # set :delayed_job_args, "-n 2"
14
+ #
15
+ # If you've got delayed_job workers running on a servers, you can also specify
16
+ # which servers have delayed_job running and should be restarted after deploy.
17
+ #
18
+ # set :delayed_job_server_role, :worker
19
+ #
20
+
21
+ Capistrano::Configuration.instance.load do
22
+ namespace :delayed_job do
23
+ def rails_env
24
+ fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
25
+ end
26
+
27
+ def args
28
+ fetch(:delayed_job_args, '')
29
+ end
30
+
31
+ def roles
32
+ fetch(:delayed_job_server_role, :app)
33
+ end
34
+
35
+ def delayed_job_command
36
+ fetch(:delayed_job_command, 'script/delayed_job')
37
+ end
38
+
39
+ desc 'Stop the delayed_job process'
40
+ task :stop, :roles => lambda { roles } do
41
+ run "cd #{current_path};#{rails_env} #{delayed_job_command} stop #{args}"
42
+ end
43
+
44
+ desc 'Start the delayed_job process'
45
+ task :start, :roles => lambda { roles } do
46
+ run "cd #{current_path};#{rails_env} #{delayed_job_command} start #{args}"
47
+ end
48
+
49
+ desc 'Restart the delayed_job process'
50
+ task :restart, :roles => lambda { roles } do
51
+ run "cd #{current_path};#{rails_env} #{delayed_job_command} restart #{args}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ if defined?(ActiveRecord)
2
+ module ActiveRecord
3
+ class Base
4
+ yaml_as 'tag:ruby.yaml.org,2002:ActiveRecord'
5
+
6
+ def self.yaml_new(klass, _tag, val)
7
+ klass.unscoped.find(val['attributes'][klass.primary_key])
8
+ rescue ActiveRecord::RecordNotFound
9
+ raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass} , primary key: #{val['attributes'][klass.primary_key]}"
10
+ end
11
+
12
+ def to_yaml_properties
13
+ ['@attributes']
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ class Module
2
+ yaml_as 'tag:ruby.yaml.org,2002:module'
3
+
4
+ def self.yaml_new(_klass, _tag, val)
5
+ val.constantize
6
+ end
7
+
8
+ def to_yaml(options = {})
9
+ YAML.quick_emit(nil, options) do |out|
10
+ out.scalar(taguri, name, :plain)
11
+ end
12
+ end
13
+
14
+ def yaml_tag_read_class(name)
15
+ # Constantize the object so that ActiveSupport can attempt
16
+ # its auto loading magic. Will raise LoadError if not successful.
17
+ name.constantize
18
+ name
19
+ end
20
+ end
21
+
22
+ class Class
23
+ yaml_as 'tag:ruby.yaml.org,2002:class'
24
+ remove_method :to_yaml if respond_to?(:to_yaml) && method(:to_yaml).owner == Class # use Module's to_yaml
25
+ end
26
+
27
+ class Struct
28
+ def self.yaml_tag_read_class(name)
29
+ # Constantize the object so that ActiveSupport can attempt
30
+ # its auto loading magic. Will raise LoadError if not successful.
31
+ name.constantize
32
+ "Struct::#{ name }"
33
+ end
34
+ end
35
+
36
+ module YAML
37
+ def load_dj(yaml)
38
+ # See https://github.com/dtao/safe_yaml
39
+ # When the method is there, we need to load our YAML like this...
40
+ respond_to?(:unsafe_load) ? load(yaml, :safe => false) : load(yaml)
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ namespace :jobs do
2
+ desc 'Clear the delayed_job queue.'
3
+ task :clear => :environment do
4
+ Delayed::Job.delete_all
5
+ end
6
+
7
+ desc 'Start a delayed_job worker.'
8
+ task :work => :environment_options do
9
+ Delayed::Worker.new(@worker_options).start
10
+ end
11
+
12
+ desc 'Start a delayed_job worker and exit when all available jobs are complete.'
13
+ task :workoff => :environment_options do
14
+ Delayed::Worker.new(@worker_options.merge(:exit_on_complete => true)).start
15
+ end
16
+
17
+ task :environment_options => :environment do
18
+ @worker_options = {
19
+ :min_priority => ENV['MIN_PRIORITY'],
20
+ :max_priority => ENV['MAX_PRIORITY'],
21
+ :queues => (ENV['QUEUES'] || ENV['QUEUE'] || '').split(','),
22
+ :quiet => false
23
+ }
24
+
25
+ @worker_options[:sleep_delay] = ENV['SLEEP_DELAY'].to_i if ENV['SLEEP_DELAY']
26
+ @worker_options[:read_ahead] = ENV['READ_AHEAD'].to_i if ENV['READ_AHEAD']
27
+ end
28
+
29
+ desc "Exit with error status if any jobs older than max_age seconds haven't been attempted yet."
30
+ task :check, [:max_age] => :environment do |_, args|
31
+ args.with_defaults(:max_age => 300)
32
+
33
+ unprocessed_jobs = Delayed::Job.where('attempts = 0 AND created_at < ?', Time.now - args[:max_age].to_i).count
34
+
35
+ if unprocessed_jobs > 0
36
+ raise "#{unprocessed_jobs} jobs older than #{args[:max_age]} seconds have not been processed yet"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,312 @@
1
+ require 'timeout'
2
+ require 'active_support/core_ext/numeric/time'
3
+ require 'active_support/core_ext/class/attribute_accessors'
4
+ require 'active_support/core_ext/kernel'
5
+ require 'active_support/core_ext/enumerable'
6
+ require 'logger'
7
+ require 'benchmark'
8
+
9
+ module Delayed
10
+ class Worker # rubocop:disable ClassLength
11
+ DEFAULT_LOG_LEVEL = 'info'
12
+ DEFAULT_SLEEP_DELAY = 5
13
+ DEFAULT_MAX_ATTEMPTS = 25
14
+ DEFAULT_MAX_RUN_TIME = 4.hours
15
+ DEFAULT_DEFAULT_PRIORITY = 0
16
+ DEFAULT_DELAY_JOBS = true
17
+ DEFAULT_QUEUES = []
18
+ DEFAULT_READ_AHEAD = 5
19
+
20
+ cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time,
21
+ :default_priority, :sleep_delay, :logger, :delay_jobs, :queues,
22
+ :read_ahead, :plugins, :destroy_failed_jobs, :exit_on_complete,
23
+ :default_log_level
24
+
25
+ # Named queue into which jobs are enqueued by default
26
+ cattr_accessor :default_queue_name
27
+
28
+ cattr_reader :backend
29
+
30
+ # name_prefix is ignored if name is set directly
31
+ attr_accessor :name_prefix
32
+
33
+ def self.reset
34
+ self.default_log_level = DEFAULT_LOG_LEVEL
35
+ self.sleep_delay = DEFAULT_SLEEP_DELAY
36
+ self.max_attempts = DEFAULT_MAX_ATTEMPTS
37
+ self.max_run_time = DEFAULT_MAX_RUN_TIME
38
+ self.default_priority = DEFAULT_DEFAULT_PRIORITY
39
+ self.delay_jobs = DEFAULT_DELAY_JOBS
40
+ self.queues = DEFAULT_QUEUES
41
+ self.read_ahead = DEFAULT_READ_AHEAD
42
+ @lifecycle = nil
43
+ end
44
+
45
+ reset
46
+
47
+ # Add or remove plugins in this list before the worker is instantiated
48
+ self.plugins = [Delayed::Plugins::ClearLocks]
49
+
50
+ # By default failed jobs are destroyed after too many attempts. If you want to keep them around
51
+ # (perhaps to inspect the reason for the failure), set this to false.
52
+ self.destroy_failed_jobs = true
53
+
54
+ # By default, Signals INT and TERM set @exit, and the worker exits upon completion of the current job.
55
+ # If you would prefer to raise a SignalException and exit immediately you can use this.
56
+ # Be aware daemons uses TERM to stop and restart
57
+ # false - No exceptions will be raised
58
+ # :term - Will only raise an exception on TERM signals but INT will wait for the current job to finish
59
+ # true - Will raise an exception on TERM and INT
60
+ cattr_accessor :raise_signal_exceptions
61
+ self.raise_signal_exceptions = false
62
+
63
+ def self.backend=(backend)
64
+ if backend.is_a? Symbol
65
+ require "delayed/serialization/#{backend}"
66
+ require "delayed/backend/#{backend}"
67
+ backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
68
+ end
69
+ @@backend = backend # rubocop:disable ClassVars
70
+ silence_warnings { ::Delayed.const_set(:Job, backend) }
71
+ end
72
+
73
+ def self.guess_backend
74
+ warn '[DEPRECATION] guess_backend is deprecated. Please remove it from your code.'
75
+ end
76
+
77
+ def self.before_fork
78
+ unless @files_to_reopen
79
+ @files_to_reopen = []
80
+ ObjectSpace.each_object(File) do |file|
81
+ @files_to_reopen << file unless file.closed?
82
+ end
83
+ end
84
+
85
+ backend.before_fork
86
+ end
87
+
88
+ def self.after_fork
89
+ # Re-open file handles
90
+ @files_to_reopen.each do |file|
91
+ begin
92
+ file.reopen file.path, 'a+'
93
+ file.sync = true
94
+ rescue ::Exception # rubocop:disable HandleExceptions, RescueException
95
+ end
96
+ end
97
+ backend.after_fork
98
+ end
99
+
100
+ def self.lifecycle
101
+ # In case a worker has not been set up, job enqueueing needs a lifecycle.
102
+ setup_lifecycle unless @lifecycle
103
+
104
+ @lifecycle
105
+ end
106
+
107
+ def self.setup_lifecycle
108
+ @lifecycle = Delayed::Lifecycle.new
109
+ plugins.each { |klass| klass.new }
110
+ end
111
+
112
+ def self.reload_app?
113
+ defined?(ActionDispatch::Reloader) && Rails.application.config.cache_classes == false
114
+ end
115
+
116
+ def initialize(options = {})
117
+ @quiet = options.key?(:quiet) ? options[:quiet] : true
118
+ @failed_reserve_count = 0
119
+
120
+ [:min_priority, :max_priority, :sleep_delay, :read_ahead, :queues, :exit_on_complete].each do |option|
121
+ self.class.send("#{option}=", options[option]) if options.key?(option)
122
+ end
123
+
124
+ # Reset lifecycle on the offhand chance that something lazily
125
+ # triggered its creation before all plugins had been registered.
126
+ self.class.setup_lifecycle
127
+ end
128
+
129
+ # Every worker has a unique name which by default is the pid of the process. There are some
130
+ # advantages to overriding this with something which survives worker restarts: Workers can
131
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
132
+ # it crashed before.
133
+ def name
134
+ return @name unless @name.nil?
135
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}" # rubocop:disable RescueModifier
136
+ end
137
+
138
+ # Sets the name of the worker.
139
+ # Setting the name to nil will reset the default worker name
140
+ attr_writer :name
141
+
142
+ def start # rubocop:disable CyclomaticComplexity, PerceivedComplexity
143
+ trap('TERM') do
144
+ Thread.new { say 'Exiting...' }
145
+ stop
146
+ raise SignalException, 'TERM' if self.class.raise_signal_exceptions
147
+ end
148
+
149
+ trap('INT') do
150
+ Thread.new { say 'Exiting...' }
151
+ stop
152
+ raise SignalException, 'INT' if self.class.raise_signal_exceptions && self.class.raise_signal_exceptions != :term
153
+ end
154
+
155
+ say 'Starting job worker'
156
+
157
+ self.class.lifecycle.run_callbacks(:execute, self) do
158
+ loop do
159
+ self.class.lifecycle.run_callbacks(:loop, self) do
160
+ @realtime = Benchmark.realtime do
161
+ @result = work_off
162
+ end
163
+ end
164
+
165
+ count = @result.sum
166
+
167
+ if count.zero?
168
+ if self.class.exit_on_complete
169
+ say 'No more jobs available. Exiting'
170
+ break
171
+ elsif !stop?
172
+ sleep(self.class.sleep_delay)
173
+ reload!
174
+ end
175
+ else
176
+ say format("#{count} jobs processed at %.4f j/s, %d failed", count / @realtime, @result.last)
177
+ end
178
+
179
+ break if stop?
180
+ end
181
+ end
182
+ end
183
+
184
+ def stop
185
+ @exit = true
186
+ end
187
+
188
+ def stop?
189
+ !!@exit
190
+ end
191
+
192
+ # Do num jobs and return stats on success/failure.
193
+ # Exit early if interrupted.
194
+ def work_off(num = 100)
195
+ success, failure = 0, 0
196
+
197
+ num.times do
198
+ case reserve_and_run_one_job
199
+ when true
200
+ success += 1
201
+ when false
202
+ failure += 1
203
+ else
204
+ break # leave if no work could be done
205
+ end
206
+ break if stop? # leave if we're exiting
207
+ end
208
+
209
+ [success, failure]
210
+ end
211
+
212
+ def run(job)
213
+ job_say job, 'RUNNING'
214
+ runtime = Benchmark.realtime do
215
+ Timeout.timeout(max_run_time(job).to_i, WorkerTimeout) { job.invoke_job }
216
+ job.destroy
217
+ end
218
+ job_say job, format('COMPLETED after %.4f', runtime)
219
+ return true # did work
220
+ rescue DeserializationError => error
221
+ job.last_error = "#{error.message}\n#{error.backtrace.join("\n")}"
222
+ failed(job)
223
+ rescue => error
224
+ self.class.lifecycle.run_callbacks(:error, self, job) { handle_failed_job(job, error) }
225
+ return false # work failed
226
+ end
227
+
228
+ # Reschedule the job in the future (when a job fails).
229
+ # Uses an exponential scale depending on the number of failed attempts.
230
+ def reschedule(job, time = nil)
231
+ if (job.attempts += 1) < max_attempts(job)
232
+ time ||= job.reschedule_at
233
+ job.run_at = time
234
+ job.unlock
235
+ job.save!
236
+ else
237
+ job_say job, "REMOVED permanently because of #{job.attempts} consecutive failures", 'error'
238
+ failed(job)
239
+ end
240
+ end
241
+
242
+ def failed(job)
243
+ self.class.lifecycle.run_callbacks(:failure, self, job) do
244
+ begin
245
+ job.hook(:failure)
246
+ rescue => error
247
+ say "Error when running failure callback: #{error}", 'error'
248
+ say error.backtrace.join("\n"), 'error'
249
+ ensure
250
+ self.class.destroy_failed_jobs ? job.destroy : job.fail!
251
+ end
252
+ end
253
+ end
254
+
255
+ def job_say(job, text, level = default_log_level)
256
+ text = "Job #{job.name} (id=#{job.id}) #{text}"
257
+ say text, level
258
+ end
259
+
260
+ def say(text, level = default_log_level)
261
+ text = "[Worker(#{name})] #{text}"
262
+ puts text unless @quiet
263
+ return unless logger
264
+ # TODO: Deprecate use of Fixnum log levels
265
+ unless level.is_a?(String)
266
+ level = Logger::Severity.constants.detect { |i| Logger::Severity.const_get(i) == level }.to_s.downcase
267
+ end
268
+ logger.send(level, "#{Time.now.strftime('%FT%T%z')}: #{text}")
269
+ end
270
+
271
+ def max_attempts(job)
272
+ job.max_attempts || self.class.max_attempts
273
+ end
274
+
275
+ def max_run_time(job)
276
+ job.max_run_time || self.class.max_run_time
277
+ end
278
+
279
+ protected
280
+
281
+ def handle_failed_job(job, error)
282
+ job.last_error = "#{error.message}\n#{error.backtrace.join("\n")}"
283
+ job_say job, "FAILED (#{job.attempts} prior attempts) with #{error.class.name}: #{error.message}", 'error'
284
+ reschedule(job)
285
+ end
286
+
287
+ # Run the next job we can get an exclusive lock on.
288
+ # If no jobs are left we return nil
289
+ def reserve_and_run_one_job
290
+ job = reserve_job
291
+ self.class.lifecycle.run_callbacks(:perform, self, job) { run(job) } if job
292
+ end
293
+
294
+ def reserve_job
295
+ job = Delayed::Job.reserve(self)
296
+ @failed_reserve_count = 0
297
+ job
298
+ rescue ::Exception => error # rubocop:disable RescueException
299
+ say "Error while reserving job: #{error}"
300
+ Delayed::Job.recover_from(error)
301
+ @failed_reserve_count += 1
302
+ raise FatalBackendError if @failed_reserve_count >= 10
303
+ nil
304
+ end
305
+
306
+ def reload!
307
+ return unless self.class.reload_app?
308
+ ActionDispatch::Reloader.cleanup!
309
+ ActionDispatch::Reloader.prepare!
310
+ end
311
+ end
312
+ end