sidekiq 4.1.4 → 6.5.6

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 (213) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +666 -0
  3. data/LICENSE +3 -3
  4. data/README.md +27 -35
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +78 -84
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  11. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  12. data/lib/sidekiq/api.rb +583 -288
  13. data/lib/sidekiq/cli.rb +255 -218
  14. data/lib/sidekiq/client.rb +86 -83
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +43 -0
  17. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +13 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +51 -0
  24. data/lib/sidekiq/job_retry.rb +282 -0
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +192 -85
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +44 -45
  29. data/lib/sidekiq/metrics/deploy.rb +47 -0
  30. data/lib/sidekiq/metrics/query.rb +153 -0
  31. data/lib/sidekiq/metrics/shared.rb +94 -0
  32. data/lib/sidekiq/metrics/tracking.rb +134 -0
  33. data/lib/sidekiq/middleware/chain.rb +102 -46
  34. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  35. data/lib/sidekiq/middleware/i18n.rb +7 -7
  36. data/lib/sidekiq/middleware/modules.rb +21 -0
  37. data/lib/sidekiq/monitor.rb +133 -0
  38. data/lib/sidekiq/paginator.rb +20 -16
  39. data/lib/sidekiq/processor.rb +178 -78
  40. data/lib/sidekiq/rails.rb +56 -27
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +123 -47
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +97 -40
  45. data/lib/sidekiq/sd_notify.rb +149 -0
  46. data/lib/sidekiq/systemd.rb +24 -0
  47. data/lib/sidekiq/testing/inline.rb +6 -5
  48. data/lib/sidekiq/testing.rb +83 -56
  49. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web/action.rb +93 -0
  52. data/lib/sidekiq/web/application.rb +379 -0
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +365 -0
  55. data/lib/sidekiq/web/router.rb +104 -0
  56. data/lib/sidekiq/web.rb +108 -213
  57. data/lib/sidekiq/worker.rb +288 -42
  58. data/lib/sidekiq.rb +188 -80
  59. data/sidekiq.gemspec +24 -22
  60. data/web/assets/images/apple-touch-icon.png +0 -0
  61. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  62. data/web/assets/javascripts/application.js +130 -75
  63. data/web/assets/javascripts/chart.min.js +13 -0
  64. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  65. data/web/assets/javascripts/dashboard.js +70 -91
  66. data/web/assets/javascripts/graph.js +16 -0
  67. data/web/assets/javascripts/metrics.js +262 -0
  68. data/web/assets/stylesheets/application-dark.css +143 -0
  69. data/web/assets/stylesheets/application-rtl.css +242 -0
  70. data/web/assets/stylesheets/application.css +390 -145
  71. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  72. data/web/assets/stylesheets/bootstrap.css +4 -8
  73. data/web/locales/ar.yml +87 -0
  74. data/web/locales/de.yml +15 -3
  75. data/web/locales/el.yml +43 -19
  76. data/web/locales/en.yml +15 -1
  77. data/web/locales/es.yml +22 -5
  78. data/web/locales/fa.yml +80 -0
  79. data/web/locales/fr.yml +10 -3
  80. data/web/locales/he.yml +79 -0
  81. data/web/locales/ja.yml +12 -4
  82. data/web/locales/lt.yml +83 -0
  83. data/web/locales/pl.yml +4 -4
  84. data/web/locales/pt-br.yml +27 -9
  85. data/web/locales/ru.yml +4 -0
  86. data/web/locales/ur.yml +80 -0
  87. data/web/locales/vi.yml +83 -0
  88. data/web/views/_footer.erb +6 -3
  89. data/web/views/_job_info.erb +4 -3
  90. data/web/views/_nav.erb +5 -19
  91. data/web/views/_paging.erb +1 -1
  92. data/web/views/_poll_link.erb +2 -5
  93. data/web/views/_summary.erb +7 -7
  94. data/web/views/busy.erb +64 -26
  95. data/web/views/dashboard.erb +26 -17
  96. data/web/views/dead.erb +4 -4
  97. data/web/views/layout.erb +15 -5
  98. data/web/views/metrics.erb +69 -0
  99. data/web/views/metrics_for_job.erb +87 -0
  100. data/web/views/morgue.erb +21 -14
  101. data/web/views/queue.erb +28 -14
  102. data/web/views/queues.erb +15 -5
  103. data/web/views/retries.erb +24 -15
  104. data/web/views/retry.erb +5 -5
  105. data/web/views/scheduled.erb +9 -6
  106. data/web/views/scheduled_job_info.erb +1 -1
  107. metadata +73 -256
  108. data/.github/contributing.md +0 -32
  109. data/.github/issue_template.md +0 -4
  110. data/.gitignore +0 -12
  111. data/.travis.yml +0 -18
  112. data/3.0-Upgrade.md +0 -70
  113. data/4.0-Upgrade.md +0 -53
  114. data/COMM-LICENSE +0 -95
  115. data/Ent-Changes.md +0 -123
  116. data/Gemfile +0 -29
  117. data/Pro-2.0-Upgrade.md +0 -138
  118. data/Pro-3.0-Upgrade.md +0 -44
  119. data/Pro-Changes.md +0 -559
  120. data/Rakefile +0 -9
  121. data/bin/sidekiqctl +0 -99
  122. data/code_of_conduct.md +0 -50
  123. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  124. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  125. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  126. data/lib/sidekiq/core_ext.rb +0 -106
  127. data/lib/sidekiq/exception_handler.rb +0 -31
  128. data/lib/sidekiq/logging.rb +0 -106
  129. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  130. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  131. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  132. data/lib/sidekiq/util.rb +0 -62
  133. data/lib/sidekiq/web_helpers.rb +0 -255
  134. data/test/config.yml +0 -9
  135. data/test/env_based_config.yml +0 -11
  136. data/test/fake_env.rb +0 -1
  137. data/test/fixtures/en.yml +0 -2
  138. data/test/helper.rb +0 -75
  139. data/test/test_actors.rb +0 -138
  140. data/test/test_api.rb +0 -528
  141. data/test/test_cli.rb +0 -406
  142. data/test/test_client.rb +0 -266
  143. data/test/test_exception_handler.rb +0 -56
  144. data/test/test_extensions.rb +0 -127
  145. data/test/test_fetch.rb +0 -50
  146. data/test/test_launcher.rb +0 -85
  147. data/test/test_logging.rb +0 -35
  148. data/test/test_manager.rb +0 -50
  149. data/test/test_middleware.rb +0 -158
  150. data/test/test_processor.rb +0 -201
  151. data/test/test_rails.rb +0 -22
  152. data/test/test_redis_connection.rb +0 -132
  153. data/test/test_retry.rb +0 -326
  154. data/test/test_retry_exhausted.rb +0 -149
  155. data/test/test_scheduled.rb +0 -115
  156. data/test/test_scheduling.rb +0 -50
  157. data/test/test_sidekiq.rb +0 -107
  158. data/test/test_testing.rb +0 -143
  159. data/test/test_testing_fake.rb +0 -357
  160. data/test/test_testing_inline.rb +0 -94
  161. data/test/test_util.rb +0 -13
  162. data/test/test_web.rb +0 -614
  163. data/test/test_web_helpers.rb +0 -54
  164. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  165. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  166. data/web/assets/images/status/active.png +0 -0
  167. data/web/assets/images/status/idle.png +0 -0
  168. data/web/assets/javascripts/locales/README.md +0 -27
  169. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  170. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  171. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  172. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  173. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  175. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  176. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  178. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  179. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  181. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  182. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  183. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  184. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  185. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  186. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  187. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  188. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  189. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  190. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  191. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  192. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  193. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  194. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  195. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  196. data/web/assets/javascripts/locales/jquery.timeago.nb.js +0 -18
  197. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  198. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  199. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  200. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  201. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  202. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  203. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  204. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  205. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  206. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  207. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  208. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  209. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  210. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  211. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  212. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  213. data/web/views/_poll_js.erb +0 -5
