sidekiq 6.4.1 → 7.0.7

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +172 -12
  3. data/README.md +41 -33
  4. data/bin/sidekiq +3 -8
  5. data/bin/sidekiqload +188 -114
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +275 -161
  8. data/lib/sidekiq/capsule.rb +127 -0
  9. data/lib/sidekiq/cli.rb +83 -88
  10. data/lib/sidekiq/client.rb +55 -43
  11. data/lib/sidekiq/component.rb +68 -0
  12. data/lib/sidekiq/config.rb +270 -0
  13. data/lib/sidekiq/deploy.rb +62 -0
  14. data/lib/sidekiq/embedded.rb +61 -0
  15. data/lib/sidekiq/fetch.rb +21 -22
  16. data/lib/sidekiq/job.rb +375 -10
  17. data/lib/sidekiq/job_logger.rb +2 -2
  18. data/lib/sidekiq/job_retry.rb +76 -54
  19. data/lib/sidekiq/job_util.rb +59 -19
  20. data/lib/sidekiq/launcher.rb +90 -82
  21. data/lib/sidekiq/logger.rb +6 -45
  22. data/lib/sidekiq/manager.rb +33 -32
  23. data/lib/sidekiq/metrics/query.rb +153 -0
  24. data/lib/sidekiq/metrics/shared.rb +95 -0
  25. data/lib/sidekiq/metrics/tracking.rb +136 -0
  26. data/lib/sidekiq/middleware/chain.rb +96 -51
  27. data/lib/sidekiq/middleware/current_attributes.rb +16 -17
  28. data/lib/sidekiq/middleware/i18n.rb +6 -4
  29. data/lib/sidekiq/middleware/modules.rb +21 -0
  30. data/lib/sidekiq/monitor.rb +17 -4
  31. data/lib/sidekiq/paginator.rb +11 -3
  32. data/lib/sidekiq/processor.rb +60 -60
  33. data/lib/sidekiq/rails.rb +12 -10
  34. data/lib/sidekiq/redis_client_adapter.rb +115 -0
  35. data/lib/sidekiq/redis_connection.rb +13 -82
  36. data/lib/sidekiq/ring_buffer.rb +29 -0
  37. data/lib/sidekiq/scheduled.rb +65 -37
  38. data/lib/sidekiq/testing/inline.rb +4 -4
  39. data/lib/sidekiq/testing.rb +41 -68
  40. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  41. data/lib/sidekiq/version.rb +2 -1
  42. data/lib/sidekiq/web/action.rb +3 -3
  43. data/lib/sidekiq/web/application.rb +40 -9
  44. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  45. data/lib/sidekiq/web/helpers.rb +34 -20
  46. data/lib/sidekiq/web.rb +7 -14
  47. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  48. data/lib/sidekiq.rb +84 -207
  49. data/sidekiq.gemspec +20 -10
  50. data/web/assets/javascripts/application.js +76 -26
  51. data/web/assets/javascripts/base-charts.js +106 -0
  52. data/web/assets/javascripts/chart.min.js +13 -0
  53. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  54. data/web/assets/javascripts/dashboard-charts.js +166 -0
  55. data/web/assets/javascripts/dashboard.js +3 -240
  56. data/web/assets/javascripts/metrics.js +264 -0
  57. data/web/assets/stylesheets/application-dark.css +4 -0
  58. data/web/assets/stylesheets/application-rtl.css +2 -91
  59. data/web/assets/stylesheets/application.css +66 -299
  60. data/web/locales/ar.yml +70 -70
  61. data/web/locales/cs.yml +62 -62
  62. data/web/locales/da.yml +60 -53
  63. data/web/locales/de.yml +65 -65
  64. data/web/locales/el.yml +43 -24
  65. data/web/locales/en.yml +82 -69
  66. data/web/locales/es.yml +68 -68
  67. data/web/locales/fa.yml +65 -65
  68. data/web/locales/fr.yml +67 -67
  69. data/web/locales/he.yml +65 -64
  70. data/web/locales/hi.yml +59 -59
  71. data/web/locales/it.yml +53 -53
  72. data/web/locales/ja.yml +73 -68
  73. data/web/locales/ko.yml +52 -52
  74. data/web/locales/lt.yml +66 -66
  75. data/web/locales/nb.yml +61 -61
  76. data/web/locales/nl.yml +52 -52
  77. data/web/locales/pl.yml +45 -45
  78. data/web/locales/pt-br.yml +63 -55
  79. data/web/locales/pt.yml +51 -51
  80. data/web/locales/ru.yml +67 -66
  81. data/web/locales/sv.yml +53 -53
  82. data/web/locales/ta.yml +60 -60
  83. data/web/locales/uk.yml +62 -61
  84. data/web/locales/ur.yml +64 -64
  85. data/web/locales/vi.yml +67 -67
  86. data/web/locales/zh-cn.yml +43 -16
  87. data/web/locales/zh-tw.yml +42 -8
  88. data/web/views/_footer.erb +5 -2
  89. data/web/views/_job_info.erb +18 -2
  90. data/web/views/_metrics_period_select.erb +12 -0
  91. data/web/views/_nav.erb +1 -1
  92. data/web/views/_paging.erb +2 -0
  93. data/web/views/_poll_link.erb +1 -1
  94. data/web/views/_summary.erb +1 -1
  95. data/web/views/busy.erb +42 -26
  96. data/web/views/dashboard.erb +36 -4
  97. data/web/views/metrics.erb +82 -0
  98. data/web/views/metrics_for_job.erb +71 -0
  99. data/web/views/morgue.erb +5 -9
  100. data/web/views/queue.erb +15 -15
  101. data/web/views/queues.erb +3 -1
  102. data/web/views/retries.erb +5 -9
  103. data/web/views/scheduled.erb +12 -13
  104. metadata +63 -28
  105. data/lib/sidekiq/delay.rb +0 -43
  106. data/lib/sidekiq/exception_handler.rb +0 -27
  107. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  108. data/lib/sidekiq/extensions/active_record.rb +0 -43
  109. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  110. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  111. data/lib/sidekiq/util.rb +0 -108
  112. data/lib/sidekiq/worker.rb +0 -362
  113. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/scheduled"
