sidekiq 6.0.0

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.github/contributing.md +32 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +13 -0
  6. data/.standard.yml +20 -0
  7. data/3.0-Upgrade.md +70 -0
  8. data/4.0-Upgrade.md +53 -0
  9. data/5.0-Upgrade.md +56 -0
  10. data/6.0-Upgrade.md +70 -0
  11. data/COMM-LICENSE +97 -0
  12. data/Changes.md +1570 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +250 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +196 -0
  17. data/LICENSE +9 -0
  18. data/Pro-2.0-Upgrade.md +138 -0
  19. data/Pro-3.0-Upgrade.md +44 -0
  20. data/Pro-4.0-Upgrade.md +35 -0
  21. data/Pro-5.0-Upgrade.md +25 -0
  22. data/Pro-Changes.md +768 -0
  23. data/README.md +95 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +18 -0
  26. data/bin/sidekiqload +153 -0
  27. data/bin/sidekiqmon +9 -0
  28. data/code_of_conduct.md +50 -0
  29. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  30. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  31. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  32. data/lib/generators/sidekiq/worker_generator.rb +47 -0
  33. data/lib/sidekiq.rb +248 -0
  34. data/lib/sidekiq/api.rb +927 -0
  35. data/lib/sidekiq/cli.rb +380 -0
  36. data/lib/sidekiq/client.rb +242 -0
  37. data/lib/sidekiq/delay.rb +41 -0
  38. data/lib/sidekiq/exception_handler.rb +27 -0
  39. data/lib/sidekiq/extensions/action_mailer.rb +47 -0
  40. data/lib/sidekiq/extensions/active_record.rb +42 -0
  41. data/lib/sidekiq/extensions/class_methods.rb +42 -0
  42. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  43. data/lib/sidekiq/fetch.rb +80 -0
  44. data/lib/sidekiq/job_logger.rb +55 -0
  45. data/lib/sidekiq/job_retry.rb +249 -0
  46. data/lib/sidekiq/launcher.rb +181 -0
  47. data/lib/sidekiq/logger.rb +69 -0
  48. data/lib/sidekiq/manager.rb +135 -0
  49. data/lib/sidekiq/middleware/chain.rb +151 -0
  50. data/lib/sidekiq/middleware/i18n.rb +40 -0
  51. data/lib/sidekiq/monitor.rb +148 -0
  52. data/lib/sidekiq/paginator.rb +42 -0
  53. data/lib/sidekiq/processor.rb +282 -0
  54. data/lib/sidekiq/rails.rb +52 -0
  55. data/lib/sidekiq/redis_connection.rb +138 -0
  56. data/lib/sidekiq/scheduled.rb +172 -0
  57. data/lib/sidekiq/testing.rb +332 -0
  58. data/lib/sidekiq/testing/inline.rb +30 -0
  59. data/lib/sidekiq/util.rb +69 -0
  60. data/lib/sidekiq/version.rb +5 -0
  61. data/lib/sidekiq/web.rb +205 -0
  62. data/lib/sidekiq/web/action.rb +93 -0
  63. data/lib/sidekiq/web/application.rb +356 -0
  64. data/lib/sidekiq/web/helpers.rb +324 -0
  65. data/lib/sidekiq/web/router.rb +103 -0
  66. data/lib/sidekiq/worker.rb +247 -0
  67. data/sidekiq.gemspec +21 -0
  68. data/web/assets/images/favicon.ico +0 -0
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +92 -0
  72. data/web/assets/javascripts/dashboard.js +296 -0
  73. data/web/assets/stylesheets/application-rtl.css +246 -0
  74. data/web/assets/stylesheets/application.css +1144 -0
  75. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  76. data/web/assets/stylesheets/bootstrap.css +5 -0
  77. data/web/locales/ar.yml +81 -0
  78. data/web/locales/cs.yml +78 -0
  79. data/web/locales/da.yml +68 -0
  80. data/web/locales/de.yml +69 -0
  81. data/web/locales/el.yml +68 -0
  82. data/web/locales/en.yml +81 -0
  83. data/web/locales/es.yml +70 -0
  84. data/web/locales/fa.yml +80 -0
  85. data/web/locales/fr.yml +78 -0
  86. data/web/locales/he.yml +79 -0
  87. data/web/locales/hi.yml +75 -0
  88. data/web/locales/it.yml +69 -0
  89. data/web/locales/ja.yml +81 -0
  90. data/web/locales/ko.yml +68 -0
  91. data/web/locales/nb.yml +77 -0
  92. data/web/locales/nl.yml +68 -0
  93. data/web/locales/pl.yml +59 -0
  94. data/web/locales/pt-br.yml +68 -0
  95. data/web/locales/pt.yml +67 -0
  96. data/web/locales/ru.yml +78 -0
  97. data/web/locales/sv.yml +68 -0
  98. data/web/locales/ta.yml +75 -0
  99. data/web/locales/uk.yml +76 -0
  100. data/web/locales/ur.yml +80 -0
  101. data/web/locales/zh-cn.yml +68 -0
  102. data/web/locales/zh-tw.yml +68 -0
  103. data/web/views/_footer.erb +20 -0
  104. data/web/views/_job_info.erb +88 -0
  105. data/web/views/_nav.erb +52 -0
  106. data/web/views/_paging.erb +23 -0
  107. data/web/views/_poll_link.erb +7 -0
  108. data/web/views/_status.erb +4 -0
  109. data/web/views/_summary.erb +40 -0
  110. data/web/views/busy.erb +98 -0
  111. data/web/views/dashboard.erb +75 -0
  112. data/web/views/dead.erb +34 -0
  113. data/web/views/layout.erb +40 -0
  114. data/web/views/morgue.erb +75 -0
  115. data/web/views/queue.erb +46 -0
  116. data/web/views/queues.erb +30 -0
  117. data/web/views/retries.erb +80 -0
  118. data/web/views/retry.erb +34 -0
  119. data/web/views/scheduled.erb +54 -0
  120. data/web/views/scheduled_job_info.erb +8 -0
  121. metadata +220 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Extensions
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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Sidekiq
6
+ module ExceptionHandler
7
+ class Logger
8
+ def call(ex, ctx)
9
+ Sidekiq.logger.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
10
+ Sidekiq.logger.warn("#{ex.class.name}: #{ex.message}")
11
+ Sidekiq.logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
12
+ end
13
+
14
+ Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new
15
+ end
16
+
17
+ def handle_exception(ex, ctx = {})
18
+ Sidekiq.error_handlers.each do |handler|
19
+ handler.call(ex, ctx)
20
+ rescue => ex
21
+ Sidekiq.logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
22
+ Sidekiq.logger.error ex
23
+ Sidekiq.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/extensions/generic_proxy"
4
+
5
+ module Sidekiq
6
+ module Extensions
7
+ ##
8
+ # Adds 'delay', 'delay_for' and `delay_until` methods to ActionMailer to offload arbitrary email
9
+ # delivery to Sidekiq. Example:
10
+ #
11
+ # UserMailer.delay.send_welcome_email(new_user)
12
+ # UserMailer.delay_for(5.days).send_welcome_email(new_user)
13
+ # UserMailer.delay_until(5.days.from_now).send_welcome_email(new_user)
14
+ class DelayedMailer
15
+ include Sidekiq::Worker
16
+
17
+ def perform(yml)
18
+ (target, method_name, args) = YAML.load(yml)
19
+ msg = target.public_send(method_name, *args)
20
+ # The email method can return nil, which causes ActionMailer to return
21
+ # an undeliverable empty message.
22
+ if msg
23
+ msg.deliver_now
24
+ else
25
+ raise "#{target.name}##{method_name} returned an undeliverable mail object"
26
+ end
27
+ end
28
+ end
29
+
30
+ module ActionMailer
31
+ def sidekiq_delay(options = {})
32
+ Proxy.new(DelayedMailer, self, options)
33
+ end
34
+
35
+ def sidekiq_delay_for(interval, options = {})
36
+ Proxy.new(DelayedMailer, self, options.merge("at" => Time.now.to_f + interval.to_f))
37
+ end
38
+
39
+ def sidekiq_delay_until(timestamp, options = {})
40
+ Proxy.new(DelayedMailer, self, options.merge("at" => timestamp.to_f))
41
+ end
42
+ alias_method :delay, :sidekiq_delay
43
+ alias_method :delay_for, :sidekiq_delay_for
44
+ alias_method :delay_until, :sidekiq_delay_until
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/extensions/generic_proxy"
4
+
5
+ module Sidekiq
6
+ module Extensions
7
+ ##
8
+ # Adds 'delay', 'delay_for' and `delay_until` methods to ActiveRecord to offload instance method
9
+ # execution to Sidekiq. Examples:
10
+ #
11
+ # User.recent_signups.each { |user| user.delay.mark_as_awesome }
12
+ #
13
+ # Please note, this is not recommended as this will serialize the entire
14
+ # object to Redis. Your Sidekiq jobs should pass IDs, not entire instances.
15
+ # This is here for backwards compatibility with Delayed::Job only.
16
+ class DelayedModel
17
+ include Sidekiq::Worker
18
+
19
+ def perform(yml)
20
+ (target, method_name, args) = YAML.load(yml)
21
+ target.__send__(method_name, *args)
22
+ end
23
+ end
24
+
25
+ module ActiveRecord
26
+ def sidekiq_delay(options = {})
27
+ Proxy.new(DelayedModel, self, options)
28
+ end
29
+
30
+ def sidekiq_delay_for(interval, options = {})
31
+ Proxy.new(DelayedModel, self, options.merge("at" => Time.now.to_f + interval.to_f))
32
+ end
33
+
34
+ def sidekiq_delay_until(timestamp, options = {})
35
+ Proxy.new(DelayedModel, self, options.merge("at" => timestamp.to_f))
36
+ end
37
+ alias_method :delay, :sidekiq_delay
38
+ alias_method :delay_for, :sidekiq_delay_for
39
+ alias_method :delay_until, :sidekiq_delay_until
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/extensions/generic_proxy"
4
+
5
+ module Sidekiq
6
+ module Extensions
7
+ ##
8
+ # Adds 'delay', 'delay_for' and `delay_until` methods to all Classes to offload class method
9
+ # execution to Sidekiq. Examples:
10
+ #
11
+ # User.delay.delete_inactive
12
+ # Wikipedia.delay.download_changes_for(Date.today)
13
+ #
14
+ class DelayedClass
15
+ include Sidekiq::Worker
16
+
17
+ def perform(yml)
18
+ (target, method_name, args) = YAML.load(yml)
19
+ target.__send__(method_name, *args)
20
+ end
21
+ end
22
+
23
+ module Klass
24
+ def sidekiq_delay(options = {})
25
+ Proxy.new(DelayedClass, self, options)
26
+ end
27
+
28
+ def sidekiq_delay_for(interval, options = {})
29
+ Proxy.new(DelayedClass, self, options.merge("at" => Time.now.to_f + interval.to_f))
30
+ end
31
+
32
+ def sidekiq_delay_until(timestamp, options = {})
33
+ Proxy.new(DelayedClass, self, options.merge("at" => timestamp.to_f))
34
+ end
35
+ alias_method :delay, :sidekiq_delay
36
+ alias_method :delay_for, :sidekiq_delay_for
37
+ alias_method :delay_until, :sidekiq_delay_until
38
+ end
39
+ end
40
+ end
41
+
42
+ Module.__send__(:include, Sidekiq::Extensions::Klass) unless defined?(::Rails)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Sidekiq
6
+ module Extensions
7
+ SIZE_LIMIT = 8_192
8
+
9
+ class Proxy < BasicObject
10
+ def initialize(performable, target, options = {})
11
+ @performable = performable
12
+ @target = target
13
+ @opts = options
14
+ end
15
+
16
+ def method_missing(name, *args)
17
+ # Sidekiq has a limitation in that its message must be JSON.
18
+ # JSON can't round trip real Ruby objects so we use YAML to
19
+ # serialize the objects to a String. The YAML will be converted
20
+ # to JSON and then deserialized on the other side back into a
21
+ # Ruby object.
22
+ obj = [@target, name, args]
23
+ marshalled = ::YAML.dump(obj)
24
+ if marshalled.size > SIZE_LIMIT
25
+ ::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
26
+ end
27
+ @performable.client_push({"class" => @performable, "args" => [marshalled]}.merge(@opts))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Sidekiq
6
+ class BasicFetch
7
+ # We want the fetch operation to timeout every few seconds so the thread
8
+ # can check if the process is shutting down.
9
+ TIMEOUT = 2
10
+
11
+ UnitOfWork = Struct.new(:queue, :job) {
12
+ def acknowledge
13
+ # nothing to do
14
+ end
15
+
16
+ def queue_name
17
+ queue.sub(/.*queue:/, "")
18
+ end
19
+
20
+ def requeue
21
+ Sidekiq.redis do |conn|
22
+ conn.rpush("queue:#{queue_name}", job)
23
+ end
24
+ end
25
+ }
26
+
27
+ def initialize(options)
28
+ @strictly_ordered_queues = !!options[:strict]
29
+ @queues = options[:queues].map { |q| "queue:#{q}" }
30
+ if @strictly_ordered_queues
31
+ @queues = @queues.uniq
32
+ @queues << TIMEOUT
33
+ end
34
+ end
35
+
36
+ def retrieve_work
37
+ work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
38
+ UnitOfWork.new(*work) if work
39
+ end
40
+
41
+ # Creating the Redis#brpop command takes into account any
42
+ # configured queue weights. By default Redis#brpop returns
43
+ # data from the first queue that has pending elements. We
44
+ # recreate the queue command each time we invoke Redis#brpop
45
+ # to honor weights and avoid queue starvation.
46
+ def queues_cmd
47
+ if @strictly_ordered_queues
48
+ @queues
49
+ else
50
+ queues = @queues.shuffle.uniq
51
+ queues << TIMEOUT
52
+ queues
53
+ end
54
+ end
55
+
56
+ # By leaving this as a class method, it can be pluggable and used by the Manager actor. Making it
57
+ # an instance method will make it async to the Fetcher actor
58
+ def self.bulk_requeue(inprogress, options)
59
+ return if inprogress.empty?
60
+
61
+ Sidekiq.logger.debug { "Re-queueing terminated jobs" }
62
+ jobs_to_requeue = {}
63
+ inprogress.each do |unit_of_work|
64
+ jobs_to_requeue[unit_of_work.queue_name] ||= []
65
+ jobs_to_requeue[unit_of_work.queue_name] << unit_of_work.job
66
+ end
67
+
68
+ Sidekiq.redis do |conn|
69
+ conn.pipelined do
70
+ jobs_to_requeue.each do |queue, jobs|
71
+ conn.rpush("queue:#{queue}", jobs)
72
+ end
73
+ end
74
+ end
75
+ Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
76
+ rescue => ex
77
+ Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class JobLogger
5
+ def initialize(logger = Sidekiq.logger)
6
+ @logger = logger
7
+ end
8
+
9
+ def call(item, queue)
10
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
+ @logger.info("start")
12
+
13
+ yield
14
+
15
+ with_elapsed_time_context(start) do
16
+ @logger.info("done")
17
+ end
18
+ rescue Exception
19
+ with_elapsed_time_context(start) do
20
+ @logger.info("fail")
21
+ end
22
+
23
+ raise
24
+ end
25
+
26
+ def with_job_hash_context(job_hash, &block)
27
+ @logger.with_context(job_hash_context(job_hash), &block)
28
+ end
29
+
30
+ def job_hash_context(job_hash)
31
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
32
+ # attribute to expose the underlying thing.
33
+ h = {
34
+ class: job_hash["wrapped"] || job_hash["class"],
35
+ jid: job_hash["jid"],
36
+ }
37
+ h[:bid] = job_hash["bid"] if job_hash["bid"]
38
+ h
39
+ end
40
+
41
+ def with_elapsed_time_context(start, &block)
42
+ @logger.with_context(elapsed_time_context(start), &block)
43
+ end
44
+
45
+ def elapsed_time_context(start)
46
+ {elapsed: elapsed(start).to_s}
47
+ end
48
+
49
+ private
50
+
51
+ def elapsed(start)
52
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/scheduled"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ ##
8
+ # Automatically retry jobs that fail in Sidekiq.
9
+ # Sidekiq's retry support assumes a typical development lifecycle:
10
+ #
11
+ # 0. Push some code changes with a bug in it.
12
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
13
+ # the job and pushes it onto a retry queue.
14
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
15
+ # an exponential delay, the job continues to fail.
16
+ # 3. After a few days, a developer deploys a fix. The job is
17
+ # reprocessed successfully.
18
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
19
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
20
+ # manually in the Web UI.
21
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
22
+ #
23
+ # A job looks like:
24
+ #
25
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
26
+ #
27
+ # The 'retry' option also accepts a number (in place of 'true'):
28
+ #
29
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
30
+ #
31
+ # The job will be retried this number of times before giving up. (If simply
32
+ # 'true', Sidekiq retries 25 times)
33
+ #
34
+ # We'll add a bit more data to the job to support retries:
35
+ #
36
+ # * 'queue' - the queue to use
37
+ # * 'retry_count' - number of times we've retried so far.
38
+ # * 'error_message' - the message from the exception
39
+ # * 'error_class' - the exception class
40
+ # * 'failed_at' - the first time it failed
41
+ # * 'retried_at' - the last time it was retried
42
+ # * 'backtrace' - the number of lines of error backtrace to store
43
+ #
44
+ # We don't store the backtrace by default as that can add a lot of overhead
45
+ # to the job and everyone is using an error service, right?
46
+ #
47
+ # The default number of retries is 25 which works out to about 3 weeks
48
+ # You can change the default maximum number of retries in your initializer:
49
+ #
50
+ # Sidekiq.options[:max_retries] = 7
51
+ #
52
+ # or limit the number of retries for a particular worker with:
53
+ #
54
+ # class MyWorker
55
+ # include Sidekiq::Worker
56
+ # sidekiq_options :retry => 10
57
+ # end
58
+ #
59
+ class JobRetry
60
+ class Handled < ::RuntimeError; end
61
+ class Skip < Handled; end
62
+
63
+ include Sidekiq::Util
64
+
65
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
66
+
67
+ def initialize(options = {})
68
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
69
+ end
70
+
71
+ # The global retry handler requires only the barest of data.
72
+ # We want to be able to retry as much as possible so we don't
73
+ # require the worker to be instantiated.
74
+ def global(msg, queue)
75
+ yield
76
+ rescue Handled => ex
77
+ raise ex
78
+ rescue Sidekiq::Shutdown => ey
79
+ # ignore, will be pushed back onto queue during hard_shutdown
80
+ raise ey
81
+ rescue Exception => e
82
+ # ignore, will be pushed back onto queue during hard_shutdown
83
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
84
+
85
+ if msg["retry"]
86
+ attempt_retry(nil, msg, queue, e)
87
+ else
88
+ Sidekiq.death_handlers.each do |handler|
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
+
95
+ raise Handled
96
+ end
97
+
98
+ # The local retry support means that any errors that occur within
99
+ # this block can be associated with the given worker instance.
100
+ # This is required to support the `sidekiq_retries_exhausted` block.
101
+ #
102
+ # Note that any exception from the block is wrapped in the Skip
103
+ # exception so the global block does not reprocess the error. The
104
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
105
+ # calling the handle_exception handlers.
106
+ def local(worker, msg, queue)
107
+ yield
108
+ rescue Handled => ex
109
+ raise ex
110
+ rescue Sidekiq::Shutdown => ey
111
+ # ignore, will be pushed back onto queue during hard_shutdown
112
+ raise ey
113
+ rescue Exception => e
114
+ # ignore, will be pushed back onto queue during hard_shutdown
115
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
116
+
117
+ if msg["retry"].nil?
118
+ msg["retry"] = worker.class.get_sidekiq_options["retry"]
119
+ end
120
+
121
+ raise e unless msg["retry"]
122
+ attempt_retry(worker, msg, queue, e)
123
+ # We've handled this error associated with this job, don't
124
+ # need to handle it at the global level
125
+ raise Skip
126
+ end
127
+
128
+ private
129
+
130
+ # Note that +worker+ can be nil here if an error is raised before we can
131
+ # instantiate the worker instance. All access must be guarded and
132
+ # best effort.
133
+ def attempt_retry(worker, msg, queue, exception)
134
+ max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
135
+
136
+ msg["queue"] = (msg["retry_queue"] || queue)
137
+
138
+ m = exception_message(exception)
139
+ if m.respond_to?(:scrub!)
140
+ m.force_encoding("utf-8")
141
+ m.scrub!
142
+ end
143
+
144
+ msg["error_message"] = m
145
+ msg["error_class"] = exception.class.name
146
+ count = if msg["retry_count"]
147
+ msg["retried_at"] = Time.now.to_f
148
+ msg["retry_count"] += 1
149
+ else
150
+ msg["failed_at"] = Time.now.to_f
151
+ msg["retry_count"] = 0
152
+ end
153
+
154
+ if msg["backtrace"] == true
155
+ msg["error_backtrace"] = exception.backtrace
156
+ elsif !msg["backtrace"]
157
+ # do nothing
158
+ elsif msg["backtrace"].to_i != 0
159
+ msg["error_backtrace"] = exception.backtrace[0...msg["backtrace"].to_i]
160
+ end
161
+
162
+ if count < max_retry_attempts
163
+ delay = delay_for(worker, count, exception)
164
+ # Logging here can break retries if the logging device raises ENOSPC #3979
165
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
166
+ retry_at = Time.now.to_f + delay
167
+ payload = Sidekiq.dump_json(msg)
168
+ Sidekiq.redis do |conn|
169
+ conn.zadd("retry", retry_at.to_s, payload)
170
+ end
171
+ else
172
+ # Goodbye dear message, you (re)tried your best I'm sure.
173
+ retries_exhausted(worker, msg, exception)
174
+ end
175
+ end
176
+
177
+ def retries_exhausted(worker, msg, exception)
178
+ begin
179
+ block = worker&.sidekiq_retries_exhausted_block
180
+ block&.call(msg, exception)
181
+ rescue => e
182
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
183
+ end
184
+
185
+ Sidekiq.death_handlers.each do |handler|
186
+ handler.call(msg, exception)
187
+ rescue => e
188
+ handle_exception(e, {context: "Error calling death handler", job: msg})
189
+ end
190
+
191
+ send_to_morgue(msg) unless msg["dead"] == false
192
+ end
193
+
194
+ def send_to_morgue(msg)
195
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
196
+ payload = Sidekiq.dump_json(msg)
197
+ DeadSet.new.kill(payload, notify_failure: false)
198
+ end
199
+
200
+ def retry_attempts_from(msg_retry, default)
201
+ if msg_retry.is_a?(Integer)
202
+ msg_retry
203
+ else
204
+ default
205
+ end
206
+ end
207
+
208
+ def delay_for(worker, count, exception)
209
+ if worker&.sidekiq_retry_in_block
210
+ custom_retry_in = retry_in(worker, count, exception).to_i
211
+ return custom_retry_in if custom_retry_in > 0
212
+ end
213
+ seconds_to_delay(count)
214
+ end
215
+
216
+ # delayed_job uses the same basic formula
217
+ def seconds_to_delay(count)
218
+ (count**4) + 15 + (rand(30) * (count + 1))
219
+ end
220
+
221
+ def retry_in(worker, count, exception)
222
+ worker.sidekiq_retry_in_block.call(count, exception)
223
+ rescue Exception => e
224
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
225
+ nil
226
+ end
227
+
228
+ def exception_caused_by_shutdown?(e, checked_causes = [])
229
+ return false unless e.cause
230
+
231
+ # Handle circular causes
232
+ checked_causes << e.object_id
233
+ return false if checked_causes.include?(e.cause.object_id)
234
+
235
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
236
+ exception_caused_by_shutdown?(e.cause, checked_causes)
237
+ end
238
+
239
+ # Extract message from exception.
240
+ # Set a default if the message raises an error
241
+ def exception_message(exception)
242
+ # App code can stuff all sorts of crazy binary data into the error message
243
+ # that won't convert to JSON.
244
+ exception.message.to_s[0, 10_000]
245
+ rescue
246
+ +"!!! ERROR MESSAGE THREW AN ERROR !!!"
247
+ end
248
+ end
249
+ end