sidekiq 6.4.0 → 6.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +119 -1
  3. data/README.md +6 -1
  4. data/bin/sidekiq +3 -3
  5. data/bin/sidekiqload +70 -66
  6. data/bin/sidekiqmon +1 -1
  7. data/lib/sidekiq/api.rb +255 -100
  8. data/lib/sidekiq/cli.rb +60 -38
  9. data/lib/sidekiq/client.rb +44 -30
  10. data/lib/sidekiq/component.rb +65 -0
  11. data/lib/sidekiq/delay.rb +2 -2
  12. data/lib/sidekiq/extensions/action_mailer.rb +2 -2
  13. data/lib/sidekiq/extensions/active_record.rb +2 -2
  14. data/lib/sidekiq/extensions/class_methods.rb +2 -2
  15. data/lib/sidekiq/extensions/generic_proxy.rb +3 -3
  16. data/lib/sidekiq/fetch.rb +20 -18
  17. data/lib/sidekiq/job_logger.rb +15 -27
  18. data/lib/sidekiq/job_retry.rb +73 -52
  19. data/lib/sidekiq/job_util.rb +15 -9
  20. data/lib/sidekiq/launcher.rb +58 -54
  21. data/lib/sidekiq/logger.rb +8 -18
  22. data/lib/sidekiq/manager.rb +28 -25
  23. data/lib/sidekiq/metrics/deploy.rb +47 -0
  24. data/lib/sidekiq/metrics/query.rb +153 -0
  25. data/lib/sidekiq/metrics/shared.rb +94 -0
  26. data/lib/sidekiq/metrics/tracking.rb +134 -0
  27. data/lib/sidekiq/middleware/chain.rb +82 -38
  28. data/lib/sidekiq/middleware/current_attributes.rb +18 -12
  29. data/lib/sidekiq/middleware/i18n.rb +6 -4
  30. data/lib/sidekiq/middleware/modules.rb +21 -0
  31. data/lib/sidekiq/monitor.rb +2 -2
  32. data/lib/sidekiq/paginator.rb +17 -9
  33. data/lib/sidekiq/processor.rb +47 -41
  34. data/lib/sidekiq/rails.rb +19 -13
  35. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  36. data/lib/sidekiq/redis_connection.rb +80 -49
  37. data/lib/sidekiq/ring_buffer.rb +29 -0
  38. data/lib/sidekiq/scheduled.rb +53 -24
  39. data/lib/sidekiq/testing/inline.rb +4 -4
  40. data/lib/sidekiq/testing.rb +37 -36
  41. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  42. data/lib/sidekiq/version.rb +1 -1
  43. data/lib/sidekiq/web/action.rb +3 -3
  44. data/lib/sidekiq/web/application.rb +21 -5
  45. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  46. data/lib/sidekiq/web/helpers.rb +21 -8
  47. data/lib/sidekiq/web.rb +8 -4
  48. data/lib/sidekiq/worker.rb +26 -20
  49. data/lib/sidekiq.rb +107 -31
  50. data/sidekiq.gemspec +2 -2
  51. data/web/assets/javascripts/application.js +59 -26
  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.js +0 -17
  55. data/web/assets/javascripts/graph.js +16 -0
  56. data/web/assets/javascripts/metrics.js +262 -0
  57. data/web/assets/stylesheets/application.css +45 -1
  58. data/web/locales/el.yml +43 -19
  59. data/web/locales/en.yml +7 -0
  60. data/web/locales/ja.yml +7 -0
  61. data/web/locales/pt-br.yml +27 -9
  62. data/web/locales/zh-cn.yml +36 -11
  63. data/web/locales/zh-tw.yml +32 -7
  64. data/web/views/_nav.erb +1 -1
  65. data/web/views/_summary.erb +1 -1
  66. data/web/views/busy.erb +9 -4
  67. data/web/views/dashboard.erb +1 -0
  68. data/web/views/metrics.erb +69 -0
  69. data/web/views/metrics_for_job.erb +87 -0
  70. data/web/views/queue.erb +5 -1
  71. metadata +34 -9
  72. data/lib/sidekiq/exception_handler.rb +0 -27
  73. data/lib/sidekiq/util.rb +0 -108