data/lib/sidekiq/fetch.rb CHANGED
@@ -1,81 +1,91 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
2
 
4
- module Sidekiq
3
+ require "sidekiq"
4
+ require "sidekiq/component"
5
+
6
+ module Sidekiq # :nodoc:
5
7
  class BasicFetch
8
+ include Sidekiq::Component
6
9
  # We want the fetch operation to timeout every few seconds so the thread
7
10
  # can check if the process is shutting down.
8
11
  TIMEOUT = 2
9
12
 
10
- UnitOfWork = Struct.new(:queue, :job) do
13
+ UnitOfWork = Struct.new(:queue, :job, :config) {
11
14
  def acknowledge
12
15
  # nothing to do
13
16
  end
14
17
 
15
18
  def queue_name
16
- queue.sub(/.*queue:/, ''.freeze)
19
+ queue.delete_prefix("queue:")
17
20
  end
18
21
 
19
22
  def requeue
20
- Sidekiq.redis do |conn|
21
- conn.rpush("queue:#{queue_name}", job)
23
+ config.redis do |conn|
24
+ conn.rpush(queue, job)
22
25
  end
23
26
  end
24
- end
27
+ }
25
28
 
26
- def initialize(options)
27
- @strictly_ordered_queues = !!options[:strict]
28
- @queues = options[:queues].map { |q| "queue:#{q}" }
29
+ def initialize(config)
30
+ raise ArgumentError, "missing queue list" unless config[:queues]
31
+ @config = config
32
+ @strictly_ordered_queues = !!@config[:strict]
33
+ @queues = @config[:queues].map { |q| "queue:#{q}" }
29
34
  if @strictly_ordered_queues
