sidekiq 6.0.0 → 6.0.5

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/6.0-Upgrade.md +3 -1
  3. data/Changes.md +110 -1
  4. data/Ent-Changes.md +7 -1
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +105 -93
  7. data/Pro-Changes.md +9 -1
  8. data/README.md +3 -1
  9. data/bin/sidekiqload +8 -4
  10. data/bin/sidekiqmon +4 -5
  11. data/lib/generators/sidekiq/worker_generator.rb +11 -1
  12. data/lib/sidekiq.rb +12 -0
  13. data/lib/sidekiq/api.rb +124 -91
  14. data/lib/sidekiq/cli.rb +29 -18
  15. data/lib/sidekiq/client.rb +18 -4
  16. data/lib/sidekiq/fetch.rb +7 -7
  17. data/lib/sidekiq/job_logger.rb +11 -3
  18. data/lib/sidekiq/job_retry.rb +23 -10
  19. data/lib/sidekiq/launcher.rb +3 -5
  20. data/lib/sidekiq/logger.rb +107 -11
  21. data/lib/sidekiq/middleware/chain.rb +11 -2
  22. data/lib/sidekiq/monitor.rb +1 -16
  23. data/lib/sidekiq/paginator.rb +7 -2
  24. data/lib/sidekiq/processor.rb +18 -20
  25. data/lib/sidekiq/redis_connection.rb +3 -0
  26. data/lib/sidekiq/scheduled.rb +13 -12
  27. data/lib/sidekiq/testing.rb +12 -0
  28. data/lib/sidekiq/util.rb +0 -2
  29. data/lib/sidekiq/version.rb +1 -1
  30. data/lib/sidekiq/web/application.rb +19 -18
  31. data/lib/sidekiq/web/helpers.rb +23 -11
  32. data/lib/sidekiq/worker.rb +4 -4
  33. data/sidekiq.gemspec +2 -2
  34. data/web/assets/javascripts/dashboard.js +2 -2
  35. data/web/assets/stylesheets/application-dark.css +125 -0
  36. data/web/assets/stylesheets/application.css +9 -0
  37. data/web/locales/de.yml +14 -2
  38. data/web/locales/en.yml +2 -0
  39. data/web/locales/ja.yml +2 -0
  40. data/web/views/_job_info.erb +2 -1
  41. data/web/views/busy.erb +4 -1
  42. data/web/views/dead.erb +2 -2
  43. data/web/views/layout.erb +1 -0
  44. data/web/views/morgue.erb +4 -1
  45. data/web/views/queue.erb +10 -1
  46. data/web/views/queues.erb +8 -0
  47. data/web/views/retries.erb +4 -1
  48. data/web/views/retry.erb +2 -2
  49. data/web/views/scheduled.erb +4 -1
  50. metadata +9 -8
@@ -38,12 +38,15 @@ module Sidekiq
38
38
  if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
39
39
  print_banner
40
40
  end
41
+ logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
41
42
 
42
43
  self_read, self_write = IO.pipe
43
44
  sigs = %w[INT TERM TTIN TSTP]
45
+ # USR1 and USR2 don't work on the JVM
46
+ sigs << "USR2" unless jruby?
44
47
  sigs.each do |sig|
45
48
  trap sig do
46
- self_write.write("#{sig}\n")
49
+ self_write.puts(sig)
47
50
  end
48
51
  rescue ArgumentError
49
52
  puts "Signal #{sig} not supported"
@@ -56,7 +59,7 @@ module Sidekiq
56
59
  # touch the connection pool so it is created before we
57
60
  # fire startup and start multithreading.
58
61
  ver = Sidekiq.redis_info["redis_version"]
59
- raise "You are using Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
62
+ raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
60
63
 
61
64
  # Since the user can pass us a connection pool explicitly in the initializer, we
62
65
  # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
@@ -162,15 +165,12 @@ module Sidekiq
162
165
  end
163
166
  },
164
167
  }
168
+ UNHANDLED_SIGNAL_HANDLER = ->(cli) { Sidekiq.logger.info "No signal handler registered, ignoring" }
169
+ SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
165
170
 
166
171
  def handle_signal(sig)
167
172
  Sidekiq.logger.debug "Got #{sig} signal"
168
- handy = SIGNAL_HANDLERS[sig]
169
- if handy
170
- handy.call(self)
171
- else
172
- Sidekiq.logger.info { "No signal handler for #{sig}" }
173
- end
173
+ SIGNAL_HANDLERS[sig].call(self)
174
174
  end