4
- require "sidekiq/api"
5
-
6
3
  require "zlib"
7
4
  require "base64"
5
+ require "sidekiq/component"
8
6
 
9
7
  module Sidekiq
10
8
  ##
@@ -25,11 +23,11 @@ module Sidekiq
25
23
  #
26
24
  # A job looks like:
27
25
  #
28
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
26
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
29
27
  #
30
28
  # The 'retry' option also accepts a number (in place of 'true'):
31
29
  #
32
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
30
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
33
31
  #
34
32
  # The job will be retried this number of times before giving up. (If simply
35
33
  # 'true', Sidekiq retries 25 times)
@@ -51,13 +49,13 @@ module Sidekiq
51
49
  # The default number of retries is 25 which works out to about 3 weeks
52
50
  # You can change the default maximum number of retries in your initializer:
53
51
  #
54
- # Sidekiq.options[:max_retries] = 7
52
+ # Sidekiq.default_configuration[:max_retries] = 7
55
53
  #
56
- # or limit the number of retries for a particular worker and send retries to
54
+ # or limit the number of retries for a particular job and send retries to
57
55
  # a low priority queue with:
58
56
  #
59
- # class MyWorker
60
- # include Sidekiq::Worker
57
+ # class MyJob
58
+ # include Sidekiq::Job
61
59
  # sidekiq_options retry: 10, retry_queue: 'low'
62
60
  # end
63
61
  #
@@ -66,17 +64,18 @@ module Sidekiq
66
64
 
67
65
  class Skip < Handled; end
68
66
 
69
- include Sidekiq::Util
67
+ include Sidekiq::Component
70
68
 
71
69
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
72
70
 
