sidekiq 4.2.10 → 5.0.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -10
  4. data/5.0-Upgrade.md +56 -0
  5. data/COMM-LICENSE +1 -1
  6. data/Changes.md +60 -0
  7. data/Ent-Changes.md +29 -2
  8. data/Gemfile +3 -0
  9. data/Pro-Changes.md +28 -3
  10. data/README.md +3 -3
  11. data/bin/sidekiqctl +1 -1
  12. data/bin/sidekiqload +3 -8
  13. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  14. data/lib/sidekiq.rb +6 -15
  15. data/lib/sidekiq/api.rb +77 -31
  16. data/lib/sidekiq/cli.rb +15 -6
  17. data/lib/sidekiq/client.rb +20 -13
  18. data/lib/sidekiq/core_ext.rb +1 -119
  19. data/lib/sidekiq/delay.rb +41 -0
  20. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  21. data/lib/sidekiq/job_logger.rb +24 -0
  22. data/lib/sidekiq/job_retry.rb +228 -0
  23. data/lib/sidekiq/launcher.rb +1 -7
  24. data/lib/sidekiq/logging.rb +12 -0
  25. data/lib/sidekiq/middleware/server/active_record.rb +9 -0
  26. data/lib/sidekiq/processor.rb +63 -33
  27. data/lib/sidekiq/rails.rb +1 -73
  28. data/lib/sidekiq/redis_connection.rb +14 -3
  29. data/lib/sidekiq/testing.rb +12 -3
  30. data/lib/sidekiq/util.rb +1 -2
  31. data/lib/sidekiq/version.rb +1 -1
  32. data/lib/sidekiq/web/action.rb +0 -4
  33. data/lib/sidekiq/web/application.rb +8 -13
  34. data/lib/sidekiq/web/helpers.rb +54 -16
  35. data/lib/sidekiq/worker.rb +99 -16
  36. data/sidekiq.gemspec +3 -7
  37. data/web/assets/javascripts/dashboard.js +17 -12
  38. data/web/assets/stylesheets/application-rtl.css +246 -0
  39. data/web/assets/stylesheets/application.css +336 -4
  40. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  41. data/web/locales/ar.yml +80 -0
  42. data/web/locales/fa.yml +1 -0
  43. data/web/locales/he.yml +79 -0
  44. data/web/locales/ur.yml +80 -0
  45. data/web/views/_footer.erb +2 -2
  46. data/web/views/_nav.erb +1 -1
  47. data/web/views/_paging.erb +1 -1
  48. data/web/views/busy.erb +9 -5
  49. data/web/views/dashboard.erb +1 -1
  50. data/web/views/layout.erb +10 -1
  51. data/web/views/morgue.erb +4 -4
  52. data/web/views/queue.erb +7 -7
  53. data/web/views/retries.erb +5 -5
  54. data/web/views/scheduled.erb +2 -2
  55. metadata +20 -83
  56. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  57. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -43,6 +43,10 @@ module Sidekiq
43
43
  write_pid
44
44
  end
45
45
 
46
+ def jruby?
47
+ defined?(::JRUBY_VERSION)
48
+ end
49
+
46
50
  # Code within this method is not tested because it alters
47
51
  # global process state irreversibly. PRs which improve the
48
52
  # test coverage of Sidekiq::CLI are welcomed.
@@ -51,8 +55,14 @@ module Sidekiq
51
55
  print_banner
52
56
 
53
57
  self_read, self_write = IO.pipe
58
+ sigs = %w(INT TERM TTIN TSTP)
59
+ # USR1 and USR2 don't work on the JVM
60
+ if !jruby?
61
+ sigs << 'USR1'
62
+ sigs << 'USR2'
63
+ end
54
64
 
55
- %w(INT TERM USR1 USR2 TTIN TSTP).each do |sig|
65
+ sigs.each do |sig|
56
66
  begin
57
67
  trap sig do
58
68
  self_write.puts(sig)
@@ -71,6 +81,9 @@ module Sidekiq
71
81
  ver = Sidekiq.redis_info['redis_version']
72
82
  raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
73
83
 