30
- @queues = @queues.uniq
31
- @queues << TIMEOUT
35
+ @queues.uniq!
36
+ @queues << {timeout: TIMEOUT}
32
37
  end
33
38
  end
34
39
 
35
40
  def retrieve_work
36
- work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
37
- UnitOfWork.new(*work) if work
38
- end
39
-
40
- # Creating the Redis#brpop command takes into account any
41
- # configured queue weights. By default Redis#brpop returns
42
- # data from the first queue that has pending elements. We
43
- # recreate the queue command each time we invoke Redis#brpop
44
- # to honor weights and avoid queue starvation.
45
- def queues_cmd
46
- if @strictly_ordered_queues
47
- @queues
48
- else
49
- queues = @queues.shuffle.uniq
50
- queues << TIMEOUT
51
- queues
41
+ qs = queues_cmd
42
+ # 4825 Sidekiq Pro with all queues paused will return an
43
+ # empty set of queues with a trailing TIMEOUT value.
44
+ if qs.size <= 1
45
+ sleep(TIMEOUT)
46
+ return nil
52
47
  end
53
- end
54
48
 
49
+ queue, job = redis { |conn| conn.brpop(*qs) }
50
+ UnitOfWork.new(queue, job, config) if queue
51
+ end
55
52
 
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)
53
+ def bulk_requeue(inprogress, options)
59
54
  return if inprogress.empty?
60
55
 
61
- Sidekiq.logger.debug { "Re-queueing terminated jobs" }
56
+ logger.debug { "Re-queueing terminated jobs" }
62
57
  jobs_to_requeue = {}
63
58
  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
59
+ jobs_to_requeue[unit_of_work.queue] ||= []
60
+ jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
66
61
  end
67
62
 
68
- Sidekiq.redis do |conn|
69
- conn.pipelined do
63
+ redis do |conn|
64
+ conn.pipelined do |pipeline|
70
65
  jobs_to_requeue.each do |queue, jobs|
71
- conn.rpush("queue:#{queue}", jobs)
66
+ pipeline.rpush(queue, jobs)
72
67
  end
73
68
  end
74
69
  end
75
- Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
70
+ logger.info("Pushed #{inprogress.size} jobs back to Redis")
76
71
  rescue => ex
77
- Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
72
+ logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
78
73
  end
79
74
 
75
+ # Creating the Redis#brpop command takes into account any
76
+ # configured queue weights. By default Redis#brpop returns
77
+ # data from the first queue that has pending elements. We
78
+ # recreate the queue command each time we invoke Redis#brpop
79
+ # to honor weights and avoid queue starvation.
80
+ def queues_cmd
81
+ if @strictly_ordered_queues
82
+ @queues
83
+ else
84
+ permute = @queues.shuffle
85
+ permute.uniq!
86
+ permute << {timeout: TIMEOUT}
87
+ permute
88
+ end
89
+ end
80
90
  end
81
91
  end