73
- def initialize(options = {})
74
- @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
71
+ def initialize(capsule)
72
+ @config = @capsule = capsule
73
+ @max_retries = Sidekiq.default_configuration[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
75
74
  end
76
75
 
77
76
  # The global retry handler requires only the barest of data.
78
77
  # We want to be able to retry as much as possible so we don't
79
- # require the worker to be instantiated.
78
+ # require the job to be instantiated.
80
79
  def global(jobstr, queue)
81
80
  yield
82
81
  rescue Handled => ex
@@ -90,9 +89,9 @@ module Sidekiq
90
89
 
91
90
  msg = Sidekiq.load_json(jobstr)
92
91
  if msg["retry"]
93
- attempt_retry(nil, msg, queue, e)
92
+ process_retry(nil, msg, queue, e)
94
93
  else
95
- Sidekiq.death_handlers.each do |handler|
94
+ @capsule.config.death_handlers.each do |handler|
96
95
  handler.call(msg, e)
97
96
  rescue => handler_ex
98
97
  handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
@@ -103,14 +102,14 @@ module Sidekiq
103
102
  end
104
103
 
105
104
  # The local retry support means that any errors that occur within
106
- # this block can be associated with the given worker instance.
105
+ # this block can be associated with the given job instance.
107
106
  # This is required to support the `sidekiq_retries_exhausted` block.
108
107
  #
109
108
  # Note that any exception from the block is wrapped in the Skip
110
109
  # exception so the global block does not reprocess the error. The
111
110
  # Skip exception is unwrapped within Sidekiq::Processor#process before
112
111
  # calling the handle_exception handlers.
113
- def local(worker, jobstr, queue)
112
+ def local(jobinst, jobstr, queue)
114
113
  yield
115
114
  rescue Handled => ex
116
115
  raise ex
@@ -123,11 +122,11 @@ module Sidekiq
123
122
 
124
123
  msg = Sidekiq.load_json(jobstr)
125
124
  if msg["retry"].nil?
126
- msg["retry"] = worker.class.get_sidekiq_options["retry"]
125
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
127
126
  end
128
127
 
129
128
  raise e unless msg["retry"]
130
- attempt_retry(worker, msg, queue, e)
129
+ process_retry(jobinst, msg, queue, e)
131
130
  # We've handled this error associated with this job, don't
132
131
  # need to handle it at the global level
133
132
  raise Skip
@@ -135,10 +134,10 @@ module Sidekiq
135
134
 
136
135
  private
137
136
 
138
- # Note that +worker+ can be nil here if an error is raised before we can
139
- # instantiate the worker instance. All access must be guarded and
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
140
139
  # best effort.
141
- def attempt_retry(worker, msg, queue, exception)
140
+ def process_retry(jobinst, msg, queue, exception)
142
141
  max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
143
142
 
144
143
  msg["queue"] = (msg["retry_queue"] || queue)
@@ -169,24 +168,55 @@ module Sidekiq
169
168
  msg["error_backtrace"] = compress_backtrace(lines)
170
169
  end
171
170
 
172
- if count < max_retry_attempts
173
- delay = delay_for(worker, count, exception)
174
- # Logging here can break retries if the logging device raises ENOSPC #3979
175
- # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
176
- retry_at = Time.now.to_f + delay
177
- payload = Sidekiq.dump_json(msg)
178
- Sidekiq.redis do |conn|
179
- conn.zadd("retry", retry_at.to_s, payload)
180
- end
181
- else
182
- # Goodbye dear message, you (re)tried your best I'm sure.
183
- retries_exhausted(worker, msg, exception)
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
+ rv = rv.to_i if rv.respond_to?(:to_i)
205
+ delay = (count**4) + 15
206
+ if Integer === rv && rv > 0
207
+ delay = rv
208
+ elsif rv == :discard
209
+ return [:discard, nil] # do nothing, job goes poof
210
+ elsif rv == :kill
211
+ return [:kill, nil]
184
212
  end
213
+
214
+ [:default, delay]
185
215
  end
186
216
 
187
- def retries_exhausted(worker, msg, exception)
217
+ def retries_exhausted(jobinst, msg, exception)
188
218
  begin
189
- block = worker&.sidekiq_retries_exhausted_block
219
+ block = jobinst&.sidekiq_retries_exhausted_block
190
220
  block&.call(msg, exception)
191
221
  rescue => e
192
222
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
@@ -194,7 +224,7 @@ module Sidekiq
194
224
 
195
225
  send_to_morgue(msg) unless msg["dead"] == false
196
226
 
197
- Sidekiq.death_handlers.each do |handler|
227
+ @capsule.config.death_handlers.each do |handler|
198
228
  handler.call(msg, exception)
199
229
  rescue => e
200
230
  handle_exception(e, {context: "Error calling death handler", job: msg})
@@ -204,7 +234,15 @@ module Sidekiq
204
234
  def send_to_morgue(msg)
205
235
  logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
206
236
  payload = Sidekiq.dump_json(msg)
207
- DeadSet.new.kill(payload, notify_failure: false)
237
+ now = Time.now.to_f
238
+
239
+ redis do |conn|
240
+ conn.multi do |xa|
241
+ xa.zadd("dead", now.to_s, payload)
242
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
243
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
244
+ end
245
+ end
208
246
  end
209
247
 
210
248
  def retry_attempts_from(msg_retry, default)
@@ -215,22 +253,6 @@ module Sidekiq
215
253
  end
216
254
  end
217
255
 
218
- def delay_for(worker, count, exception)
219
- jitter = rand(10) * (count + 1)
220
- if worker&.sidekiq_retry_in_block
221
- custom_retry_in = retry_in(worker, count, exception).to_i
222
- return custom_retry_in + jitter if custom_retry_in > 0
223
- end
224
- (count**4) + 15 + jitter
225
- end
226
-
227
- def retry_in(worker, count, exception)
228
- worker.sidekiq_retry_in_block.call(count, exception)
229
- rescue Exception => e
230
- handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
231
- nil
232
- end
233
-
234
256
  def exception_caused_by_shutdown?(e, checked_causes = [])
235
257
  return false unless e.cause
236
258
 
@@ -4,7 +4,8 @@ require "time"
4
4
  module Sidekiq
5
5
  module JobUtil
6
6
  # These functions encapsulate various job utilities.
7
- # They must be simple and free from side effects.
7
+
8
+ TRANSIENT_ATTRIBUTES = %w[]
8
9
 
9
10
  def validate(item)
10
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,19 +13,27 @@ module Sidekiq
12
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)
13
14
  raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