175
175
 
176
176
  private
@@ -182,7 +182,11 @@ module Sidekiq
182
182
  end
183
183
 
184
184
  def set_environment(cli_env)
185
- @environment = cli_env || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
185
+ # See #984 for discussion.
186
+ # APP_ENV is now the preferred ENV term since it is not tech-specific.
187
+ # Both Sinatra 2.0+ and Sidekiq support this term.
188
+ # RACK_ENV and RAILS_ENV are there for legacy support.
189
+ @environment = cli_env || ENV["APP_ENV"] || ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
186
190
  end
187
191
 
188
192
  def symbolize_keys_deep!(hash)
@@ -204,7 +208,7 @@ module Sidekiq
204
208
 
205
209
  # check config file presence
206
210
  if opts[:config_file]
207
- if opts[:config_file] && !File.exist?(opts[:config_file])
211
+ unless File.exist?(opts[:config_file])
208
212
  raise ArgumentError, "No such file #{opts[:config_file]}"
209
213
  end
210
214
  else
@@ -224,7 +228,7 @@ module Sidekiq
224
228
  opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
225
229
 
226
230
  # set defaults
227
- opts[:queues] = Array(opts[:queues]) << "default" if opts[:queues].nil? || opts[:queues].empty?
231
+ opts[:queues] = ["default"] if opts[:queues].nil? || opts[:queues].empty?
228
232
  opts[:strict] = true if opts[:strict].nil?
229
233
  opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
230
234
 
@@ -283,8 +287,13 @@ module Sidekiq
283
287
 
284
288
  def parse_options(argv)
285
289
  opts = {}
290
+ @parser = option_parser(opts)
291
+ @parser.parse!(argv)
292
+ opts
293
+ end
286
294
 
287
- @parser = OptionParser.new { |o|
295
+ def option_parser(opts)
296
+ parser = OptionParser.new { |o|
288
297
  o.on "-c", "--concurrency INT", "processor threads to use" do |arg|
289
298
  opts[:concurrency] = Integer(arg)
290
299
  end
@@ -336,15 +345,13 @@ module Sidekiq
336
345
  end
337
346
  }
338
347
 
339
- @parser.banner = "sidekiq [options]"
340
- @parser.on_tail "-h", "--help", "Show help" do
341
- logger.info @parser
348
+ parser.banner = "sidekiq [options]"
349
+ parser.on_tail "-h", "--help", "Show help" do
350
+ logger.info parser
342
351
  die 1
343
352
  end
344
353
 
345
- @parser.parse!(argv)
346
-
347
- opts
354
+ parser
348
355
  end
349
356
 
350
357
  def initialize_logger
@@ -376,5 +383,9 @@ module Sidekiq
376
383
  [weight.to_i, 1].max.times { opts[:queues] << queue }
377
384
  opts[:strict] = false if weight.to_i > 0
378
385
  end
386
+
387
+ def rails_app?
388
+ defined?(::Rails) && ::Rails.respond_to?(:application)
389
+ end
379
390
  end
380
391
  end
@@ -94,9 +94,14 @@ module Sidekiq
94
94
  return [] unless arg # no jobs to push
95
95
  raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless arg.is_a?(Array)
96
96
 
97
+ at = items.delete("at")
98
+ raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all?(Numeric))
99
+
97
100
  normed = normalize_item(items)
