sidekiq 6.3.1 → 6.5.9

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +134 -0
  3. data/LICENSE +3 -3
  4. data/README.md +7 -2
  5. data/bin/sidekiq +3 -3
  6. data/bin/sidekiqload +70 -66
  7. data/bin/sidekiqmon +1 -1
  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/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +261 -104
  13. data/lib/sidekiq/cli.rb +62 -38
  14. data/lib/sidekiq/client.rb +47 -67
  15. data/lib/sidekiq/{util.rb → component.rb} +12 -42
  16. data/lib/sidekiq/delay.rb +3 -1
  17. data/lib/sidekiq/extensions/generic_proxy.rb +1 -1
  18. data/lib/sidekiq/fetch.rb +20 -18
  19. data/lib/sidekiq/job_logger.rb +15 -27
  20. data/lib/sidekiq/job_retry.rb +78 -55
  21. data/lib/sidekiq/job_util.rb +71 -0
  22. data/lib/sidekiq/launcher.rb +58 -54
  23. data/lib/sidekiq/logger.rb +8 -18
  24. data/lib/sidekiq/manager.rb +35 -34
  25. data/lib/sidekiq/metrics/deploy.rb +47 -0
  26. data/lib/sidekiq/metrics/query.rb +153 -0
  27. data/lib/sidekiq/metrics/shared.rb +94 -0
  28. data/lib/sidekiq/metrics/tracking.rb +134 -0
  29. data/lib/sidekiq/middleware/chain.rb +82 -38
  30. data/lib/sidekiq/middleware/current_attributes.rb +19 -8
  31. data/lib/sidekiq/middleware/i18n.rb +6 -4
  32. data/lib/sidekiq/middleware/modules.rb +21 -0
  33. data/lib/sidekiq/monitor.rb +2 -2
  34. data/lib/sidekiq/paginator.rb +17 -9
  35. data/lib/sidekiq/processor.rb +47 -41
  36. data/lib/sidekiq/rails.rb +15 -8
  37. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  38. data/lib/sidekiq/redis_connection.rb +80 -49
  39. data/lib/sidekiq/ring_buffer.rb +29 -0
  40. data/lib/sidekiq/scheduled.rb +66 -27
  41. data/lib/sidekiq/testing/inline.rb +4 -4
  42. data/lib/sidekiq/testing.rb +37 -36
  43. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  44. data/lib/sidekiq/version.rb +1 -1
  45. data/lib/sidekiq/web/action.rb +3 -3
  46. data/lib/sidekiq/web/application.rb +26 -7
  47. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  48. data/lib/sidekiq/web/helpers.rb +21 -8
  49. data/lib/sidekiq/web.rb +8 -4
  50. data/lib/sidekiq/worker.rb +78 -19
  51. data/lib/sidekiq.rb +111 -30
  52. data/sidekiq.gemspec +2 -2
  53. data/web/assets/javascripts/application.js +58 -26
  54. data/web/assets/javascripts/chart.min.js +13 -0
  55. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  56. data/web/assets/javascripts/dashboard.js +0 -17
  57. data/web/assets/javascripts/graph.js +16 -0
  58. data/web/assets/javascripts/metrics.js +262 -0
  59. data/web/assets/stylesheets/application-dark.css +13 -17
  60. data/web/assets/stylesheets/application.css +48 -6
  61. data/web/locales/el.yml +43 -19
  62. data/web/locales/en.yml +7 -0
  63. data/web/locales/ja.yml +7 -0
  64. data/web/locales/pt-br.yml +27 -9
  65. data/web/locales/zh-cn.yml +36 -11
  66. data/web/locales/zh-tw.yml +32 -7
  67. data/web/views/_nav.erb +1 -1
  68. data/web/views/_summary.erb +1 -1
  69. data/web/views/busy.erb +9 -4
  70. data/web/views/dashboard.erb +1 -0
  71. data/web/views/metrics.erb +69 -0
  72. data/web/views/metrics_for_job.erb +87 -0
  73. data/web/views/queue.erb +5 -1
  74. metadata +39 -13
  75. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  76. data/lib/sidekiq/exception_handler.rb +0 -27
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"
@@ -26,24 +27,23 @@ module Sidekiq
26
27
  timeout: 25,