data/lib/sidekiq.rb CHANGED
@@ -5,6 +5,7 @@ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." i
5
5
 
6
6
  require "sidekiq/logger"
7
7
  require "sidekiq/client"
8
+ require "sidekiq/transaction_aware_client"
8
9
  require "sidekiq/worker"
9
10
  require "sidekiq/job"
10
11
  require "sidekiq/redis_connection"
@@ -33,18 +34,16 @@ module Sidekiq
33
34
  startup: [],
34
35
  quiet: [],
35
36
  shutdown: [],
36
- heartbeat: []
37
+ # triggers when we fire the first heartbeat on startup OR repairing a network partition
38
+ heartbeat: [],
39
+ # triggers on EVERY heartbeat call, every 10 seconds
40
+ beat: []
37
41
  },
38
42
  dead_max_jobs: 10_000,
39
43
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
40
44
  reloader: proc { |&block| block.call }
41
45
  }
42
46
 
43
- DEFAULT_WORKER_OPTIONS = {
44
- "retry" => true,
45
- "queue" => "default"
46
- }
47
-
48
47
  FAKE_INFO = {
49
48
  "redis_version" => "9.9.9",
50
49
  "uptime_in_days" => "9999",
@@ -57,19 +56,84 @@ module Sidekiq
57
56
  puts "Calm down, yo."
58
57
  end
59
58
 
59
+ # config.concurrency = 5
60
+ def self.concurrency=(val)
61
+ self[:concurrency] = Integer(val)
62
+ end
63
+
64
+ # config.queues = %w( high default low ) # strict
65
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
66
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
67
+ #
68
+ # With weighted priority, queue will be checked first (weight / total) of the time.
69
+ # high will be checked first (3/6) or 50% of the time.
70
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
71
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
72
+ # by explicitly setting all weights to 1.
73
+ def self.queues=(val)
74
+ self[:queues] = Array(val).each_with_object([]) do |qstr, memo|
75
+ name, weight = qstr.split(",")
76
+ self[:strict] = false if weight.to_i > 0
77
+ [weight.to_i, 1].max.times do
78
+ memo << name
79
+ end
80
+ end
81
+ end
82
+
83
+ ### Private APIs
84
+ def self.default_error_handler(ex, ctx)
85
+ logger.warn(dump_json(ctx)) unless ctx.empty?
86
+ logger.warn("#{ex.class.name}: #{ex.message}")
87
+ logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
88
+ end
89
+
90
+ # DEFAULT_ERROR_HANDLER is a constant that allows the default error handler to
91
+ # be referenced. It must be defined here, after the default_error_handler
92
+ # method is defined.
93
+ DEFAULT_ERROR_HANDLER = method(:default_error_handler)
94
+
95
+ @config = DEFAULTS.dup
60
96
  def self.options
61
- @options ||= DEFAULTS.dup
97
+ logger.warn "`config.options[:key] = value` is deprecated, use `config[:key] = value`: #{caller(1..2)}"
98
+ @config
62
99
  end
63
100
 
64
101
  def self.options=(opts)
65
- @options = opts
102
+ logger.warn "config.options = hash` is deprecated, use `config.merge!(hash)`: #{caller(1..2)}"
103
+ @config = opts
66
104
  end
67
105
 
106
+ def self.[](key)
107
+ @config[key]
108
+ end
109
+
110
+ def self.[]=(key, val)
111
+ @config[key] = val
112
+ end
113
+
114
+ def self.merge!(hash)
115
+ @config.merge!(hash)
116
+ end
117
+
118
+ def self.fetch(*args, &block)
119
+ @config.fetch(*args, &block)
120
+ end
121
+
122
+ def self.handle_exception(ex, ctx = {})
123
+ self[:error_handlers].each do |handler|
124
+ handler.call(ex, ctx)
125
+ rescue => ex
126
+ logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
127
+ logger.error ex
128
+ logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
129
+ end
130
+ end
131
+ ###
132
+
68
133
  ##
69
134
  # Configuration for Sidekiq server, use like:
70
135
  #
71
136
  # Sidekiq.configure_server do |config|
72
- # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' }
73
137
  # config.server_middleware do |chain|
74
138
  # chain.add MyServerHook
75
139
  # end
@@ -82,7 +146,7 @@ module Sidekiq
82
146
  # Configuration for Sidekiq client, use like:
83
147
  #
84
148
  # Sidekiq.configure_client do |config|
85
- # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' }
149
+ # config.redis = { size: 1, url: 'redis://myhost:8877/0' }
86
150
  # end
87
151
  def self.configure_client
88
152
  yield self unless server?
@@ -98,11 +162,12 @@ module Sidekiq
98
162
  retryable = true
99
163
  begin
100
164
  yield conn
101
- rescue Redis::BaseError => ex
165
+ rescue RedisConnection.adapter::BaseError => ex
102
166
  # 2550 Failover can cause the server to become a replica, need
103
167
  # to disconnect and reopen the socket to get back to the primary.
104
168
  # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
105
169
  # 4985 Use the same logic when a blocking command is force-unblocked
170
+ # The same retry logic is also used in client.rb
106
171
  if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
107
172
  conn.disconnect!
108
173
  retryable = false
@@ -122,7 +187,7 @@ module Sidekiq
122
187
  else
123
188
  conn.info
124
189
  end
125
- rescue Redis::CommandError => ex
190
+ rescue RedisConnection.adapter::CommandError => ex
126
191
  # 2850 return fake version when INFO command has (probably) been renamed
127
192
  raise unless /unknown command/.match?(ex.message)
128
193
  FAKE_INFO
@@ -130,19 +195,19 @@ module Sidekiq
130
195
  end
131
196
 
132
197
  def self.redis_pool
133
- @redis ||= Sidekiq::RedisConnection.create
198
+ @redis ||= RedisConnection.create
134
199
  end
135
200
 
136
201
  def self.redis=(hash)
137
202
  @redis = if hash.is_a?(ConnectionPool)
138
203
  hash
139
204
  else
140
- Sidekiq::RedisConnection.create(hash)
205
+ RedisConnection.create(hash)
141
206
  end
142
207
  end
143
208
 
144
209
  def self.client_middleware
145
- @client_chain ||= Middleware::Chain.new
210
+ @client_chain ||= Middleware::Chain.new(self)
146
211
  yield @client_chain if block_given?
147
212
  @client_chain
148
213
  end
@@ -154,16 +219,23 @@ module Sidekiq
154
219
  end
155
220
 
156
221
  def self.default_server_middleware
157
- Middleware::Chain.new
222
+ Middleware::Chain.new(self)
223
+ end
224
+
225
+ def self.default_worker_options=(hash) # deprecated
226
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
158
227
  end
159
228
 
160
- def self.default_worker_options=(hash)
161
- # stringify
162
- @default_worker_options = default_worker_options.merge(hash.transform_keys(&:to_s))
229
+ def self.default_job_options=(hash)
230
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
163
231
  end
164
232
 
165
- def self.default_worker_options
166
- defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
233
+ def self.default_worker_options # deprecated
234
+ @default_job_options ||= {"retry" => true, "queue" => "default"}
235
+ end
236
+
237
+ def self.default_job_options
238
+ @default_job_options ||= {"retry" => true, "queue" => "default"}
167
239
  end
168
240
 
169
241
  ##
@@ -176,7 +248,7 @@ module Sidekiq
176
248
  # end
177
249
  # end
178
250
  def self.death_handlers
179
- options[:death_handlers]
251
+ self[:death_handlers]
180
252
  end
181
253
 
182
254
  def self.load_json(string)
@@ -201,7 +273,7 @@ module Sidekiq
201
273
  end
202
274
 
203
275
  def self.logger
204
- @logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO)
276
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info)
205
277
  end