@@ -0,0 +1,13 @@
1
+ require "sidekiq/worker"
2
+
3
+ module Sidekiq
4
+ # Sidekiq::Job is a new alias for Sidekiq::Worker as of Sidekiq 6.3.0.
5
+ # Use `include Sidekiq::Job` rather than `include Sidekiq::Worker`.
6
+ #
7
+ # The term "worker" is too generic and overly confusing, used in several
8
+ # different contexts meaning different things. Many people call a Sidekiq
9
+ # process a "worker". Some people call the thread that executes jobs a
10
+ # "worker". This change brings Sidekiq closer to ActiveJob where your job
11
+ # classes extend ApplicationJob.
12
+ Job = Worker
13
+ end
@@ -0,0 +1,51 @@
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
+ Sidekiq::Context.add(:elapsed, elapsed(start))
16
+ @logger.info("done")
17
+ rescue Exception
18
+ Sidekiq::Context.add(:elapsed, elapsed(start))
19
+ @logger.info("fail")
20
+
21
+ raise
22
+ end
23
+
24
+ def prepare(job_hash, &block)
25
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
26
+ # attribute to expose the underlying thing.
27
+ h = {
28
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
29
+ jid: job_hash["jid"]
30
+ }
31
+ h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
32
+ h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
33
+
34
+ Thread.current[:sidekiq_context] = h
35
+ level = job_hash["log_level"]
36
+ if level
37
+ @logger.log_at(level, &block)
38
+ else
39
+ yield
40
+ end
41
+ ensure
42
+ Thread.current[:sidekiq_context] = nil
43
+ end
44
+
45
+ private
46
+
47
+ def elapsed(start)
48
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "base64"
5
+ require "sidekiq/component"
6
+
7
+ module Sidekiq
8
+ ##
9
+ # Automatically retry jobs that fail in Sidekiq.
10
+ # Sidekiq's retry support assumes a typical development lifecycle:
11
+ #
12
+ # 0. Push some code changes with a bug in it.
13
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
14
+ # the job and pushes it onto a retry queue.
15
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
16
+ # an exponential delay, the job continues to fail.
17
+ # 3. After a few days, a developer deploys a fix. The job is
18
+ # reprocessed successfully.
19
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
20
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
21
+ # manually in the Web UI.
22
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
23
+ #
24
+ # A job looks like:
25
+ #
26
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
27
+ #
28
+ # The 'retry' option also accepts a number (in place of 'true'):
29
+ #
30
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
31
+ #
32
+ # The job will be retried this number of times before giving up. (If simply
33
+ # 'true', Sidekiq retries 25 times)
34
+ #
35
+ # Relevant options for job retries:
36
+ #
37
+ # * 'queue' - the queue for the initial job
38
+ # * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue
39
+ # * 'retry_count' - number of times we've retried so far.
40
+ # * 'error_message' - the message from the exception
41
+ # * 'error_class' - the exception class
42
+ # * 'failed_at' - the first time it failed
43
+ # * 'retried_at' - the last time it was retried
44
+ # * 'backtrace' - the number of lines of error backtrace to store
45
+ #
46
+ # We don't store the backtrace by default as that can add a lot of overhead
47
+ # to the job and everyone is using an error service, right?
48
+ #
49
+ # The default number of retries is 25 which works out to about 3 weeks
50
+ # You can change the default maximum number of retries in your initializer:
51
+ #
52
+ # Sidekiq.options[:max_retries] = 7
53
+ #
54
+ # or limit the number of retries for a particular job and send retries to
55
+ # a low priority queue with:
56
+ #
57
+ # class MyJob
58
+ # include Sidekiq::Job
59
+ # sidekiq_options retry: 10, retry_queue: 'low'
60
+ # end
61
+ #
62
+ class JobRetry
63
+ class Handled < ::RuntimeError; end
64
+
65
+ class Skip < Handled; end
66
+
67
+ include Sidekiq::Component
68
+
69
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
70
+
71
+ def initialize(options)
72
+ @config = options
73
+ @max_retries = @config[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
74
+ end
75
+
76
+ # The global retry handler requires only the barest of data.
77
+ # We want to be able to retry as much as possible so we don't
78
+ # require the job to be instantiated.
79
+ def global(jobstr, queue)
80
+ yield
81
+ rescue Handled => ex
82
+ raise ex
83
+ rescue Sidekiq::Shutdown => ey
84
+ # ignore, will be pushed back onto queue during hard_shutdown
85
+ raise ey
86
+ rescue Exception => e
87
+ # ignore, will be pushed back onto queue during hard_shutdown
88
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
89
+
90
+ msg = Sidekiq.load_json(jobstr)
91
+ if msg["retry"]
92
+ process_retry(nil, msg, queue, e)
93
+ else
94
+ Sidekiq.death_handlers.each do |handler|
95
+ handler.call(msg, e)
96
+ rescue => handler_ex
97
+ handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
98
+ end
99
+ end
100
+
101
+ raise Handled
102
+ end
103
+
104
+ # The local retry support means that any errors that occur within
105
+ # this block can be associated with the given job instance.
106
+ # This is required to support the `sidekiq_retries_exhausted` block.
107
+ #
108
+ # Note that any exception from the block is wrapped in the Skip
109
+ # exception so the global block does not reprocess the error. The
110
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
111
+ # calling the handle_exception handlers.
112
+ def local(jobinst, jobstr, queue)
113
+ yield
114
+ rescue Handled => ex
115
+ raise ex
116
+ rescue Sidekiq::Shutdown => ey
117
+ # ignore, will be pushed back onto queue during hard_shutdown
118
+ raise ey
119
+ rescue Exception => e
120
+ # ignore, will be pushed back onto queue during hard_shutdown
121
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
122
+
123
+ msg = Sidekiq.load_json(jobstr)
124
+ if msg["retry"].nil?
125
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
126
+ end
127
+
128
+ raise e unless msg["retry"]
129
+ process_retry(jobinst, msg, queue, e)
130
+ # We've handled this error associated with this job, don't
131
+ # need to handle it at the global level
132
+ raise Skip
133
+ end
134
+
135
+ private
136
+
137
+ # Note that +jobinst+ can be nil here if an error is raised before we can
138
+ # instantiate the job instance. All access must be guarded and
139
+ # best effort.
140
+ def process_retry(jobinst, msg, queue, exception)
141
+ max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
142
+
143
+ msg["queue"] = (msg["retry_queue"] || queue)
144
+
145
+ m = exception_message(exception)
146
+ if m.respond_to?(:scrub!)
147
+ m.force_encoding("utf-8")
148
+ m.scrub!
149
+ end
150
+
151
+ msg["error_message"] = m
152
+ msg["error_class"] = exception.class.name
153
+ count = if msg["retry_count"]
154
+ msg["retried_at"] = Time.now.to_f
155
+ msg["retry_count"] += 1
156
+ else
157
+ msg["failed_at"] = Time.now.to_f
158
+ msg["retry_count"] = 0
159
+ end
160
+
161
+ if msg["backtrace"]
162
+ lines = if msg["backtrace"] == true
163
+ exception.backtrace
164
+ else
165
+ exception.backtrace[0...msg["backtrace"].to_i]
166
+ end
167
+
168
+ msg["error_backtrace"] = compress_backtrace(lines)
169
+ end
170
+
171
+ # Goodbye dear message, you (re)tried your best I'm sure.
172
+ return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
173
+
174
+ strategy, delay = delay_for(jobinst, count, exception)
175
+ case strategy
176
+ when :discard
177
+ return # poof!
178
+ when :kill
179
+ return retries_exhausted(jobinst, msg, exception)
180
+ end
181
+
182
+ # Logging here can break retries if the logging device raises ENOSPC #3979
183
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
184
+ jitter = rand(10) * (count + 1)
185
+ retry_at = Time.now.to_f + delay + jitter
186
+ payload = Sidekiq.dump_json(msg)
187
+ redis do |conn|
188
+ conn.zadd("retry", retry_at.to_s, payload)
189
+ end
190
+ end
191
+
192
+ # returns (strategy, seconds)
193
+ def delay_for(jobinst, count, exception)
194
+ rv = begin
195
+ # sidekiq_retry_in can return two different things:
196
+ # 1. When to retry next, as an integer of seconds
197
+ # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
198
+ jobinst&.sidekiq_retry_in_block&.call(count, exception)
199
+ rescue Exception => e
200
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
201
+ nil
202
+ end
203
+
204
+ delay = (count**4) + 15
205
+ if Integer === rv && rv > 0
206
+ delay = rv
207
+ elsif rv == :discard
208
+ return [:discard, nil] # do nothing, job goes poof
209
+ elsif rv == :kill
210
+ return [:kill, nil]
211
+ end
212
+
213
+ [:default, delay]
214
+ end
215
+
216
+ def retries_exhausted(jobinst, msg, exception)
217
+ begin
218
+ block = jobinst&.sidekiq_retries_exhausted_block
219
+ block&.call(msg, exception)
220
+ rescue => e
221
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
222
+ end
223
+
224
+ send_to_morgue(msg) unless msg["dead"] == false
225
+
226
+ config.death_handlers.each do |handler|
227
+ handler.call(msg, exception)
228
+ rescue => e
229
+ handle_exception(e, {context: "Error calling death handler", job: msg})
230
+ end
231
+ end
232
+
233
+ def send_to_morgue(msg)
234
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
235
+ payload = Sidekiq.dump_json(msg)
236
+ now = Time.now.to_f
237
+
238
+ config.redis do |conn|
239
+ conn.multi do |xa|
240
+ xa.zadd("dead", now.to_s, payload)
241
+ xa.zremrangebyscore("dead", "-inf", now - config[:dead_timeout_in_seconds])
242
+ xa.zremrangebyrank("dead", 0, - config[:dead_max_jobs])
243
+ end
244
+ end
245
+ end
246
+
247
+ def retry_attempts_from(msg_retry, default)
248
+ if msg_retry.is_a?(Integer)
249
+ msg_retry
250
+ else
251
+ default
252
+ end
253
+ end
254
+
255
+ def exception_caused_by_shutdown?(e, checked_causes = [])
256
+ return false unless e.cause
257
+
258
+ # Handle circular causes
259
+ checked_causes << e.object_id
260
+ return false if checked_causes.include?(e.cause.object_id)
261
+
262
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
263
+ exception_caused_by_shutdown?(e.cause, checked_causes)
264
+ end
265
+
266
+ # Extract message from exception.
267
+ # Set a default if the message raises an error
268
+ def exception_message(exception)
269
+ # App code can stuff all sorts of crazy binary data into the error message
270
+ # that won't convert to JSON.
271
+ exception.message.to_s[0, 10_000]
272
+ rescue
273
+ +"!!! ERROR MESSAGE THREW AN ERROR !!!"
274
+ end
275
+
276
+ def compress_backtrace(backtrace)
277
+ serialized = Sidekiq.dump_json(backtrace)
278
+ compressed = Zlib::Deflate.deflate(serialized)
279
+ Base64.encode64(compressed)
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,71 @@
1
+ require "securerandom"
2
+ require "time"
3
+
4
+ module Sidekiq
5
+ module JobUtil
6
+ # These functions encapsulate various job utilities.
7
+
8
+ TRANSIENT_ATTRIBUTES = %w[]
9
+
10
+ def validate(item)
11
+ raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
12
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
13
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
14
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
15
+ raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
16
+ end
17
+
18
+ def verify_json(item)
19
+ job_class = item["wrapped"] || item["class"]
20
+ if Sidekiq[:on_complex_arguments] == :raise
21
+ msg = <<~EOM
22
+ Job arguments to #{job_class} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
23
+ To disable this error, remove `Sidekiq.strict_args!` from your initializer.
24
+ EOM
25
+ raise(ArgumentError, msg) unless json_safe?(item)
26
+ elsif Sidekiq[:on_complex_arguments] == :warn
27
+ Sidekiq.logger.warn <<~EOM unless json_safe?(item)
28
+ Job arguments to #{job_class} do not serialize to JSON safely. This will raise an error in
29
+ Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
30
+ by calling `Sidekiq.strict_args!` during Sidekiq initialization.
31
+ EOM
32
+ end
33
+ end
34
+
35
+ def normalize_item(item)
36
+ validate(item)
37
+
38
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
39
+ # this allows ActiveJobs to control sidekiq_options too.
40
+ defaults = normalized_hash(item["class"])
41
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
42
+ item = defaults.merge(item)
43
+
44
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
45
+
46
+ # remove job attributes which aren't necessary to persist into Redis
47
+ TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
48
+
49
+ item["jid"] ||= SecureRandom.hex(12)
50
+ item["class"] = item["class"].to_s
51
+ item["queue"] = item["queue"].to_s
52
+ item["created_at"] ||= Time.now.to_f
53
+ item
54
+ end
55
+
56
+ def normalized_hash(item_class)
57
+ if item_class.is_a?(Class)
58
+ raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
59
+ item_class.get_sidekiq_options
60
+ else
61
+ Sidekiq.default_job_options
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def json_safe?(item)
68
+ JSON.parse(JSON.dump(item["args"])) == item["args"]
69
+ end
70
+ end
71
+ end