98
- payloads = items["args"].map { |args|
101
+ payloads = items["args"].map.with_index { |args, index|
99
102
  copy = normed.merge("args" => args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
103
+ copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
104
+
100
105
  result = process_single(items["class"], copy)
101
106
  result || nil
102
107
  }.compact
@@ -188,7 +193,7 @@ module Sidekiq
188
193
  end
189
194
 
190
195
  def atomic_push(conn, payloads)
191
- if payloads.first["at"]
196
+ if payloads.first.key?("at")
192
197
  conn.zadd("schedule", payloads.map { |hash|
193
198
  at = hash.delete("at").to_s
194
199
  [at, Sidekiq.dump_json(hash)]
@@ -214,19 +219,28 @@ module Sidekiq
214
219
  end
215
220
 
216
221
  def normalize_item(item)
222
+ # 6.0.0 push_bulk bug, #4321
223
+ # TODO Remove after a while...
224
+ item.delete("at") if item.key?("at") && item["at"].nil?
225
+
217
226
  raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
218
227
  raise(ArgumentError, "Job args must be an Array") unless item["args"].is_a?(Array)
219
228
  raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
220
229
  raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.key?("at") && !item["at"].is_a?(Numeric)
230
+ raise(ArgumentError, "Job tags must be an Array") if item["tags"] && !item["tags"].is_a?(Array)
221
231
  # raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
222
232
 
223
- normalized_hash(item["class"])
224
- .each { |key, value| item[key] = value if item[key].nil? }
233
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
234
+ # this allows ActiveJobs to control sidekiq_options too.
235
+ defaults = normalized_hash(item["class"])
236
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
237
+ item = defaults.merge(item)
225
238
 
226
239
  item["class"] = item["class"].to_s
227
240
  item["queue"] = item["queue"].to_s
228
241
  item["jid"] ||= SecureRandom.hex(12)
229
242
  item["created_at"] ||= Time.now.to_f
243
+
230
244
  item
231
245
  end
232
246
 
@@ -14,12 +14,12 @@ module Sidekiq
14
14
  end
15
15
 
16
16
  def queue_name
17
- queue.sub(/.*queue:/, "")
17
+ queue.delete_prefix("queue:")
18
18
  end
19
19
 
20
20
  def requeue
21
21
  Sidekiq.redis do |conn|
22
- conn.rpush("queue:#{queue_name}", job)
22
+ conn.rpush(queue, job)
23
23
  end
24
24
  end
25
25
  }
@@ -28,7 +28,7 @@ module Sidekiq
28
28
  @strictly_ordered_queues = !!options[:strict]
29
29
  @queues = options[:queues].map { |q| "queue:#{q}" }
30
30
  if @strictly_ordered_queues
31
- @queues = @queues.uniq
31
+ @queues.uniq!
32
32
  @queues << TIMEOUT
33
33
  end
34
34
  end
@@ -47,7 +47,7 @@ module Sidekiq
47
47
  if @strictly_ordered_queues
48
48
  @queues
49
49
  else
50
- queues = @queues.shuffle.uniq
50
+ queues = @queues.shuffle!.uniq
51
51
  queues << TIMEOUT
52
52
  queues
53
53
  end
@@ -61,14 +61,14 @@ module Sidekiq
61
61
  Sidekiq.logger.debug { "Re-queueing terminated jobs" }
62
62
  jobs_to_requeue = {}
63
63
  inprogress.each do |unit_of_work|
64
- jobs_to_requeue[unit_of_work.queue_name] ||= []
65
- jobs_to_requeue[unit_of_work.queue_name] << unit_of_work.job
64
+ jobs_to_requeue[unit_of_work.queue] ||= []
65
+ jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
66
66
  end
67
67
 
68
68
  Sidekiq.redis do |conn|
69
69
  conn.pipelined do
70
70
  jobs_to_requeue.each do |queue, jobs|
71
- conn.rpush("queue:#{queue}", jobs)
71
+ conn.rpush(queue, jobs)
72
72
  end
73
73
  end
74
74
  end
@@ -23,8 +23,15 @@ module Sidekiq
23
23
  raise
24
24
  end
25
25
 
26
- def with_job_hash_context(job_hash, &block)
27
- @logger.with_context(job_hash_context(job_hash), &block)
26
+ def prepare(job_hash, &block)
27
+ level = job_hash["log_level"]
28
+ if level
29
+ @logger.log_at(level) do
30
+ Sidekiq::Context.with(job_hash_context(job_hash), &block)
31
+ end
32
+ else
33
+ Sidekiq::Context.with(job_hash_context(job_hash), &block)
34
+ end
28
35
  end
29
36
 
30
37
  def job_hash_context(job_hash)
@@ -35,11 +42,12 @@ module Sidekiq
35
42
  jid: job_hash["jid"],
36
43
  }
37
44
  h[:bid] = job_hash["bid"] if job_hash["bid"]
45
+ h[:tags] = job_hash["tags"] if job_hash["tags"]
38
46
  h
39
47
  end
40
48
 
41
49
  def with_elapsed_time_context(start, &block)
42
- @logger.with_context(elapsed_time_context(start), &block)
50
+ Sidekiq::Context.with(elapsed_time_context(start), &block)
43
51
  end
44
52
 
45
53
  def elapsed_time_context(start)
@@ -3,6 +3,9 @@
3
3
  require "sidekiq/scheduled"
4
4
  require "sidekiq/api"
5
5
 
6
+ require "zlib"
7
+ require "base64"
8
+
6
9
  module Sidekiq
7
10
  ##
8
11
  # Automatically retry jobs that fail in Sidekiq.
@@ -71,7 +74,7 @@ module Sidekiq
71
74
  # The global retry handler requires only the barest of data.
72
75
  # We want to be able to retry as much as possible so we don't
73
76
  # require the worker to be instantiated.
74
- def global(msg, queue)
77
+ def global(jobstr, queue)
75
78
  yield
76
79
  rescue Handled => ex
77
80
  raise ex
@@ -82,6 +85,7 @@ module Sidekiq
82
85
  # ignore, will be pushed back onto queue during hard_shutdown
83
86
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
84
87
 
88
+ msg = Sidekiq.load_json(jobstr)
85
89
  if msg["retry"]
86
90
  attempt_retry(nil, msg, queue, e)
87
91
  else
@@ -103,7 +107,7 @@ module Sidekiq
103
107
  # exception so the global block does not reprocess the error. The
104
108
  # Skip exception is unwrapped within Sidekiq::Processor#process before
105
109
  # calling the handle_exception handlers.
106
- def local(worker, msg, queue)
110
+ def local(worker, jobstr, queue)
107
111
  yield
108
112
  rescue Handled => ex
109
113
  raise ex
@@ -114,6 +118,7 @@ module Sidekiq
114
118
  # ignore, will be pushed back onto queue during hard_shutdown
115
119
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
116
120
 
121
+ msg = Sidekiq.load_json(jobstr)
117
122
  if msg["retry"].nil?
118
123
  msg["retry"] = worker.class.get_sidekiq_options["retry"]
119
124
  end
@@ -151,12 +156,14 @@ module Sidekiq
151
156
  msg["retry_count"] = 0
152
157
  end
153
158
 
154
- if msg["backtrace"] == true
155
- msg["error_backtrace"] = exception.backtrace
156
- elsif !msg["backtrace"]
157
- # do nothing
158
- elsif msg["backtrace"].to_i != 0
159
- msg["error_backtrace"] = exception.backtrace[0...msg["backtrace"].to_i]
159
+ if msg["backtrace"]
160
+ lines = if msg["backtrace"] == true
161
+ exception.backtrace
162
+ else
163
+ exception.backtrace[0...msg["backtrace"].to_i]
164
+ end
165
+
166
+ msg["error_backtrace"] = compress_backtrace(lines)
160
167
  end
161
168
 
162
169
  if count < max_retry_attempts
@@ -182,13 +189,13 @@ module Sidekiq
182
189
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
183
190
  end
184
191
 
192
+ send_to_morgue(msg) unless msg["dead"] == false
193
+
185
194
  Sidekiq.death_handlers.each do |handler|
186
195
  handler.call(msg, exception)
187
196
  rescue => e
188
197
  handle_exception(e, {context: "Error calling death handler", job: msg})
189
198
  end
190
-
191
- send_to_morgue(msg) unless msg["dead"] == false
192
199
  end
193
200
 
194
201
  def send_to_morgue(msg)
@@ -245,5 +252,11 @@ module Sidekiq
245
252
  rescue
246
253
  +"!!! ERROR MESSAGE THREW AN ERROR !!!"
247
254
  end
255
+
256
+ def compress_backtrace(backtrace)
257
+ serialized = Sidekiq.dump_json(backtrace)
258
+ compressed = Zlib::Deflate.deflate(serialized)
259
+ Base64.encode64(compressed)
260
+ end
248
261
  end
249
262
  end
@@ -83,7 +83,7 @@ module Sidekiq
83
83
  Sidekiq.redis do |conn|
84
84
  conn.pipelined do
85
85
  conn.srem("processes", identity)
86
- conn.del("#{identity}:workers")
86
+ conn.unlink("#{identity}:workers")
87
87
  end
88
88
  end
89
89
  rescue
@@ -118,7 +118,7 @@ module Sidekiq
118
118
  conn.incrby("stat:failed:#{nowdate}", fails)
119
119
  conn.expire("stat:failed:#{nowdate}", STATS_TTL)
120
120
 
121
- conn.del(workers_key)
121
+ conn.unlink(workers_key)
122
122
  curstate.each_pair do |tid, hash|
123
123
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
124
124
  end
@@ -129,15 +129,13 @@ module Sidekiq
129
129
  fails = procd = 0
130
130
 
131
131
  _, exists, _, _, msg = Sidekiq.redis { |conn|
132
- res = conn.multi {
132
+ conn.multi {
133
133
  conn.sadd("processes", key)
134
134
  conn.exists(key)
135
135
  conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
136
136
  conn.expire(key, 60)
137
137
  conn.rpop("#{key}-signals")
138
138
  }
139
-
140
- res
141
139
  }
142
140
 
143
141
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
@@ -4,22 +4,109 @@ require "logger"
4
4
  require "time"
5
5
 
6
6
  module Sidekiq
7
- class Logger < ::Logger
8
- def initialize(*args)
9
- super
7
+ module Context
8
+ def self.with(hash)
9
+ current.merge!(hash)
10
+ yield
11
+ ensure
12
+ hash.each_key { |key| current.delete(key) }
13
+ end
10
14
 
11
- self.formatter = Sidekiq.log_formatter
15
+ def self.current
16
+ Thread.current[:sidekiq_context] ||= {}
17
+ end
18
+ end
19
+
20
+ module LoggingUtils
21
+ LEVELS = {
22
+ "debug" => 0,
23
+ "info" => 1,
24
+ "warn" => 2,
25
+ "error" => 3,
26
+ "fatal" => 4,
27
+ }
28
+ LEVELS.default_proc = proc do |_, level|
29
+ Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
30
+ nil
31
+ end
32
+
33
+ def debug?
34
+ level >= 0
35
+ end
36
+
37
+ def info?
38
+ level >= 1
39
+ end
40
+
41
+ def warn?
42
+ level >= 2
43
+ end
44
+
45
+ def error?
46
+ level >= 3
47
+ end
48
+
49
+ def fatal?
50
+ level >= 4
12
51
  end
13
52
 
14
- def with_context(hash)
15
- ctx.merge!(hash)
53
+ def local_level
54
+ Thread.current[:sidekiq_log_level]
55
+ end
56
+
57
+ def local_level=(level)
58
+ case level
59
+ when Integer
60
+ Thread.current[:sidekiq_log_level] = level
61
+ when Symbol, String
62
+ Thread.current[:sidekiq_log_level] = LEVELS[level.to_s]
63
+ when nil
64
+ Thread.current[:sidekiq_log_level] = nil
65
+ else
66
+ raise ArgumentError, "Invalid log level: #{level.inspect}"
67
+ end
68
+ end
69
+
70
+ def level
71
+ local_level || super
72
+ end
73
+
74
+ # Change the thread-local level for the duration of the given block.
75
+ def log_at(level)
76
+ old_local_level = local_level
77
+ self.local_level = level
16
78
  yield
17
79
  ensure
18
- hash.keys.each { |key| ctx.delete(key) }
80
+ self.local_level = old_local_level
19
81
  end
20
82
 
21
- def ctx
22
- Thread.current[:sidekiq_context] ||= {}
83
+ # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
84
+ # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
85
+ def add(severity, message = nil, progname = nil, &block)
86
+ severity ||= ::Logger::UNKNOWN
87
+ progname ||= @progname
88
+
89
+ return true if @logdev.nil? || severity < level
90
+
91
+ if message.nil?
92
+ if block_given?
93
+ message = yield
94
+ else
95
+ message = progname
96
+ progname = @progname
97
+ end
98
+ end
99
+
100
+ @logdev.write format_message(format_severity(severity), Time.now, progname, message)
101
+ end
102
+ end
103
+
104
+ class Logger < ::Logger
105
+ include LoggingUtils
106
+
107
+ def initialize(*args, **kwargs)
108
+ super
109
+ self.formatter = Sidekiq.log_formatter
23
110
  end
24
111
 
25
112
  module Formatters
@@ -29,11 +116,20 @@ module Sidekiq
29
116
  end
30
117
 
31
118
  def ctx
32
- Thread.current[:sidekiq_context] ||= {}
119
+ Sidekiq::Context.current
33
120
  end
34
121
 
35
122
  def format_context
36
- " " + ctx.compact.map { |k, v| "#{k}=#{v}" }.join(" ") if ctx.any?
123
+ if ctx.any?
124
+ " " + ctx.compact.map { |k, v|
125
+ case v
126
+ when Array
127
+ "#{k}=#{v.join(",")}"
128
+ else
129
+ "#{k}=#{v}"
130
+ end
131
+ }.join(" ")
132
+ end
37
133
  end
38
134
  end
39
135