84
+ # cache process identity
85
+ Sidekiq.options[:identity] = identity
86
+
74
87
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
75
88
  Sidekiq.server_middleware
76
89
 
@@ -213,7 +226,6 @@ module Sidekiq
213
226
 
214
227
  opts[:strict] = true if opts[:strict].nil?
215
228
  opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
216
- opts[:identity] = identity
217
229
 
218
230
  options.merge!(opts)
219
231
  end
@@ -230,9 +242,7 @@ module Sidekiq
230
242
  if File.directory?(options[:require])
231
243
  require 'rails'
232
244
  if ::Rails::VERSION::MAJOR < 4
233
- require 'sidekiq/rails'
234
- require File.expand_path("#{options[:require]}/config/environment.rb")
235
- ::Rails.application.eager_load!
245
+ raise "Sidekiq no longer supports this version of Rails"
236
246
  elsif ::Rails::VERSION::MAJOR == 4
237
247
  # Painful contortions, see 1791 for discussion
238
248
  # No autoloading, we want to force eager load for everything.
@@ -243,7 +253,6 @@ module Sidekiq
243
253
  require 'sidekiq/rails'
244
254
  require File.expand_path("#{options[:require]}/config/environment.rb")
245
255
  else
246
- # Rails 5+ && development mode, use Reloader
247
256
  require 'sidekiq/rails'
248
257
  require File.expand_path("#{options[:require]}/config/environment.rb")
249
258
  end
@@ -48,9 +48,15 @@ module Sidekiq
48
48
  # queue - the named queue to use, default 'default'
49
49
  # class - the worker class to call, required
50
50
  # args - an array of simple arguments to the perform method, must be JSON-serializable
51
+ # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
51
52
  # retry - whether to retry this job if it fails, default true or an integer number of retries
52
53
  # backtrace - whether to save any error backtrace, default false
53
54
  #
55
+ # If class is set to the class name, the jobs' options will be based on Sidekiq's default
56
+ # worker options. Otherwise, they will be based on the job class's options.
57
+ #
58
+ # Any options valid for a worker class's sidekiq_options are also available here.
59
+ #
54
60
  # All options must be strings, not symbols. NB: because we are serializing to JSON, all
55
61
  # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
56
62
  # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
@@ -62,11 +68,11 @@ module Sidekiq
62
68
  #
63
69
  def push(item)
64
70
  normed = normalize_item(item)
65
- payload = process_single(item['class'], normed)
71
+ payload = process_single(item['class'.freeze], normed)
66
72
 
67
73
  if payload
68
74
  raw_push([payload])
69
- payload['jid']
75
+ payload['jid'.freeze]
70
76
  end
71
77
  end
72
78
 
@@ -83,19 +89,19 @@ module Sidekiq
83
89
  # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
84
90
  # than the number given if the middleware stopped processing for one or more jobs.
85
91
  def push_bulk(items)
86
- arg = items['args'].first
92
+ arg = items['args'.freeze].first
87
93
  return [] unless arg # no jobs to push
88
94
  raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
89
95
 
90
96
  normed = normalize_item(items)
91
- payloads = items['args'].map do |args|
92
- copy = normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
93
- result = process_single(items['class'], copy)
97
+ payloads = items['args'.freeze].map do |args|
98
+ copy = normed.merge('args'.freeze => args, 'jid'.freeze => SecureRandom.hex(12), 'enqueued_at'.freeze => Time.now.to_f)
99
+ result = process_single(items['class'.freeze], copy)
94
100
  result ? result : nil
95
101
  end.compact
96
102
 
97
103
  raw_push(payloads) if !payloads.empty?
98
- payloads.collect { |payload| payload['jid'] }
104
+ payloads.collect { |payload| payload['jid'.freeze] }
99
105
  end
100
106
 
101
107
  # Allows sharding of jobs across any number of Redis instances. All jobs
@@ -139,14 +145,14 @@ module Sidekiq
139
145
  # Messages are enqueued to the 'default' queue.
140
146
  #
141
147
  def enqueue(klass, *args)
142
- klass.client_push('class' => klass, 'args' => args)
148
+ klass.client_push('class'.freeze => klass, 'args'.freeze => args)
143
149
  end