14
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
+ args = item["args"]
21
+ mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
15
22
 
16
- if Sidekiq.options[:on_complex_arguments] == :raise
17
- msg = <<~EOM
18
- Job arguments to #{item["class"]} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
19
- To disable this error, remove `Sidekiq.strict_args!` from your initializer.
20
- EOM
21
- raise(ArgumentError, msg) unless json_safe?(item)
22
- elsif Sidekiq.options[:on_complex_arguments] == :warn
23
- Sidekiq.logger.warn <<~EOM unless json_safe?(item)
24
- Job arguments to #{item["class"]} do not serialize to JSON safely. This will raise an error in
25
- Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
26
- by calling `Sidekiq.strict_args!` during Sidekiq initialization.
27
- EOM
23
+ if mode == :raise || mode == :warn
24
+ if (unsafe_item = json_unsafe?(args))
25
+ msg = <<~EOM
26
+ Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
27
+ See https://github.com/sidekiq/sidekiq/wiki/Best-Practices.
28
+ To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
29
+ EOM
30
+
31
+ if mode == :raise
32
+ raise(ArgumentError, msg)
33
+ else
34
+ warn(msg)
35
+ end
36
+ end
28
37
  end
29
38
  end
30
39
 
@@ -39,27 +48,58 @@ module Sidekiq
39
48
 
40
49
  raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
41
50
 
51
+ # remove job attributes which aren't necessary to persist into Redis
52
+ TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
53
+
54
+ item["jid"] ||= SecureRandom.hex(12)
42
55
  item["class"] = item["class"].to_s
43
56
  item["queue"] = item["queue"].to_s
44
- item["jid"] ||= SecureRandom.hex(12)
45
57
  item["created_at"] ||= Time.now.to_f
46
-
47
58
  item
48
59
  end
49
60
 
