sidekiq 4.2.4 → 5.2.10

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 (108) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +8 -1
  4. data/.gitignore +3 -0
  5. data/.travis.yml +5 -6
  6. data/5.0-Upgrade.md +56 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +220 -0
  9. data/Ent-Changes.md +94 -2
  10. data/Gemfile +12 -22
  11. data/LICENSE +1 -1
  12. data/Pro-4.0-Upgrade.md +35 -0
  13. data/Pro-Changes.md +176 -2
  14. data/README.md +10 -7
  15. data/Rakefile +3 -3
  16. data/bin/sidekiqctl +13 -92
  17. data/bin/sidekiqload +16 -34
  18. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  19. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  20. data/lib/sidekiq/api.rb +166 -68
  21. data/lib/sidekiq/cli.rb +122 -77
  22. data/lib/sidekiq/client.rb +25 -18
  23. data/lib/sidekiq/core_ext.rb +1 -106
  24. data/lib/sidekiq/ctl.rb +221 -0
  25. data/lib/sidekiq/delay.rb +42 -0
  26. data/lib/sidekiq/exception_handler.rb +2 -4
  27. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  28. data/lib/sidekiq/fetch.rb +1 -1
  29. data/lib/sidekiq/job_logger.rb +25 -0
  30. data/lib/sidekiq/job_retry.rb +262 -0
  31. data/lib/sidekiq/launcher.rb +49 -40
  32. data/lib/sidekiq/logging.rb +18 -2
  33. data/lib/sidekiq/manager.rb +6 -7
  34. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  35. data/lib/sidekiq/processor.rb +127 -37
  36. data/lib/sidekiq/rails.rb +16 -51
  37. data/lib/sidekiq/redis_connection.rb +50 -5
  38. data/lib/sidekiq/scheduled.rb +35 -8
  39. data/lib/sidekiq/testing.rb +24 -7
  40. data/lib/sidekiq/util.rb +6 -2
  41. data/lib/sidekiq/version.rb +1 -1
  42. data/lib/sidekiq/web/action.rb +3 -7
  43. data/lib/sidekiq/web/application.rb +38 -22
  44. data/lib/sidekiq/web/helpers.rb +78 -27
  45. data/lib/sidekiq/web/router.rb +14 -10
  46. data/lib/sidekiq/web.rb +4 -4
  47. data/lib/sidekiq/worker.rb +118 -19
  48. data/lib/sidekiq.rb +27 -26
  49. data/sidekiq.gemspec +8 -13
  50. data/web/assets/javascripts/application.js +0 -0
  51. data/web/assets/javascripts/dashboard.js +33 -18
  52. data/web/assets/stylesheets/application-rtl.css +246 -0
  53. data/web/assets/stylesheets/application.css +371 -6
  54. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  55. data/web/assets/stylesheets/bootstrap.css +2 -2
  56. data/web/locales/ar.yml +81 -0
  57. data/web/locales/en.yml +2 -0
  58. data/web/locales/es.yml +4 -3
  59. data/web/locales/fa.yml +80 -0
  60. data/web/locales/he.yml +79 -0
  61. data/web/locales/ja.yml +5 -3
  62. data/web/locales/ur.yml +80 -0
  63. data/web/views/_footer.erb +5 -2
  64. data/web/views/_job_info.erb +1 -1
  65. data/web/views/_nav.erb +4 -18
  66. data/web/views/_paging.erb +1 -1
  67. data/web/views/busy.erb +9 -5
  68. data/web/views/dashboard.erb +3 -3
  69. data/web/views/layout.erb +11 -2
  70. data/web/views/morgue.erb +14 -10
  71. data/web/views/queue.erb +11 -10
  72. data/web/views/queues.erb +4 -2
  73. data/web/views/retries.erb +17 -11
  74. data/web/views/retry.erb +1 -1
  75. data/web/views/scheduled.erb +2 -2
  76. metadata +32 -151
  77. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  78. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  79. data/test/config.yml +0 -9
  80. data/test/env_based_config.yml +0 -11
  81. data/test/fake_env.rb +0 -1
  82. data/test/fixtures/en.yml +0 -2
  83. data/test/helper.rb +0 -75
  84. data/test/test_actors.rb +0 -138
  85. data/test/test_api.rb +0 -528
  86. data/test/test_cli.rb +0 -418
  87. data/test/test_client.rb +0 -266
  88. data/test/test_exception_handler.rb +0 -56
  89. data/test/test_extensions.rb +0 -127
  90. data/test/test_fetch.rb +0 -50
  91. data/test/test_launcher.rb +0 -95
  92. data/test/test_logging.rb +0 -35
  93. data/test/test_manager.rb +0 -50
  94. data/test/test_middleware.rb +0 -158
  95. data/test/test_processor.rb +0 -235
  96. data/test/test_rails.rb +0 -22
  97. data/test/test_redis_connection.rb +0 -132
  98. data/test/test_retry.rb +0 -326
  99. data/test/test_retry_exhausted.rb +0 -149
  100. data/test/test_scheduled.rb +0 -115
  101. data/test/test_scheduling.rb +0 -58
  102. data/test/test_sidekiq.rb +0 -107
  103. data/test/test_testing.rb +0 -143
  104. data/test/test_testing_fake.rb +0 -357
  105. data/test/test_testing_inline.rb +0 -94
  106. data/test/test_util.rb +0 -13
  107. data/test/test_web.rb +0 -726
  108. data/test/test_web_helpers.rb +0 -54
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'sidekiq/api'
5
+
6
+ class Sidekiq::Ctl
7
+ DEFAULT_KILL_TIMEOUT = 10
8
+ CMD = File.basename($0)
9
+
10
+ attr_reader :stage, :pidfile, :kill_timeout
11
+
12
+ def self.print_usage
13
+ puts "#{CMD} - control Sidekiq from the command line."
14
+ puts
15
+ puts "Usage: #{CMD} quiet <pidfile> <kill_timeout>"
16
+ puts " #{CMD} stop <pidfile> <kill_timeout>"
17
+ puts " #{CMD} status <section>"
18
+ puts
19
+ puts " <pidfile> is path to a pidfile"
20
+ puts " <kill_timeout> is number of seconds to wait until Sidekiq exits"
21
+ puts " (default: #{Sidekiq::Ctl::DEFAULT_KILL_TIMEOUT}), after which Sidekiq will be KILL'd"
22
+ puts
23
+ puts " <section> (optional) view a specific section of the status output"
24
+ puts " Valid sections are: #{Sidekiq::Ctl::Status::VALID_SECTIONS.join(', ')}"
25
+ puts
26
+ puts "Be sure to set the kill_timeout LONGER than Sidekiq's -t timeout. If you want"
27
+ puts "to wait 60 seconds for jobs to finish, use `sidekiq -t 60` and `sidekiqctl stop"
28
+ puts " path_to_pidfile 61`"
29
+ puts
30
+ end
31
+
32
+ def initialize(stage, pidfile, timeout)
33
+ @stage = stage
34
+ @pidfile = pidfile
35
+ @kill_timeout = timeout
36
+
37
+ done('No pidfile given', :error) if !pidfile
38
+ done("Pidfile #{pidfile} does not exist", :warn) if !File.exist?(pidfile)
39
+ done('Invalid pidfile content', :error) if pid == 0
40
+
41
+ fetch_process
42
+
43
+ begin
44
+ send(stage)
45
+ rescue NoMethodError
46
+ done "Invalid command: #{stage}", :error
47
+ end
48
+ end
49
+
50
+ def fetch_process
51
+ Process.kill(0, pid)
52
+ rescue Errno::ESRCH
53
+ done "Process doesn't exist", :error
54
+ # We were not allowed to send a signal, but the process must have existed
55
+ # when Process.kill() was called.
56
+ rescue Errno::EPERM
57
+ return pid
58
+ end
59
+
60
+ def done(msg, error = nil)
61
+ puts msg
62
+ exit(exit_signal(error))
63
+ end
64
+
65
+ def exit_signal(error)
66
+ (error == :error) ? 1 : 0
67
+ end
68
+
69
+ def pid
70
+ @pid ||= File.read(pidfile).to_i
71
+ end
72
+
73
+ def quiet
74
+ `kill -TSTP #{pid}`
75
+ end
76
+
77
+ def stop
78
+ `kill -TERM #{pid}`
79
+ kill_timeout.times do
80
+ begin
81
+ Process.kill(0, pid)
82
+ rescue Errno::ESRCH
83
+ FileUtils.rm_f pidfile
84
+ done 'Sidekiq shut down gracefully.'
85
+ rescue Errno::EPERM
86
+ done 'Not permitted to shut down Sidekiq.'
87
+ end
88
+ sleep 1
89
+ end
90
+ `kill -9 #{pid}`
91
+ FileUtils.rm_f pidfile
92
+ done 'Sidekiq shut down forcefully.'
93
+ end
94
+ alias_method :shutdown, :stop
95
+
96
+ class Status
97
+ VALID_SECTIONS = %w[all version overview processes queues]
98
+ def display(section = nil)
99
+ section ||= 'all'
100
+ unless VALID_SECTIONS.include? section
101
+ puts "I don't know how to check the status of '#{section}'!"
102
+ puts "Try one of these: #{VALID_SECTIONS.join(', ')}"
103
+ return
104
+ end
105
+ send(section)
106
+ rescue StandardError => e
107
+ puts "Couldn't get status: #{e}"
108
+ end
109
+
110
+ def all
111
+ version
112
+ puts
113
+ overview
114
+ puts
115
+ processes
116
+ puts
117
+ queues
118
+ end
119
+
120
+ def version
121
+ puts "Sidekiq #{Sidekiq::VERSION}"
122
+ puts Time.now
123
+ end
124
+
125
+ def overview
126
+ puts '---- Overview ----'
127
+ puts " Processed: #{delimit stats.processed}"
128
+ puts " Failed: #{delimit stats.failed}"
129
+ puts " Busy: #{delimit stats.workers_size}"
130
+ puts " Enqueued: #{delimit stats.enqueued}"
131
+ puts " Retries: #{delimit stats.retry_size}"
132
+ puts " Scheduled: #{delimit stats.scheduled_size}"
133
+ puts " Dead: #{delimit stats.dead_size}"
134
+ end
135
+
136
+ def processes
137
+ puts "---- Processes (#{process_set.size}) ----"
138
+ process_set.each_with_index do |process, index|
139
+ puts "#{process['identity']} #{tags_for(process)}"
140
+ puts " Started: #{Time.at(process['started_at'])} (#{time_ago(process['started_at'])})"
141
+ puts " Threads: #{process['concurrency']} (#{process['busy']} busy)"
142
+ puts " Queues: #{split_multiline(process['queues'].sort, pad: 11)}"
143
+ puts '' unless (index+1) == process_set.size
144
+ end
145
+ end
146
+
147
+ COL_PAD = 2
148
+ def queues
149
+ puts "---- Queues (#{queue_data.size}) ----"
150
+ columns = {
151
+ name: [:ljust, (['name'] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
152
+ size: [:rjust, (['size'] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
153
+ latency: [:rjust, (['latency'] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
154
+ }
155
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
156
+ puts
157
+ queue_data.each do |q|
158
+ columns.each do |col, (dir, width)|
159
+ print q.send(col).public_send(dir, width)
160
+ end
161
+ puts
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def delimit(number)
168
+ number.to_s.reverse.scan(/.{1,3}/).join(',').reverse
169
+ end
170
+
171
+ def split_multiline(values, opts = {})
172
+ return 'none' unless values
173
+ pad = opts[:pad] || 0
174
+ max_length = opts[:max_length] || (80 - pad)
175
+ out = []
176
+ line = ''
177
+ values.each do |value|
178
+ if (line.length + value.length) > max_length
179
+ out << line
180
+ line = ' ' * pad
181
+ end
182
+ line << value + ', '
183
+ end
184
+ out << line[0..-3]
185
+ out.join("\n")
186
+ end
187
+
188
+ def tags_for(process)
189
+ tags = [
190
+ process['tag'],
191
+ process['labels'],
192
+ (process['quiet'] == 'true' ? 'quiet' : nil)
193
+ ].flatten.compact
194
+ tags.any? ? "[#{tags.join('] [')}]" : nil
195
+ end
196
+
197
+ def time_ago(timestamp)
198
+ seconds = Time.now - Time.at(timestamp)
199
+ return 'just now' if seconds < 60
200
+ return 'a minute ago' if seconds < 120
201
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
202
+ return 'an hour ago' if seconds < 7200
203
+ "#{seconds.floor / 60 / 60} hours ago"
204
+ end
205
+
206
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
207
+ def queue_data
208
+ @queue_data ||= Sidekiq::Queue.all.map do |q|
209
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf('%#.2f', q.latency))
210
+ end
211
+ end
212
+
213
+ def process_set
214
+ @process_set ||= Sidekiq::ProcessSet.new
215
+ end
216
+
217
+ def stats
218
+ @stats ||= Sidekiq::Stats.new
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ module Sidekiq
3
+ module Extensions
4
+
5
+ def self.enable_delay!
6
+ if defined?(::ActiveSupport)
7
+ require 'sidekiq/extensions/active_record'
8
+ require 'sidekiq/extensions/action_mailer'
9
+
10
+ # Need to patch Psych so it can autoload classes whose names are serialized
11
+ # in the delayed YAML.
12
+ Psych::Visitors::ToRuby.prepend(Sidekiq::Extensions::PsychAutoload)
13
+
14
+ ActiveSupport.on_load(:active_record) do
15
+ include Sidekiq::Extensions::ActiveRecord
16
+ end
17
+ ActiveSupport.on_load(:action_mailer) do
18
+ extend Sidekiq::Extensions::ActionMailer
19
+ end
20
+ end
21
+
22
+ require 'sidekiq/extensions/class_methods'
23
+ Module.__send__(:include, Sidekiq::Extensions::Klass)
24
+ end
25
+
26
+ module PsychAutoload
27
+ def resolve_class(klass_name)
28
+ return nil if !klass_name || klass_name.empty?
29
+ # constantize
30
+ names = klass_name.split('::')
31
+ names.shift if names.empty? || names.first.empty?
32
+
33
+ names.inject(Object) do |constant, name|
34
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
35
+ end
36
+ rescue NameError
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -7,11 +7,10 @@ module Sidekiq
7
7
  class Logger
8
8
  def call(ex, ctxHash)
9
9
  Sidekiq.logger.warn(Sidekiq.dump_json(ctxHash)) if !ctxHash.empty?
10
- Sidekiq.logger.warn "#{ex.class.name}: #{ex.message}"
11
- Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
10
+ Sidekiq.logger.warn("#{ex.class.name}: #{ex.message}")
11
+ Sidekiq.logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
12
12
  end
13
13
 
14
- # Set up default handler which just logs the error
15
14
  Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new
16
15
  end
17
16
 
@@ -26,6 +25,5 @@ module Sidekiq
26
25
  end
27
26
  end
28
27
  end
29
-
30
28
  end
31
29
  end
@@ -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
 
data/lib/sidekiq/fetch.rb CHANGED
@@ -13,7 +13,7 @@ module Sidekiq
13
13
  end
14
14
 
15
15
  def queue_name
16
- queue.sub(/.*queue:/, ''.freeze)
16
+ queue.sub(/.*queue:/, '')
17
17
  end
18
18
 
19
19
  def requeue
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Sidekiq
3
+ class JobLogger
4
+
5
+ def call(item, queue)
6
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
7
+ logger.info("start")
8
+ yield
9
+ logger.info("done: #{elapsed(start)} sec")
10
+ rescue Exception
11
+ logger.info("fail: #{elapsed(start)} sec")
12
+ raise
13
+ end
14
+
15
+ private
16
+
17
+ def elapsed(start)
18
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
19
+ end
20
+
21
+ def logger
22
+ Sidekiq.logger
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq/scheduled'
3
+ require 'sidekiq/api'
4
+
5
+ module Sidekiq
6
+ ##
7
+ # Automatically retry jobs that fail in Sidekiq.
8
+ # Sidekiq's retry support assumes a typical development lifecycle:
9
+ #
10
+ # 0. Push some code changes with a bug in it.
11
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
12
+ # the job and pushes it onto a retry queue.
13
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
14
+ # an exponential delay, the job continues to fail.
15
+ # 3. After a few days, a developer deploys a fix. The job is
16
+ # reprocessed successfully.
17
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
18
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
19
+ # manually in the Web UI.
20
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
21
+ #
22
+ # A job looks like:
23
+ #
24
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
25
+ #
26
+ # The 'retry' option also accepts a number (in place of 'true'):
27
+ #
28
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
+ #
30
+ # The job will be retried this number of times before giving up. (If simply
31
+ # 'true', Sidekiq retries 25 times)
32
+ #
33
+ # We'll add a bit more data to the job to support retries:
34
+ #
35
+ # * 'queue' - the queue to use
36
+ # * 'retry_count' - number of times we've retried so far.
37
+ # * 'error_message' - the message from the exception
38
+ # * 'error_class' - the exception class
39
+ # * 'failed_at' - the first time it failed
40
+ # * 'retried_at' - the last time it was retried
41
+ # * 'backtrace' - the number of lines of error backtrace to store
42
+ #
43
+ # We don't store the backtrace by default as that can add a lot of overhead
44
+ # to the job and everyone is using an error service, right?
45
+ #
46
+ # The default number of retries is 25 which works out to about 3 weeks
47
+ # You can change the default maximum number of retries in your initializer:
48
+ #
49
+ # Sidekiq.options[:max_retries] = 7
50
+ #
51
+ # or limit the number of retries for a particular worker with:
52
+ #
53
+ # class MyWorker
54
+ # include Sidekiq::Worker
55
+ # sidekiq_options :retry => 10
56
+ # end
57
+ #
58
+ class JobRetry
59
+ class Handled < ::RuntimeError; end
60
+ class Skip < Handled; end
61
+
62
+ include Sidekiq::Util
63
+
64
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
65
+
66
+ def initialize(options = {})
67
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
68
+ end
69
+
70
+ # The global retry handler requires only the barest of data.
71
+ # We want to be able to retry as much as possible so we don't
72
+ # require the worker to be instantiated.
73
+ def global(msg, queue)
74
+ yield
75
+ rescue Handled => ex
76
+ raise ex
77
+ rescue Sidekiq::Shutdown => ey
78
+ # ignore, will be pushed back onto queue during hard_shutdown
79
+ raise ey
80
+ rescue Exception => e
81
+ # ignore, will be pushed back onto queue during hard_shutdown
82
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
83
+
84
+ if msg['retry']
85
+ attempt_retry(nil, msg, queue, e)
86
+ else
87
+ Sidekiq.death_handlers.each do |handler|
88
+ begin
89
+ handler.call(msg, e)
90
+ rescue => handler_ex
91
+ handle_exception(handler_ex, { context: "Error calling death handler", job: msg })
92
+ end
93
+ end
94
+ end
95
+
96
+ raise Handled
97
+ end
98
+
99
+
100
+ # The local retry support means that any errors that occur within
101
+ # this block can be associated with the given worker instance.
102
+ # This is required to support the `sidekiq_retries_exhausted` block.
103
+ #
104
+ # Note that any exception from the block is wrapped in the Skip
105
+ # exception so the global block does not reprocess the error. The
106
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
107
+ # calling the handle_exception handlers.
108
+ def local(worker, msg, queue)
109
+ yield
110
+ rescue Handled => ex
111
+ raise ex
112
+ rescue Sidekiq::Shutdown => ey
113
+ # ignore, will be pushed back onto queue during hard_shutdown
114
+ raise ey
115
+ rescue Exception => e
116
+ # ignore, will be pushed back onto queue during hard_shutdown
117
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
118
+
119
+ if msg['retry'] == nil
120
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
121
+ end
122
+
123
+ raise e unless msg['retry']
124
+ attempt_retry(worker, msg, queue, e)
125
+ # We've handled this error associated with this job, don't
126
+ # need to handle it at the global level
127
+ raise Skip
128
+ end
129
+
130
+ private
131
+
132
+ # Note that +worker+ can be nil here if an error is raised before we can
133
+ # instantiate the worker instance. All access must be guarded and
134
+ # best effort.
135
+ def attempt_retry(worker, msg, queue, exception)
136
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
137
+
138
+ msg['queue'] = if msg['retry_queue']
139
+ msg['retry_queue']
140
+ else
141
+ queue
142
+ end
143
+
144
+ m = exception_message(exception)
145
+ if m.respond_to?(:scrub!)
146
+ m.force_encoding("utf-8")
147
+ m.scrub!
148
+ end
149
+
150
+ msg['error_message'] = m
151
+ msg['error_class'] = exception.class.name
152
+ count = if msg['retry_count']
153
+ msg['retried_at'] = Time.now.to_f
154
+ msg['retry_count'] += 1
155
+ else
156
+ msg['failed_at'] = Time.now.to_f
157
+ msg['retry_count'] = 0
158
+ end
159
+
160
+ if msg['backtrace'] == true
161
+ msg['error_backtrace'] = exception.backtrace
162
+ elsif !msg['backtrace']
163
+ # do nothing
164
+ elsif msg['backtrace'].to_i != 0
165
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
166
+ end
167
+
168
+ if count < max_retry_attempts
169
+ delay = delay_for(worker, count, exception)
170
+ # Logging here can break retries if the logging device raises ENOSPC #3979
171
+ #logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
172
+ retry_at = Time.now.to_f + delay
173
+ payload = Sidekiq.dump_json(msg)
174
+ Sidekiq.redis do |conn|
175
+ conn.zadd('retry', retry_at.to_s, payload)
176
+ end
177
+ else
178
+ # Goodbye dear message, you (re)tried your best I'm sure.
179
+ retries_exhausted(worker, msg, exception)
180
+ end
181
+ end
182
+
183
+ def retries_exhausted(worker, msg, exception)
184
+ begin
185
+ block = worker && worker.sidekiq_retries_exhausted_block
186
+ block.call(msg, exception) if block
187
+ rescue => e
188
+ handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
189
+ end
190
+
191
+ Sidekiq.death_handlers.each do |handler|
192
+ begin
193
+ handler.call(msg, exception)
194
+ rescue => e
195
+ handle_exception(e, { context: "Error calling death handler", job: msg })
196
+ end
197
+ end
198
+
199
+ send_to_morgue(msg) unless msg['dead'] == false
200
+ end
201
+
202
+ def send_to_morgue(msg)
203
+ logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
204
+ payload = Sidekiq.dump_json(msg)
205
+ DeadSet.new.kill(payload, notify_failure: false)
206
+ end
207
+
208
+ def retry_attempts_from(msg_retry, default)
209
+ if msg_retry.is_a?(Integer)
210
+ msg_retry
211
+ else
212
+ default
213
+ end
214
+ end
215
+
216
+ def delay_for(worker, count, exception)
217
+ if worker && worker.sidekiq_retry_in_block
218
+ custom_retry_in = retry_in(worker, count, exception).to_i
219
+ return custom_retry_in if custom_retry_in > 0
220
+ end
221
+ seconds_to_delay(count)
222
+ end
223
+
224
+ # delayed_job uses the same basic formula
225
+ def seconds_to_delay(count)
226
+ (count ** 4) + 15 + (rand(30)*(count+1))
227
+ end
228
+
229
+ def retry_in(worker, count, exception)
230
+ begin
231
+ worker.sidekiq_retry_in_block.call(count, exception)
232
+ rescue Exception => e
233
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
234
+ nil
235
+ end
236
+ end
237
+
238
+ def exception_caused_by_shutdown?(e, checked_causes = [])
239
+ return false unless e.cause
240
+
241
+ # Handle circular causes
242
+ checked_causes << e.object_id
243
+ return false if checked_causes.include?(e.cause.object_id)
244
+
245
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
246
+ exception_caused_by_shutdown?(e.cause, checked_causes)
247
+ end
248
+
249
+ # Extract message from exception.
250
+ # Set a default if the message raises an error
251
+ def exception_message(exception)
252
+ begin
253
+ # App code can stuff all sorts of crazy binary data into the error message
254
+ # that won't convert to JSON.
255
+ exception.message.to_s[0, 10_000]
256
+ rescue
257
+ "!!! ERROR MESSAGE THREW AN ERROR !!!".dup
258
+ end
259
+ end
260
+
261
+ end
262
+ end