144
150
 
145
151
  # Example usage:
146
152
  # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
147
153
  #
148
154
  def enqueue_to(queue, klass, *args)
149
- klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
155
+ klass.client_push('queue'.freeze => queue, 'class'.freeze => klass, 'args'.freeze => args)
150
156
  end
151
157
 
152
158
  # Example usage:
@@ -157,7 +163,7 @@ module Sidekiq
157
163
  now = Time.now.to_f
158
164
  ts = (int < 1_000_000_000 ? now + int : int)
159
165
 
160
- item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
166
+ item = { 'class'.freeze => klass, 'args'.freeze => args, 'at'.freeze => ts, 'queue'.freeze => queue }
161
167
  item.delete('at'.freeze) if ts <= now
162
168
 
163
169
  klass.client_push(item)
@@ -183,13 +189,13 @@ module Sidekiq
183
189
  end
184
190
 
185
191
  def atomic_push(conn, payloads)
186
- if payloads.first['at']
192
+ if payloads.first['at'.freeze]
187
193
  conn.zadd('schedule'.freeze, payloads.map do |hash|
188
194
  at = hash.delete('at'.freeze).to_s
189
195
  [at, Sidekiq.dump_json(hash)]
190
196
  end)
191
197
  else
192
- q = payloads.first['queue']
198
+ q = payloads.first['queue'.freeze]
193
199
  now = Time.now.to_f
194
200
  to_push = payloads.map do |entry|
195
201
  entry['enqueued_at'.freeze] = now
@@ -201,7 +207,7 @@ module Sidekiq
201
207
  end
202
208
 
203
209
  def process_single(worker_class, item)
204
- queue = item['queue']
210
+ queue = item['queue'.freeze]
205
211
 
206
212
  middleware.invoke(worker_class, item, queue, @redis_pool) do
207
213
  item
@@ -212,6 +218,7 @@ module Sidekiq
212
218
  raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.has_key?('class'.freeze) && item.has_key?('args'.freeze)
213
219
  raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
214
220
  raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'.freeze].is_a?(Class) || item['class'.freeze].is_a?(String)
221
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.has_key?('at'.freeze) && !item['at'].is_a?(Numeric)
215
222
  #raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
216
223
 
217
224
  normalized_hash(item['class'.freeze])