50
61
  def normalized_hash(item_class)
51
62
  if item_class.is_a?(Class)
52
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
63
+ raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
53
64
  item_class.get_sidekiq_options
54
65
  else
55
- Sidekiq.default_worker_options
66
+ Sidekiq.default_job_options
56
67
  end
57
68
  end
58
69
 
59
70
  private
60
71
 
61
- def json_safe?(item)
62
- JSON.parse(JSON.dump(item["args"])) == item["args"]
72
+ RECURSIVE_JSON_UNSAFE = {
73
+ Integer => ->(val) {},
74
+ Float => ->(val) {},
75
+ TrueClass => ->(val) {},
76
+ FalseClass => ->(val) {},
77
+ NilClass => ->(val) {},
78
+ String => ->(val) {},
79
+ Array => ->(val) {
80
+ val.each do |e|
81
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
82
+ return unsafe_item unless unsafe_item.nil?
83
+ end
84
+ nil
85
+ },
86
+ Hash => ->(val) {
87
+ val.each do |k, v|
88
+ return k unless String === k
89
+
90
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
91
+ return unsafe_item unless unsafe_item.nil?
92
+ end
93
+ nil
94
+ }
95
+ }
96
+
97
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
98
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
99
+ private_constant :RECURSIVE_JSON_UNSAFE
100
+
101
+ def json_unsafe?(item)
102
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
63
103
  end
64
104
  end
65
105
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/manager"
4
- require "sidekiq/fetch"
4
+ require "sidekiq/capsule"
5
5
  require "sidekiq/scheduled"
6
+ require "sidekiq/ring_buffer"
6
7
 
7
8
  module Sidekiq
8
- # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
9
+ # The Launcher starts the Capsule Managers, the Poller thread and provides the process heartbeat.
9
10
  class Launcher
10
- include Util
11
+ include Sidekiq::Component
11
12
 
12
13
  STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
13
14
 
@@ -15,50 +16,56 @@ module Sidekiq
15
16
  proc { "sidekiq" },
16
17
  proc { Sidekiq::VERSION },
17
18
  proc { |me, data| data["tag"] },
18
- proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
19
+ proc { |me, data| "[#{Processor::WORK_STATE.size} of #{me.config.total_concurrency} busy]" },
19
20
  proc { |me, data| "stopping" if me.stopping? }
20
21
  ]
21
22
 
22
- attr_accessor :manager, :poller, :fetcher
23
+ attr_accessor :managers, :poller
23
24
 
24
- def initialize(options)
25
- options[:fetch] ||= BasicFetch.new(options)
26
- @manager = Sidekiq::Manager.new(options)
27
- @poller = Sidekiq::Scheduled::Poller.new
25
+ def initialize(config, embedded: false)
26
+ @config = config
27
+ @embedded = embedded
28
+ @managers = config.capsules.values.map do |cap|
29
+ Sidekiq::Manager.new(cap)
30
+ end
31
+ @poller = Sidekiq::Scheduled::Poller.new(@config)
28
32
  @done = false
29
- @options = options
30
33
  end
31
34
 
32
- def run
33
- @thread = safe_thread("heartbeat", &method(:start_heartbeat))
35
+ # Start this Sidekiq instance. If an embedding process already
36
+ # has a heartbeat thread, caller can use `async_beat: false`
37
+ # and instead have thread call Launcher#heartbeat every N seconds.
38
+ def run(async_beat: true)
39
+ Sidekiq.freeze!
40
+ logger.debug { @config.merge!({}) }
41
+ @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
34
42
  @poller.start
35
- @manager.start
43
+ @managers.each(&:start)
36
44
  end
37
45
 
38
46
  # Stops this instance from processing any more jobs,
39
- #
40
47
  def quiet
48
+ return if @done
49
+
41
50
  @done = true
42
- @manager.quiet
51
+ @managers.each(&:quiet)
43
52
  @poller.terminate
53
+ fire_event(:quiet, reverse: true)
44
54
  end
45
55
 
46
- # Shuts down the process. This method does not
47
- # return until all work is complete and cleaned up.
48
- # It can take up to the timeout to complete.
56
+ # Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
49
57
  def stop