206
278
 
207
279
  def self.logger=(logger)
@@ -219,13 +291,17 @@ module Sidekiq
219
291
  defined?(Sidekiq::Pro)
220
292
  end
221
293
 
294
+ def self.ent?
295
+ defined?(Sidekiq::Enterprise)
296
+ end
297
+
222
298
  # How frequently Redis should be checked by a random Sidekiq process for
223
299
  # scheduled and retriable jobs. Each individual process will take turns by
224
300
  # waiting some multiple of this value.
225
301
  #
226
302
  # See sidekiq/scheduled.rb for an in-depth explanation of this value
227
303
  def self.average_scheduled_poll_interval=(interval)
228
- options[:average_scheduled_poll_interval] = interval
304
+ self[:average_scheduled_poll_interval] = interval
229
305
  end
230
306
 
231
307
  # Register a proc to handle any error which occurs within the Sidekiq process.
@@ -236,7 +312,7 @@ module Sidekiq
236
312
  #
237
313
  # The default error handler logs errors to Sidekiq.logger.
238
314
  def self.error_handlers
239
- options[:error_handlers]
315
+ self[:error_handlers]
240
316
  end
241
317
 
242
318
  # Register a block to run at a point in the Sidekiq lifecycle.
@@ -249,20 +325,20 @@ module Sidekiq
249
325
  # end