27
28
  poll_interval_average: nil,
28
29
  average_scheduled_poll_interval: 5,
30
+ on_complex_arguments: :warn,
29
31
  error_handlers: [],
30
32
  death_handlers: [],
31
33
  lifecycle_events: {
32
34
  startup: [],
33
35
  quiet: [],
34
36
  shutdown: [],
35
- 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: []
36
41
  },
37
42
  dead_max_jobs: 10_000,
38
43
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
39
44
  reloader: proc { |&block| block.call }
40
45
  }
41
46
 
42
- DEFAULT_WORKER_OPTIONS = {
43
- "retry" => true,
44
- "queue" => "default"
45
- }
46
-
47
47
  FAKE_INFO = {
48
48
  "redis_version" => "9.9.9",
49
49
  "uptime_in_days" => "9999",
@@ -56,19 +56,84 @@ module Sidekiq
56
56
  puts "Calm down, yo."
57
57
  end
58
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
59
96
  def self.options
60
- @options ||= DEFAULTS.dup
97
+ logger.warn "`config.options[:key] = value` is deprecated, use `config[:key] = value`: #{caller(1..2)}"
98
+ @config
61
99
  end
62
100
 
63
101
  def self.options=(opts)
64
- @options = opts
102
+ logger.warn "config.options = hash` is deprecated, use `config.merge!(hash)`: #{caller(1..2)}"
103
+ @config = opts
104
+ end
105
+
106
+ def self.[](key)
107
+ @config[key]
108
+ end
109
+
110
+ def self.[]=(key, val)
111
+ @config[key] = val
65
112
  end
66
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
+
67
133
  ##
68
134
  # Configuration for Sidekiq server, use like:
69
135
  #
70
136
  # Sidekiq.configure_server do |config|
71
- # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' }
72
137
  # config.server_middleware do |chain|
73
138
  # chain.add MyServerHook
74
139
  # end
@@ -81,7 +146,7 @@ module Sidekiq
81
146
  # Configuration for Sidekiq client, use like:
82
147
  #
83
148
  # Sidekiq.configure_client do |config|
84
- # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' }
149
+ # config.redis = { size: 1, url: 'redis://myhost:8877/0' }
85
150
  # end
86
151
  def self.configure_client
87
152
  yield self unless server?
@@ -97,11 +162,12 @@ module Sidekiq
97
162
  retryable = true
98
163
  begin
99
164
  yield conn
100
- rescue Redis::BaseError => ex
165
+ rescue RedisConnection.adapter::BaseError => ex
101
166
  # 2550 Failover can cause the server to become a replica, need
102
167
  # to disconnect and reopen the socket to get back to the primary.
103
168
  # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
104
169
  # 4985 Use the same logic when a blocking command is force-unblocked
170
+ # The same retry logic is also used in client.rb
105
171
  if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
106
172
  conn.disconnect!
107
173
  retryable = false
@@ -121,7 +187,7 @@ module Sidekiq
121
187
  else
122
188
  conn.info
123
189
  end
124
- rescue Redis::CommandError => ex
190
+ rescue RedisConnection.adapter::CommandError => ex
125
191
  # 2850 return fake version when INFO command has (probably) been renamed
126
192
  raise unless /unknown command/.match?(ex.message)
127
193
  FAKE_INFO
@@ -129,19 +195,19 @@ module Sidekiq
129
195
  end
130
196
 
131
197
  def self.redis_pool
132
- @redis ||= Sidekiq::RedisConnection.create
198
+ @redis ||= RedisConnection.create
133
199
  end
134
200
 
135
201
  def self.redis=(hash)
136
202
  @redis = if hash.is_a?(ConnectionPool)
137
203
  hash
138
204
  else
139
- Sidekiq::RedisConnection.create(hash)
205
+ RedisConnection.create(hash)
140
206
  end
141
207
  end
142
208
 
143
209
  def self.client_middleware
144
- @client_chain ||= Middleware::Chain.new
210
+ @client_chain ||= Middleware::Chain.new(self)
145
211
  yield @client_chain if block_given?
146
212
  @client_chain
147
213
  end
@@ -153,16 +219,23 @@ module Sidekiq
153
219
  end
154
220
 
155
221
  def self.default_server_middleware
156
- Middleware::Chain.new
222
+ Middleware::Chain.new(self)
157
223
  end
158
224
 
159
- def self.default_worker_options=(hash)
160
- # stringify
161
- @default_worker_options = default_worker_options.merge(hash.transform_keys(&:to_s))
225
+ def self.default_worker_options=(hash) # deprecated
226
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
162
227
  end
163
228
 
164
- def self.default_worker_options
165
- defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
229
+ def self.default_job_options=(hash)
230
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
231
+ end
232
+
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"}
166
239
  end
167
240
 
168
241
  ##
@@ -175,7 +248,7 @@ module Sidekiq
175
248
  # end
176
249
  # end
177
250
  def self.death_handlers
178
- options[:death_handlers]
251
+ self[:death_handlers]
179
252
  end
180
253
 
181
254
  def self.load_json(string)
@@ -200,7 +273,7 @@ module Sidekiq
200
273
  end
201
274
 
202
275
  def self.logger
203
- @logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO)
276
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info)
204
277
  end