@@ -1,119 +1 @@
1
- # frozen_string_literal: true
2
- begin
3
- require 'active_support/core_ext/class/attribute'
4
- rescue LoadError
5
-
6
- # A dumbed down version of ActiveSupport's
7
- # Class#class_attribute helper.
8
- class Class
9
- def class_attribute(*attrs)
10
- instance_writer = true
11
-
12
- attrs.each do |name|
13
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
- def self.#{name}() nil end
15
- def self.#{name}?() !!#{name} end
16
-
17
- def self.#{name}=(val)
18
- singleton_class.class_eval do
19
- define_method(:#{name}) { val }
20
- end
21
-
22
- if singleton_class?
23
- class_eval do
24
- def #{name}
25
- defined?(@#{name}) ? @#{name} : singleton_class.#{name}
26
- end
27
- end
28
- end
29
- val
30
- end
31
-
32
- def #{name}
33
- defined?(@#{name}) ? @#{name} : self.class.#{name}
34
- end
35
-
36
- def #{name}?
37
- !!#{name}
38
- end
39
- RUBY
40
-
41
- attr_writer name if instance_writer
42
- end
43
- end
44
-
45
- private
46
- def singleton_class?
47
- ancestors.first != self
48
- end
49
- end
50
- end
51
-
52
- begin
53
- require 'active_support/core_ext/hash/keys'
54
- require 'active_support/core_ext/hash/deep_merge'
55
- rescue LoadError
56
- class Hash
57
- def stringify_keys
58
- keys.each do |key|
59
- self[key.to_s] = delete(key)
60
- end
61
- self
62
- end if !{}.respond_to?(:stringify_keys)
63
-
64
- def symbolize_keys
65
- keys.each do |key|
66
- self[(key.to_sym rescue key) || key] = delete(key)
67
- end
68
- self
69
- end if !{}.respond_to?(:symbolize_keys)
70
-
71
- def deep_merge(other_hash, &block)
72
- dup.deep_merge!(other_hash, &block)
73
- end if !{}.respond_to?(:deep_merge)
74
-
75
- def deep_merge!(other_hash, &block)
76
- other_hash.each_pair do |k,v|
77
- tv = self[k]
78
- if tv.is_a?(Hash) && v.is_a?(Hash)
79
- self[k] = tv.deep_merge(v, &block)
80
- else
81
- self[k] = block && tv ? block.call(k, tv, v) : v
82
- end
83
- end
84
- self
85
- end if !{}.respond_to?(:deep_merge!)
86
- end
87
- end
88
-
89
- begin
90
- require 'active_support/core_ext/string/inflections'
91
- rescue LoadError
92
- class String
93
- def constantize
94
- names = self.split('::')
95
- names.shift if names.empty? || names.first.empty?
96
-
97
- constant = Object
98
- names.each do |name|
99
- constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
100
- end
101
- constant
102
- end
103
- end if !"".respond_to?(:constantize)
104
- end
105
-
106
-
107
- begin
108
- require 'active_support/core_ext/kernel/reporting'
109
- rescue LoadError
110
- module Kernel
111
- module_function
112
- def silence_warnings
113
- old_verbose, $VERBOSE = $VERBOSE, nil
114
- yield
115
- ensure
116
- $VERBOSE = old_verbose
117
- end
118
- end
119
- end
1
+ raise "no longer used, will be removed in 5.1"
@@ -0,0 +1,41 @@
1
+ module Sidekiq
2
+ module Extensions
3
+
4
+ def self.enable_delay!
5
+ if defined?(::ActiveSupport)
6
+ require 'sidekiq/extensions/active_record'
7
+ require 'sidekiq/extensions/action_mailer'
8
+
9
+ # Need to patch Psych so it can autoload classes whose names are serialized
10
+ # in the delayed YAML.
11
+ Psych::Visitors::ToRuby.prepend(Sidekiq::Extensions::PsychAutoload)
12
+
13
+ ActiveSupport.on_load(:active_record) do
14
+ include Sidekiq::Extensions::ActiveRecord
15
+ end
16
+ ActiveSupport.on_load(:action_mailer) do
17
+ extend Sidekiq::Extensions::ActionMailer
18
+ end
19
+ end
20
+
21
+ require 'sidekiq/extensions/class_methods'
22
+ Module.__send__(:include, Sidekiq::Extensions::Klass)
23
+ end
24
+
25
+ module PsychAutoload
26
+ def resolve_class(klass_name)
27
+ return nil if !klass_name || klass_name.empty?
28
+ # constantize
29
+ names = klass_name.split('::')
30
+ names.shift if names.empty? || names.first.empty?
31
+
32
+ names.inject(Object) do |constant, name|
33
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
34
+ end
35
+ rescue NameError
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -3,6 +3,8 @@ require 'yaml'
3
3
 
4
4
  module Sidekiq
5
5
  module Extensions
6
+ SIZE_LIMIT = 8_192
7
+
6
8
  class Proxy < BasicObject
7
9
  def initialize(performable, target, options={})
8
10
  @performable = performable
@@ -17,7 +19,11 @@ module Sidekiq
17
19
  # to JSON and then deserialized on the other side back into a
18
20
  # Ruby object.
19
21
  obj = [@target, name, args]
20
- @performable.client_push({ 'class' => @performable, 'args' => [::YAML.dump(obj)] }.merge(@opts))
22
+ marshalled = ::YAML.dump(obj)
23
+ if marshalled.size > SIZE_LIMIT
24
+ ::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
25
+ end
26
+ @performable.client_push({ 'class' => @performable, 'args' => [marshalled] }.merge(@opts))
21
27
  end
22
28
  end
23
29
 
@@ -0,0 +1,24 @@
1
+ module Sidekiq
2
+ class JobLogger
3
+
4
+ def call(item, queue)
5
+ start = Time.now
6
+ logger.info("start".freeze)
7
+ yield
8
+ logger.info("done: #{elapsed(start)} sec")
9
+ rescue Exception
10
+ logger.info("fail: #{elapsed(start)} sec")
11
+ raise
12
+ end
13
+
14
+ private
15
+
16
+ def elapsed(start)
17
+ (Time.now - start).round(3)
18
+ end
19
+
20
+ def logger
21
+ Sidekiq.logger
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,228 @@
1
+ require 'sidekiq/scheduled'
2
+ require 'sidekiq/api'
3
+
4
+ module Sidekiq
5
+ ##
6
+ # Automatically retry jobs that fail in Sidekiq.
7
+ # Sidekiq's retry support assumes a typical development lifecycle:
8
+ #
9
+ # 0. Push some code changes with a bug in it.
10
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
11
+ # the job and pushes it onto a retry queue.
12
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
13
+ # an exponential delay, the job continues to fail.
14
+ # 3. After a few days, a developer deploys a fix. The job is
15
+ # reprocessed successfully.
16
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
17
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
18
+ # manually in the Web UI.
19
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
20
+ #
21
+ # A job looks like:
22
+ #
23
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
24
+ #
25
+ # The 'retry' option also accepts a number (in place of 'true'):
26
+ #
27
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
28
+ #
29
+ # The job will be retried this number of times before giving up. (If simply
30
+ # 'true', Sidekiq retries 25 times)
31
+ #
32
+ # We'll add a bit more data to the job to support retries:
33
+ #
34
+ # * 'queue' - the queue to use
35
+ # * 'retry_count' - number of times we've retried so far.
36
+ # * 'error_message' - the message from the exception
37
+ # * 'error_class' - the exception class
38
+ # * 'failed_at' - the first time it failed
39
+ # * 'retried_at' - the last time it was retried
40
+ # * 'backtrace' - the number of lines of error backtrace to store
41
+ #
42
+ # We don't store the backtrace by default as that can add a lot of overhead
43
+ # to the job and everyone is using an error service, right?
44
+ #
45
+ # The default number of retries is 25 which works out to about 3 weeks
46
+ # You can change the default maximum number of retries in your initializer:
47
+ #
48
+ # Sidekiq.options[:max_retries] = 7
49
+ #
50
+ # or limit the number of retries for a particular worker with:
51
+ #
52
+ # class MyWorker
53
+ # include Sidekiq::Worker
54
+ # sidekiq_options :retry => 10
55
+ # end
56
+ #
57
+ class JobRetry
58
+ class Skip < ::RuntimeError; end
59
+
60
+ include Sidekiq::Util
61
+
62
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
63
+
64
+ def initialize(options = {})
65
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
66
+ end
67
+
68
+ # The global retry handler requires only the barest of data.
69
+ # We want to be able to retry as much as possible so we don't
70
+ # require the worker to be instantiated.
71
+ def global(msg, queue)
72
+ yield
73
+ rescue Skip => ex
74
+ raise ex
75
+ rescue Sidekiq::Shutdown => ey
76
+ # ignore, will be pushed back onto queue during hard_shutdown
77
+ raise ey
78
+ rescue Exception => e
79
+ # ignore, will be pushed back onto queue during hard_shutdown
80
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
81
+
82
+ raise e unless msg['retry']
83
+ attempt_retry(nil, msg, queue, e)
84
+ raise e
85
+ end
86
+
87
+
88
+ # The local retry support means that any errors that occur within
89
+ # this block can be associated with the given worker instance.
90
+ # This is required to support the `sidekiq_retries_exhausted` block.
91
+ #
92
+ # Note that any exception from the block is wrapped in the Skip
93
+ # exception so the global block does not reprocess the error. The
94
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
95
+ # calling the handle_exception handlers.
96
+ def local(worker, msg, queue)
97
+ yield
98
+ rescue Skip => ex
99
+ raise ex
100
+ rescue Sidekiq::Shutdown => ey
101
+ # ignore, will be pushed back onto queue during hard_shutdown
102
+ raise ey
103
+ rescue Exception => e
104
+ # ignore, will be pushed back onto queue during hard_shutdown
105
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
106
+
107
+ if msg['retry'] == nil
108
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
109
+ end
110
+
111
+ raise e unless msg['retry']
112
+ attempt_retry(worker, msg, queue, e)
113
+ # We've handled this error associated with this job, don't
114
+ # need to handle it at the global level
115
+ raise Skip
116
+ end
117
+
118
+ private
119
+
120
+ # Note that +worker+ can be nil here if an error is raised before we can
121
+ # instantiate the worker instance. All access must be guarded and
122
+ # best effort.
123
+ def attempt_retry(worker, msg, queue, exception)
124
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
125
+
126
+ msg['queue'] = if msg['retry_queue']
127
+ msg['retry_queue']
128
+ else
129
+ queue
130
+ end
131
+
132
+ # App code can stuff all sorts of crazy binary data into the error message
133
+ # that won't convert to JSON.
134
+ m = exception.message.to_s[0, 10_000]
135
+ if m.respond_to?(:scrub!)
136
+ m.force_encoding("utf-8")
137
+ m.scrub!
138
+ end
139
+
140
+ msg['error_message'] = m
141
+ msg['error_class'] = exception.class.name
142
+ count = if msg['retry_count']
143
+ msg['retried_at'] = Time.now.to_f
144
+ msg['retry_count'] += 1
145
+ else
146
+ msg['failed_at'] = Time.now.to_f
147
+ msg['retry_count'] = 0
148
+ end
149
+
150
+ if msg['backtrace'] == true
151
+ msg['error_backtrace'] = exception.backtrace
152
+ elsif !msg['backtrace']
153
+ # do nothing
154
+ elsif msg['backtrace'].to_i != 0
155
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
156
+ end
157
+
158
+ if count < max_retry_attempts
159
+ delay = delay_for(worker, count, exception)
160
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
161
+ retry_at = Time.now.to_f + delay
162
+ payload = Sidekiq.dump_json(msg)
163
+ Sidekiq.redis do |conn|
164
+ conn.zadd('retry', retry_at.to_s, payload)
165
+ end
166
+ else
167
+ # Goodbye dear message, you (re)tried your best I'm sure.
168
+ retries_exhausted(worker, msg, exception)
169
+ end
170
+ end
171
+
172
+ def retries_exhausted(worker, msg, exception)
173
+ logger.debug { "Retries exhausted for job" }
174
+ begin
175
+ block = worker && worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
176
+ block.call(msg, exception) if block
177
+ rescue => e
178
+ handle_exception(e, { context: "Error calling retries_exhausted for #{msg['class']}", job: msg })
179
+ end
180
+
181
+ send_to_morgue(msg) unless msg['dead'] == false
182
+ end
183
+
184
+ def send_to_morgue(msg)
185
+ Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
186
+ payload = Sidekiq.dump_json(msg)
187
+ DeadSet.new.kill(payload)
188
+ end
189
+
190
+ def retry_attempts_from(msg_retry, default)
191
+ if msg_retry.is_a?(Integer)
192
+ msg_retry
193
+ else
194
+ default
195
+ end
196
+ end
197
+
198
+ def delay_for(worker, count, exception)
199
+ worker && worker.sidekiq_retry_in_block && retry_in(worker, count, exception) || seconds_to_delay(count)
200
+ end
201
+
202
+ # delayed_job uses the same basic formula
203
+ def seconds_to_delay(count)
204
+ (count ** 4) + 15 + (rand(30)*(count+1))
205
+ end
206
+
207
+ def retry_in(worker, count, exception)
208
+ begin
209
+ worker.sidekiq_retry_in_block.call(count, exception).to_i
210
+ rescue Exception => e
211
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
212
+ nil
213
+ end
214
+ end
215
+
216
+ def exception_caused_by_shutdown?(e, checked_causes = [])
217
+ return false unless e.cause
218
+
219
+ # Handle circular causes
220
+ checked_causes << e.object_id
221
+ return false if checked_causes.include?(e.cause.object_id)
222
+
223
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
224
+ exception_caused_by_shutdown?(e.cause, checked_causes)
225
+ end
226
+
227
+ end
228
+ end