50
- deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
58
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
51
59
 
52
- @done = true
53
- @manager.quiet
54
- @poller.terminate
55
-
56
- @manager.stop(deadline)
60
+ quiet
61
+ stoppers = @managers.map do |mgr|
62
+ Thread.new do
63
+ mgr.stop(deadline)
64
+ end
65
+ end
57
66
 
58
- # Requeue everything in case there was a worker who grabbed work while stopped
59
- # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
60
- strategy = @options[:fetch]
61
- strategy.bulk_requeue([], @options)
67
+ fire_event(:shutdown, reverse: true)
68
+ stoppers.each(&:join)
62
69
 
63
70
  clear_heartbeat
64
71
  end
@@ -67,46 +74,54 @@ module Sidekiq
67
74
  @done
68
75
  end
69
76
 
77
+ # If embedding Sidekiq, you can have the process heartbeat
78
+ # call this method to regularly heartbeat rather than creating
79
+ # a separate thread.
80
+ def heartbeat
81
+
82
+ end
83
+
70
84
  private unless $TESTING
71
85
 
72
- BEAT_PAUSE = 5
86
+ BEAT_PAUSE = 10
73
87
 
74
88
  def start_heartbeat
75
89
  loop do
76
- heartbeat
90
+ beat
77
91
  sleep BEAT_PAUSE
78
92
  end
79
- Sidekiq.logger.info("Heartbeat stopping...")
93
+ logger.info("Heartbeat stopping...")
94
+ end
95
+
96
+ def beat
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
98
+
80
99
  end
81
100
 
82
101
  def clear_heartbeat
102
+ flush_stats
103
+
83
104
  # Remove record from Redis since we are shutting down.
84
105
  # Note we don't stop the heartbeat thread; if the process
85
106
  # doesn't actually exit, it'll reappear in the Web UI.
86
- Sidekiq.redis do |conn|
107
+ redis do |conn|
87
108
  conn.pipelined do |pipeline|
88
- pipeline.srem("processes", identity)
89
- pipeline.unlink("#{identity}:workers")
109
+ pipeline.srem("processes", [identity])
110
+ pipeline.unlink("#{identity}:work")
90
111
  end
91
112
  end
92
113
  rescue
93
114
  # best effort, ignore network errors
94
115
  end
95
116
 
96
- def heartbeat
97
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
98
-
99
-
100
- end
101
-
102
- def self.flush_stats
117
+ def flush_stats
103
118
  fails = Processor::FAILURE.reset
104
119
  procd = Processor::PROCESSED.reset
105
120
  return if fails + procd == 0
106
121
 
107
122
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
108
123
  begin
109
- Sidekiq.redis do |conn|
124
+ redis do |conn|
110
125
  conn.pipelined do |pipeline|
111
126
  pipeline.incrby("stat:processed", procd)
112
127
  pipeline.incrby("stat:processed:#{nowdate}", procd)
@@ -118,40 +133,27 @@ module Sidekiq
118
133
  end
119
134
  end
120
135
  rescue => ex
121
- # we're exiting the process, things might be shut down so don't
122
- # try to handle the exception
123
- Sidekiq.logger.warn("Unable to flush stats: #{ex}")
136
+ logger.warn("Unable to flush stats: #{ex}")
124
137
  end
125
138
  end
126
- at_exit(&method(:flush_stats))
127
139
 
128
140
  def ❤
129
141
  key = identity
130
142
  fails = procd = 0
131
143
 
132
144
  begin
133
- fails = Processor::FAILURE.reset
134
- procd = Processor::PROCESSED.reset
135
- curstate = Processor::WORKER_STATE.dup
136
-
137
- workers_key = "#{key}:workers"
138
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
139
-
140
- Sidekiq.redis do |conn|
141
- conn.multi do |transaction|
142
- transaction.incrby("stat:processed", procd)
143
- transaction.incrby("stat:processed:#{nowdate}", procd)
144
- transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
145
-
146
- transaction.incrby("stat:failed", fails)
147
- transaction.incrby("stat:failed:#{nowdate}", fails)
148
- transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
149
-
150
- transaction.unlink(workers_key)
145
+ flush_stats
146
+
147
+ curstate = Processor::WORK_STATE.dup
148
+ redis do |conn|
149
+ # work is the current set of executing jobs
150
+ work_key = "#{key}:work"
151
+ conn.pipelined do |transaction|
152
+ transaction.unlink(work_key)
151
153
  curstate.each_pair do |tid, hash|