205
278
 
206
279
  def self.logger=(logger)
@@ -218,13 +291,17 @@ module Sidekiq
218
291
  defined?(Sidekiq::Pro)
219
292
  end
220
293
 
294
+ def self.ent?
295
+ defined?(Sidekiq::Enterprise)
296
+ end
297
+
221
298
  # How frequently Redis should be checked by a random Sidekiq process for
222
299
  # scheduled and retriable jobs. Each individual process will take turns by
223
300
  # waiting some multiple of this value.
224
301
  #
225
302
  # See sidekiq/scheduled.rb for an in-depth explanation of this value
226
303
  def self.average_scheduled_poll_interval=(interval)
227
- options[:average_scheduled_poll_interval] = interval
304
+ self[:average_scheduled_poll_interval] = interval
228
305
  end
229
306
 
230
307
  # Register a proc to handle any error which occurs within the Sidekiq process.
@@ -235,7 +312,7 @@ module Sidekiq
235
312
  #
236
313
  # The default error handler logs errors to Sidekiq.logger.
237
314
  def self.error_handlers
238
- options[:error_handlers]
315
+ self[:error_handlers]
239
316
  end
240
317
 
241
318
  # Register a block to run at a point in the Sidekiq lifecycle.
@@ -248,16 +325,20 @@ module Sidekiq
248
325
  # end
249
326
  def self.on(event, &block)
250
327
  raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
251
- raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
252
- options[:lifecycle_events][event] << block
328
+ raise ArgumentError, "Invalid event name: #{event}" unless self[:lifecycle_events].key?(event)
329
+ self[:lifecycle_events][event] << block
330
+ end
331
+
332
+ def self.strict_args!(mode = :raise)
333
+ self[:on_complex_arguments] = mode
253
334
  end
254
335
 
255
- # We are shutting down Sidekiq but what about workers that
336
+ # We are shutting down Sidekiq but what about threads that
256
337
  # are working on some long job? This error is
257
- # raised in workers that have not finished within the hard
338
+ # raised in jobs that have not finished within the hard
258
339
  # timeout limit. This is needed to rollback db transactions,
259
340
  # otherwise Ruby's Thread#kill will commit. See #377.
260
- # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
341
+ # DO NOT RESCUE THIS ERROR IN YOUR JOBS
261
342
  class Shutdown < Interrupt; end
262
343
  end
263
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,7 +105,19 @@ 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() {
@@ -107,5 +135,9 @@ function replacePage(text) {
107
135
  var header_status = doc.querySelector('.status')
108
136
  document.querySelector('.status').replaceWith(header_status)
109
137
 
110
- updateFuzzyTimes();
138
+ addListeners();
139
+ }
140
+
141
+ function showError(error) {
142
+ console.error(error)
111
143
  }