250
326
  def self.on(event, &block)
251
327
  raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
252
- raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
253
- options[:lifecycle_events][event] << block
328
+ raise ArgumentError, "Invalid event name: #{event}" unless self[:lifecycle_events].key?(event)
329
+ self[:lifecycle_events][event] << block
254
330
  end
255
331
 
256
332
  def self.strict_args!(mode = :raise)
257
- options[:on_complex_arguments] = mode
333
+ self[:on_complex_arguments] = mode
258
334
  end
259
335
 
260
- # We are shutting down Sidekiq but what about workers that
336
+ # We are shutting down Sidekiq but what about threads that
261
337
  # are working on some long job? This error is
262
- # raised in workers that have not finished within the hard
338
+ # raised in jobs that have not finished within the hard
263
339
  # timeout limit. This is needed to rollback db transactions,
264
340
  # otherwise Ruby's Thread#kill will commit. See #377.
265
- # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
341
+ # DO NOT RESCUE THIS ERROR IN YOUR JOBS
266
342
  class Shutdown < Interrupt; end
267
343
  end
268
344
 
data/sidekiq.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |gem|
22
22
  "source_code_uri" => "https://github.com/mperham/sidekiq"
23
23
  }
24
24
 
25
- gem.add_dependency "redis", ">= 4.2.0"
26
- gem.add_dependency "connection_pool", ">= 2.2.2"
25
+ gem.add_dependency "redis", ["<5", ">= 4.5.0"]
26
+ gem.add_dependency "connection_pool", ["<3", ">= 2.2.5"]
27
27
  gem.add_dependency "rack", "~> 2.0"
28
28
  end
@@ -9,7 +9,9 @@ var ready = (callback) => {
9
9
  else document.addEventListener("DOMContentLoaded", callback);
10
10
  }
11
11
 
