sidekiq 5.0.1 → 5.2.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 (59) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +3 -1
  4. data/.gitignore +2 -0
  5. data/.travis.yml +6 -13
  6. data/COMM-LICENSE +11 -9
  7. data/Changes.md +136 -1
  8. data/Ent-Changes.md +46 -3
  9. data/Gemfile +14 -20
  10. data/LICENSE +1 -1
  11. data/Pro-4.0-Upgrade.md +35 -0
  12. data/Pro-Changes.md +125 -0
  13. data/README.md +5 -3
  14. data/Rakefile +2 -5
  15. data/bin/sidekiqctl +13 -92
  16. data/bin/sidekiqload +2 -2
  17. data/lib/sidekiq.rb +24 -15
  18. data/lib/sidekiq/api.rb +83 -37
  19. data/lib/sidekiq/cli.rb +106 -76
  20. data/lib/sidekiq/client.rb +36 -33
  21. data/lib/sidekiq/ctl.rb +221 -0
  22. data/lib/sidekiq/delay.rb +23 -2
  23. data/lib/sidekiq/exception_handler.rb +2 -4
  24. data/lib/sidekiq/fetch.rb +1 -1
  25. data/lib/sidekiq/job_logger.rb +4 -3
  26. data/lib/sidekiq/job_retry.rb +51 -24
  27. data/lib/sidekiq/launcher.rb +18 -12
  28. data/lib/sidekiq/logging.rb +9 -5
  29. data/lib/sidekiq/manager.rb +5 -6
  30. data/lib/sidekiq/middleware/server/active_record.rb +2 -1
  31. data/lib/sidekiq/processor.rb +85 -48
  32. data/lib/sidekiq/rails.rb +7 -0
  33. data/lib/sidekiq/redis_connection.rb +40 -4
  34. data/lib/sidekiq/scheduled.rb +35 -8
  35. data/lib/sidekiq/testing.rb +4 -4
  36. data/lib/sidekiq/util.rb +5 -1
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web.rb +4 -4
  39. data/lib/sidekiq/web/action.rb +2 -2
  40. data/lib/sidekiq/web/application.rb +24 -2
  41. data/lib/sidekiq/web/helpers.rb +18 -8
  42. data/lib/sidekiq/web/router.rb +10 -10
  43. data/lib/sidekiq/worker.rb +39 -22
  44. data/sidekiq.gemspec +6 -17
  45. data/web/assets/javascripts/application.js +0 -0
  46. data/web/assets/javascripts/dashboard.js +15 -5
  47. data/web/assets/stylesheets/application.css +35 -2
  48. data/web/assets/stylesheets/bootstrap.css +2 -2
  49. data/web/locales/ar.yml +1 -0
  50. data/web/locales/en.yml +2 -0
  51. data/web/locales/es.yml +4 -3
  52. data/web/locales/ja.yml +5 -3
  53. data/web/views/_footer.erb +3 -0
  54. data/web/views/_nav.erb +3 -17
  55. data/web/views/layout.erb +1 -1
  56. data/web/views/queue.erb +1 -0
  57. data/web/views/queues.erb +2 -0
  58. data/web/views/retries.erb +4 -0
  59. metadata +20 -156
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'sidekiq/api'
5
+
6
+ class Sidekiq::Ctl
7
+ DEFAULT_KILL_TIMEOUT = 10
8
+ CMD = File.basename($0)
9
+
10
+ attr_reader :stage, :pidfile, :kill_timeout
11
+
12
+ def self.print_usage
13
+ puts "#{CMD} - control Sidekiq from the command line."
14
+ puts
15
+ puts "Usage: #{CMD} quiet <pidfile> <kill_timeout>"
16
+ puts " #{CMD} stop <pidfile> <kill_timeout>"
17
+ puts " #{CMD} status <section>"
18
+ puts
19
+ puts " <pidfile> is path to a pidfile"
20
+ puts " <kill_timeout> is number of seconds to wait until Sidekiq exits"
21
+ puts " (default: #{Sidekiq::Ctl::DEFAULT_KILL_TIMEOUT}), after which Sidekiq will be KILL'd"
22
+ puts
23
+ puts " <section> (optional) view a specific section of the status output"
24
+ puts " Valid sections are: #{Sidekiq::Ctl::Status::VALID_SECTIONS.join(', ')}"
25
+ puts
26
+ puts "Be sure to set the kill_timeout LONGER than Sidekiq's -t timeout. If you want"
27
+ puts "to wait 60 seconds for jobs to finish, use `sidekiq -t 60` and `sidekiqctl stop"
28
+ puts " path_to_pidfile 61`"
29
+ puts
30
+ end
31
+
32
+ def initialize(stage, pidfile, timeout)
33
+ @stage = stage
34
+ @pidfile = pidfile
35
+ @kill_timeout = timeout
36
+
37
+ done('No pidfile given', :error) if !pidfile
38
+ done("Pidfile #{pidfile} does not exist", :warn) if !File.exist?(pidfile)
39
+ done('Invalid pidfile content', :error) if pid == 0
40
+
41
+ fetch_process
42
+
43
+ begin
44
+ send(stage)
45
+ rescue NoMethodError
46
+ done "Invalid command: #{stage}", :error
47
+ end
48
+ end
49
+
50
+ def fetch_process
51
+ Process.kill(0, pid)
52
+ rescue Errno::ESRCH
53
+ done "Process doesn't exist", :error
54
+ # We were not allowed to send a signal, but the process must have existed
55
+ # when Process.kill() was called.
56
+ rescue Errno::EPERM
57
+ return pid
58
+ end
59
+
60
+ def done(msg, error = nil)
61
+ puts msg
62
+ exit(exit_signal(error))
63
+ end
64
+
65
+ def exit_signal(error)
66
+ (error == :error) ? 1 : 0
67
+ end
68
+
69
+ def pid
70
+ @pid ||= File.read(pidfile).to_i
71
+ end
72
+
73
+ def quiet
74
+ `kill -TSTP #{pid}`
75
+ end
76
+
77
+ def stop
78
+ `kill -TERM #{pid}`
79
+ kill_timeout.times do
80
+ begin
81
+ Process.kill(0, pid)
82
+ rescue Errno::ESRCH
83
+ FileUtils.rm_f pidfile
84
+ done 'Sidekiq shut down gracefully.'
85
+ rescue Errno::EPERM
86
+ done 'Not permitted to shut down Sidekiq.'
87
+ end
88
+ sleep 1
89
+ end
90
+ `kill -9 #{pid}`
91
+ FileUtils.rm_f pidfile
92
+ done 'Sidekiq shut down forcefully.'
93
+ end
94
+ alias_method :shutdown, :stop
95
+
96
+ class Status
97
+ VALID_SECTIONS = %w[all version overview processes queues]
98
+ def display(section = nil)
99
+ section ||= 'all'
100
+ unless VALID_SECTIONS.include? section
101
+ puts "I don't know how to check the status of '#{section}'!"
102
+ puts "Try one of these: #{VALID_SECTIONS.join(', ')}"
103
+ return
104
+ end
105
+ send(section)
106
+ rescue StandardError => e
107
+ puts "Couldn't get status: #{e}"
108
+ end
109
+
110
+ def all
111
+ version
112
+ puts
113
+ overview
114
+ puts
115
+ processes
116
+ puts
117
+ queues
118
+ end
119
+
120
+ def version
121
+ puts "Sidekiq #{Sidekiq::VERSION}"
122
+ puts Time.now
123
+ end
124
+
125
+ def overview
126
+ puts '---- Overview ----'
127
+ puts " Processed: #{delimit stats.processed}"
128
+ puts " Failed: #{delimit stats.failed}"
129
+ puts " Busy: #{delimit stats.workers_size}"
130
+ puts " Enqueued: #{delimit stats.enqueued}"
131
+ puts " Retries: #{delimit stats.retry_size}"
132
+ puts " Scheduled: #{delimit stats.scheduled_size}"
133
+ puts " Dead: #{delimit stats.dead_size}"
134
+ end
135
+
136
+ def processes
137
+ puts "---- Processes (#{process_set.size}) ----"
138
+ process_set.each_with_index do |process, index|
139
+ puts "#{process['identity']} #{tags_for(process)}"
140
+ puts " Started: #{Time.at(process['started_at'])} (#{time_ago(process['started_at'])})"
141
+ puts " Threads: #{process['concurrency']} (#{process['busy']} busy)"
142
+ puts " Queues: #{split_multiline(process['queues'].sort, pad: 11)}"
143
+ puts '' unless (index+1) == process_set.size
144
+ end
145
+ end
146
+
147
+ COL_PAD = 2
148
+ def queues
149
+ puts "---- Queues (#{queue_data.size}) ----"
150
+ columns = {
151
+ name: [:ljust, (['name'] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
152
+ size: [:rjust, (['size'] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
153
+ latency: [:rjust, (['latency'] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
154
+ }
155
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
156
+ puts
157
+ queue_data.each do |q|
158
+ columns.each do |col, (dir, width)|
159
+ print q.send(col).public_send(dir, width)
160
+ end
161
+ puts
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def delimit(number)
168
+ number.to_s.reverse.scan(/.{1,3}/).join(',').reverse
169
+ end
170
+
171
+ def split_multiline(values, opts = {})
172
+ return 'none' unless values
173
+ pad = opts[:pad] || 0
174
+ max_length = opts[:max_length] || (80 - pad)
175
+ out = []
176
+ line = ''
177
+ values.each do |value|
178
+ if (line.length + value.length) > max_length
179
+ out << line
180
+ line = ' ' * pad
181
+ end
182
+ line << value + ', '
183
+ end
184
+ out << line[0..-3]
185
+ out.join("\n")
186
+ end
187
+
188
+ def tags_for(process)
189
+ tags = [
190
+ process['tag'],
191
+ process['labels'],
192
+ (process['quiet'] == 'true' ? 'quiet' : nil)
193
+ ].flatten.compact
194
+ tags.any? ? "[#{tags.join('] [')}]" : nil
195
+ end
196
+
197
+ def time_ago(timestamp)
198
+ seconds = Time.now - Time.at(timestamp)
199
+ return 'just now' if seconds < 60
200
+ return 'a minute ago' if seconds < 120
201
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
202
+ return 'an hour ago' if seconds < 7200
203
+ "#{seconds.floor / 60 / 60} hours ago"
204
+ end
205
+
206
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
207
+ def queue_data
208
+ @queue_data ||= Sidekiq::Queue.all.map do |q|
209
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf('%#.2f', q.latency))
210
+ end
211
+ end
212
+
213
+ def process_set
214
+ @process_set ||= Sidekiq::ProcessSet.new
215
+ end
216
+
217
+ def stats
218
+ @stats ||= Sidekiq::Stats.new
219
+ end
220
+ end
221
+ end
data/lib/sidekiq/delay.rb CHANGED
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  module Extensions
3
4
 
4
5
  def self.enable_delay!
5
6
  if defined?(::ActiveSupport)
7
+ require 'sidekiq/extensions/active_record'
8
+ require 'sidekiq/extensions/action_mailer'
9
+
10
+ # Need to patch Psych so it can autoload classes whose names are serialized
11
+ # in the delayed YAML.
12
+ Psych::Visitors::ToRuby.prepend(Sidekiq::Extensions::PsychAutoload)
13
+
6
14
  ActiveSupport.on_load(:active_record) do
7
- require 'sidekiq/extensions/active_record'
8
15
  include Sidekiq::Extensions::ActiveRecord
9
16
  end
10
17
  ActiveSupport.on_load(:action_mailer) do
11
- require 'sidekiq/extensions/action_mailer'
12
18
  extend Sidekiq::Extensions::ActionMailer
13
19
  end
14
20
  end
@@ -17,5 +23,20 @@ module Sidekiq
17
23
  Module.__send__(:include, Sidekiq::Extensions::Klass)
18
24
  end
19
25
 
26
+ module PsychAutoload
27
+ def resolve_class(klass_name)
28
+ return nil if !klass_name || klass_name.empty?
29
+ # constantize
30
+ names = klass_name.split('::')
31
+ names.shift if names.empty? || names.first.empty?
32
+
33
+ names.inject(Object) do |constant, name|
34
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
35
+ end
36
+ rescue NameError
37
+ super
38
+ end
39
+ end
20
40
  end
21
41
  end
42
+
@@ -7,11 +7,10 @@ module Sidekiq
7
7
  class Logger
8
8
  def call(ex, ctxHash)
9
9
  Sidekiq.logger.warn(Sidekiq.dump_json(ctxHash)) if !ctxHash.empty?
10
- Sidekiq.logger.warn "#{ex.class.name}: #{ex.message}"
11
- Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
10
+ Sidekiq.logger.warn("#{ex.class.name}: #{ex.message}")
11
+ Sidekiq.logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
12
12
  end
13
13
 
14
- # Set up default handler which just logs the error
15
14
  Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new
16
15
  end
17
16
 
@@ -26,6 +25,5 @@ module Sidekiq
26
25
  end
27
26
  end
28
27
  end
29
-
30
28
  end
31
29
  end
data/lib/sidekiq/fetch.rb CHANGED
@@ -13,7 +13,7 @@ module Sidekiq
13
13
  end
14
14
 
15
15
  def queue_name
16
- queue.sub(/.*queue:/, ''.freeze)
16
+ queue.sub(/.*queue:/, '')
17
17
  end
18
18
 
19
19
  def requeue
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  class JobLogger
3
4
 
4
5
  def call(item, queue)
5
- start = Time.now
6
- logger.info("start".freeze)
6
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
7
+ logger.info("start")
7
8
  yield
8
9
  logger.info("done: #{elapsed(start)} sec")
9
10
  rescue Exception
@@ -14,7 +15,7 @@ module Sidekiq
14
15
  private
15
16
 
16
17
  def elapsed(start)
17
- (Time.now - start).round(3)
18
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
18
19
  end
19
20
 
20
21
  def logger
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/scheduled'
2
3
  require 'sidekiq/api'
3
4
 
@@ -55,7 +56,8 @@ module Sidekiq
55
56
  # end
56
57
  #
57
58
  class JobRetry
58
- class Skip < ::RuntimeError; end
59
+ class Handled < ::RuntimeError; end
60
+ class Skip < Handled; end
59
61
 
60
62
  include Sidekiq::Util
61
63
 
@@ -70,7 +72,7 @@ module Sidekiq
70
72
  # require the worker to be instantiated.
71
73
  def global(msg, queue)
72
74
  yield
73
- rescue Skip => ex
75
+ rescue Handled => ex
74
76
  raise ex
75
77
  rescue Sidekiq::Shutdown => ey
76
78
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -79,9 +81,19 @@ module Sidekiq
79
81
  # ignore, will be pushed back onto queue during hard_shutdown
80
82
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
81
83
 
82
- raise e unless msg['retry']
83
- attempt_retry(nil, msg, queue, e)
84
- raise e
84
+ if msg['retry']
85
+ attempt_retry(nil, msg, queue, e)
86
+ else
87
+ Sidekiq.death_handlers.each do |handler|
88
+ begin
89
+ handler.call(msg, e)
90
+ rescue => handler_ex
91
+ handle_exception(handler_ex, { context: "Error calling death handler", job: msg })
92
+ end
93
+ end
94
+ end
95
+
96
+ raise Handled
85
97
  end
86
98
 
87
99
 
@@ -95,7 +107,7 @@ module Sidekiq
95
107
  # calling the handle_exception handlers.
96
108
  def local(worker, msg, queue)
97
109
  yield
98
- rescue Skip => ex
110
+ rescue Handled => ex
99
111
  raise ex
100
112
  rescue Sidekiq::Shutdown => ey
101
113
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -129,9 +141,7 @@ module Sidekiq
129
141
  queue
130
142
  end
131
143
 
132
- # App code can stuff all sorts of crazy binary data into the error message
133
- # that won't convert to JSON.
134
- m = exception.message.to_s[0, 10_000]
144
+ m = exception_message(exception)
135
145
  if m.respond_to?(:scrub!)
136
146
  m.force_encoding("utf-8")
137
147
  m.scrub!
@@ -157,7 +167,8 @@ module Sidekiq
157
167
 
158
168
  if count < max_retry_attempts
159
169
  delay = delay_for(worker, count, exception)
160
- logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
170
+ # Logging here can break retries if the logging device raises ENOSPC #3979
171
+ #logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
161
172
  retry_at = Time.now.to_f + delay
162
173
  payload = Sidekiq.dump_json(msg)
163
174
  Sidekiq.redis do |conn|
@@ -170,28 +181,28 @@ module Sidekiq
170
181
  end
171
182
 
172
183
  def retries_exhausted(worker, msg, exception)
173
- logger.debug { "Retries exhausted for job" }
174
184
  begin
175
- block = worker && worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
185
+ block = worker && worker.sidekiq_retries_exhausted_block
176
186
  block.call(msg, exception) if block
177
187
  rescue => e
178
- handle_exception(e, { context: "Error calling retries_exhausted for #{msg['class']}", job: msg })
188
+ handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
189
+ end
190
+
191
+ Sidekiq.death_handlers.each do |handler|
192
+ begin
193
+ handler.call(msg, exception)
194
+ rescue => e
195
+ handle_exception(e, { context: "Error calling death handler", job: msg })
196
+ end
179
197
  end
180
198
 
181
199
  send_to_morgue(msg) unless msg['dead'] == false
182
200
  end
183
201
 
184
202
  def send_to_morgue(msg)
185
- Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
203
+ logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
186
204
  payload = Sidekiq.dump_json(msg)
187
- now = Time.now.to_f
188
- Sidekiq.redis do |conn|
189
- conn.multi do
190
- conn.zadd('dead', now, payload)
191
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
192
- conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
193
- end
194
- end
205
+ DeadSet.new.kill(payload, notify_failure: false)
195
206
  end
196
207
 
197
208
  def retry_attempts_from(msg_retry, default)
@@ -203,7 +214,11 @@ module Sidekiq
203
214
  end
204
215
 
205
216
  def delay_for(worker, count, exception)
206
- worker && worker.sidekiq_retry_in_block && retry_in(worker, count, exception) || seconds_to_delay(count)
217
+ if worker && worker.sidekiq_retry_in_block
218
+ custom_retry_in = retry_in(worker, count, exception).to_i
219
+ return custom_retry_in if custom_retry_in > 0
220
+ end
221
+ seconds_to_delay(count)
207
222
  end
208
223
 
209
224
  # delayed_job uses the same basic formula
@@ -213,7 +228,7 @@ module Sidekiq
213
228
 
214
229
  def retry_in(worker, count, exception)
215
230
  begin
216
- worker.sidekiq_retry_in_block.call(count, exception).to_i
231
+ worker.sidekiq_retry_in_block.call(count, exception)
217
232
  rescue Exception => e
218
233
  handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
219
234
  nil
@@ -231,5 +246,17 @@ module Sidekiq
231
246
  exception_caused_by_shutdown?(e.cause, checked_causes)
232
247
  end
233
248
 
249
+ # Extract message from exception.
250
+ # Set a default if the message raises an error
251
+ def exception_message(exception)
252
+ begin
253
+ # App code can stuff all sorts of crazy binary data into the error message
254
+ # that won't convert to JSON.
255
+ exception.message.to_s[0, 10_000]
256
+ rescue
257
+ "!!! ERROR MESSAGE THREW AN ERROR !!!".dup
258
+ end
259
+ end
260
+
234
261
  end
235
262
  end