152
- transaction.hset(workers_key, tid, Sidekiq.dump_json(hash))
154
+ transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
153
155
  end
154
- transaction.expire(workers_key, 60)
156
+ transaction.expire(work_key, 60)
155
157
  end
156
158
  end
157
159
 
@@ -160,15 +162,15 @@ module Sidekiq
160
162
  fails = procd = 0
161
163
  kb = memory_usage(::Process.pid)
162
164
 
163
- _, exists, _, _, msg = Sidekiq.redis { |conn|
165
+ _, exists, _, _, signal = redis { |conn|
164
166
  conn.multi { |transaction|
165
- transaction.sadd("processes", key)
166
- transaction.exists?(key)
167
+ transaction.sadd("processes", [key])
168
+ transaction.exists(key)
167
169
  transaction.hmset(key, "info", to_json,
168
170
  "busy", curstate.size,
169
171
  "beat", Time.now.to_f,
170
172
  "rtt_us", rtt,
171
- "quiet", @done,
173
+ "quiet", @done.to_s,
172
174
  "rss", kb)
173
175
  transaction.expire(key, 60)
174
176
  transaction.rpop("#{key}-signals")
@@ -176,11 +178,10 @@ module Sidekiq
176
178
  }
177
179
 
178
180
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
179
- fire_event(:heartbeat) unless exists
181
+ fire_event(:heartbeat) unless exists > 0
182
+ fire_event(:beat, oneshot: false)
180
183
 
181
- return unless msg
182
-
183
- ::Process.kill(msg, ::Process.pid)
184
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
184
185
  rescue => e
185
186
  # ignore all redis/network issues
186
187
  logger.error("heartbeat: #{e}")
@@ -198,7 +199,7 @@ module Sidekiq
198
199
 
199
200
  def check_rtt
200
201
  a = b = 0
201
- Sidekiq.redis do |x|
202
+ redis do |x|
202
203
  a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
203
204
  x.ping
204
205
  b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
@@ -209,12 +210,12 @@ module Sidekiq
209
210
  # Workable is < 10,000µs
210
211
  # Log a warning if it's a disaster.
211
212
  if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
212
- Sidekiq.logger.warn <<~EOM
213
+ logger.warn <<~EOM
213
214
  Your Redis network connection is performing extremely poorly.
214
215
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
215
216
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
216
217
  If these values are close to 100,000, that means your Sidekiq process may be
217
- CPU overloaded; see https://github.com/mperham/sidekiq/discussions/5039
218
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
218
219
  EOM
219
220
  RTT_READINGS.reset
220
221
  end
@@ -246,14 +247,21 @@ module Sidekiq
246
247
  "hostname" => hostname,
247
248
  "started_at" => Time.now.to_f,
248
249
  "pid" => ::Process.pid,
249
- "tag" => @options[:tag] || "",
250
- "concurrency" => @options[:concurrency],
251
- "queues" => @options[:queues].uniq,
252
- "labels" => @options[:labels],
253
- "identity" => identity
250
+ "tag" => @config[:tag] || "",
251
+ "concurrency" => @config.total_concurrency,
252
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
253
+ "weights" => to_weights,
254
+ "labels" => @config[:labels].to_a,
255
+ "identity" => identity,
256
+ "version" => Sidekiq::VERSION,
257
+ "embedded" => @embedded
254
258
  }
255
259
  end
256
260
 
261
+ def to_weights
262
+ @config.capsules.values.map(&:weights)
263
+ end
264
+
257
265
  def to_json
258
266
  # this data changes infrequently so dump it to a string
259
267
  # now so we don't need to dump it every heartbeat.