12
- ready(() => {
12
+ ready(addListeners)
13
+
14
+ function addListeners() {
13
15
  document.querySelectorAll(".check_all").forEach(node => {
14
16
  node.addEventListener("click", event => {
15
17
  node.closest('table').querySelectorAll('input[type=checkbox]').forEach(inp => { inp.checked = !!node.checked; });
@@ -26,42 +28,48 @@ ready(() => {
26
28
  })
27
29
 
28
30
  document.querySelectorAll("[data-toggle]").forEach(node => {
29
- node.addEventListener("click", event => {
30
- var targName = node.getAttribute("data-toggle");
31
- var full = document.getElementById(targName + "_full");
32
- if (full.style.display == "block") {
33
- full.style.display = 'none';
34
- } else {
35
- full.style.display = 'block';
36
- }
37
- })
31
+ node.addEventListener("click", addDataToggleListeners)
38
32
  })
39
33
 
40
34
  updateFuzzyTimes();
35
+ setLivePollFromUrl();
41
36
 
42
37
  var buttons = document.querySelectorAll(".live-poll");
43
38
  if (buttons.length > 0) {
44
39
  buttons.forEach(node => {
45
- node.addEventListener("click", event => {
46
- if (localStorage.sidekiqLivePoll == "enabled") {
47
- localStorage.sidekiqLivePoll = "disabled";
48
- clearTimeout(livePollTimer);
49
- livePollTimer = null;
50
- } else {
51
- localStorage.sidekiqLivePoll = "enabled";
52
- livePollCallback();
53
- }
54
-
55
- updateLivePollButton();
56
- })
40
+ node.addEventListener("click", addPollingListeners)
57
41
  });
58
42
 
59
43
  updateLivePollButton();
60
- if (localStorage.sidekiqLivePoll == "enabled") {
44
+ if (localStorage.sidekiqLivePoll == "enabled" && !livePollTimer) {
61
45
  scheduleLivePoll();
62
46
  }
63
47
  }
64
- })
48
+ }
49
+
50
+ function addPollingListeners(_event) {
51
+ if (localStorage.sidekiqLivePoll == "enabled") {
52
+ localStorage.sidekiqLivePoll = "disabled";
53
+ clearTimeout(livePollTimer);
54
+ livePollTimer = null;
55
+ } else {
56
+ localStorage.sidekiqLivePoll = "enabled";
57
+ livePollCallback();
58
+ }
59
+
60
+ updateLivePollButton();
61
+ }
62
+
63
+ function addDataToggleListeners(event) {
64
+ var source = event.target || event.srcElement;
65
+ var targName = source.getAttribute("data-toggle");
66
+ var full = document.getElementById(targName);
67
+ if (full.style.display == "block") {
68
+ full.style.display = 'none';
69
+ } else {
70
+ full.style.display = 'block';
71
+ }
72
+ }
65
73
 
66
74
  function updateFuzzyTimes() {
67
75
  var locale = document.body.getAttribute("data-locale");
@@ -76,6 +84,14 @@ function updateFuzzyTimes() {
76
84
  t.cancel();
77
85
  }
78
86
 
87
+ function setLivePollFromUrl() {
88
+ var url_params = new URL(window.location.href).searchParams
89
+
90
+ if (url_params.get("poll") == "true") {
91
+ localStorage.sidekiqLivePoll = "enabled";
92
+ }
93
+ }
94
+
79
95
  function updateLivePollButton() {
80
96
  if (localStorage.sidekiqLivePoll == "enabled") {
81
97
  document.querySelectorAll('.live-poll-stop').forEach(box => { box.style.display = "inline-block" })
@@ -89,11 +105,24 @@ function updateLivePollButton() {
89
105
  function livePollCallback() {
90
106
  clearTimeout(livePollTimer);
91
107
 
92
- fetch(window.location.href).then(resp => resp.text()).then(replacePage).finally(scheduleLivePoll)
108
+ fetch(window.location.href)
109
+ .then(checkResponse)
110
+ .then(resp => resp.text())
111
+ .then(replacePage)
112
+ .catch(showError)
113
+ .finally(scheduleLivePoll)
114
+ }
115
+
116
+ function checkResponse(resp) {
117
+ if (!resp.ok) {
118
+ throw response.error();
119
+ }
120
+ return resp
93
121
  }
94
122
 
95
123
  function scheduleLivePoll() {
96
124
  let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000;
125
+ if (ti < 2000) { ti = 2000 }
97
126
  livePollTimer = setTimeout(livePollCallback, ti);
98
127
  }
99
128
 
@@ -107,5 +136,9 @@ function replacePage(text) {
107
136
  var header_status = doc.querySelector('.status')
108
137
  document.querySelector('.status').replaceWith(header_status)
109
138
 
110
- updateFuzzyTimes();
139
+ addListeners();
140
+ }
141
+
142
+ function showError(error) {
143
+ console.error(error)
111
144
  }