sidekiq 2.17.8 → 3.0.0

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/3.0-Upgrade.md +63 -0
  3. data/Changes.md +66 -3
  4. data/Contributing.md +1 -3
  5. data/Pro-Changes.md +18 -0
  6. data/README.md +2 -2
  7. data/bin/sidekiqctl +19 -6
  8. data/lib/sidekiq.rb +53 -11
  9. data/lib/sidekiq/actor.rb +1 -0
  10. data/lib/sidekiq/api.rb +145 -58
  11. data/lib/sidekiq/cli.rb +22 -18
  12. data/lib/sidekiq/client.rb +44 -14
  13. data/lib/sidekiq/core_ext.rb +5 -8
  14. data/lib/sidekiq/exception_handler.rb +19 -28
  15. data/lib/sidekiq/fetch.rb +3 -3
  16. data/lib/sidekiq/launcher.rb +30 -3
  17. data/lib/sidekiq/logging.rb +2 -2
  18. data/lib/sidekiq/manager.rb +19 -16
  19. data/lib/sidekiq/middleware/chain.rb +1 -1
  20. data/lib/sidekiq/middleware/i18n.rb +1 -1
  21. data/lib/sidekiq/middleware/server/retry_jobs.rb +23 -7
  22. data/lib/sidekiq/processor.rb +36 -54
  23. data/lib/sidekiq/redis_connection.rb +1 -3
  24. data/lib/sidekiq/util.rb +4 -4
  25. data/lib/sidekiq/version.rb +1 -1
  26. data/lib/sidekiq/web.rb +57 -8
  27. data/lib/sidekiq/web_helpers.rb +6 -15
  28. data/lib/sidekiq/worker.rb +3 -1
  29. data/sidekiq.gemspec +5 -5
  30. data/test/test_api.rb +59 -19
  31. data/test/test_cli.rb +1 -1
  32. data/test/test_client.rb +44 -5
  33. data/test/test_exception_handler.rb +4 -87
  34. data/test/test_middleware.rb +3 -2
  35. data/test/test_redis_connection.rb +0 -6
  36. data/test/test_retry.rb +13 -68
  37. data/test/test_scheduled.rb +1 -1
  38. data/test/test_scheduling.rb +5 -0
  39. data/test/test_sidekiq.rb +18 -0
  40. data/test/test_web.rb +98 -58
  41. data/web/assets/stylesheets/application.css +5 -0
  42. data/web/locales/cs.yml +68 -0
  43. data/web/locales/da.yml +9 -1
  44. data/web/locales/de.yml +15 -7
  45. data/web/locales/el.yml +68 -0
  46. data/web/locales/en.yml +8 -3
  47. data/web/locales/es.yml +9 -1
  48. data/web/locales/fr.yml +34 -26
  49. data/web/locales/it.yml +26 -18
  50. data/web/locales/ja.yml +8 -2
  51. data/web/locales/ko.yml +0 -2
  52. data/web/locales/nl.yml +8 -3
  53. data/web/locales/no.yml +9 -3
  54. data/web/locales/pl.yml +0 -1
  55. data/web/locales/pt-br.yml +11 -4
  56. data/web/locales/pt.yml +8 -1
  57. data/web/locales/ru.yml +29 -22
  58. data/web/locales/sv.yml +68 -0
  59. data/web/locales/zh-tw.yml +68 -0
  60. data/web/views/_job_info.erb +8 -2
  61. data/web/views/_summary.erb +13 -7
  62. data/web/views/busy.erb +55 -0
  63. data/web/views/dead.erb +30 -0
  64. data/web/views/layout.erb +1 -0
  65. data/web/views/morgue.erb +66 -0
  66. metadata +29 -30
  67. data/config.ru +0 -18
  68. data/lib/sidekiq/capistrano.rb +0 -5
  69. data/lib/sidekiq/capistrano2.rb +0 -54
  70. data/lib/sidekiq/tasks/sidekiq.rake +0 -119
  71. data/lib/sidekiq/yaml_patch.rb +0 -21
  72. data/test/test_util.rb +0 -18
  73. data/web/views/_workers.erb +0 -22
  74. data/web/views/workers.erb +0 -16
@@ -43,6 +43,13 @@ module Sidekiq
43
43
  end
44
44
 
45
45
  def run
46
+ # Print logo and banner for development
47
+ if environment == 'development' && $stdout.tty?
48
+ puts "\e[#{31}m"
49
+ puts Sidekiq::BANNER
50
+ puts "\e[0m"
51
+ end
52
+
46
53
  self_read, self_write = IO.pipe
47
54
 
48
55
  %w(INT TERM USR1 USR2 TTIN).each do |sig|
@@ -55,23 +62,19 @@ module Sidekiq
55
62
  end
56
63
  end
57
64
 
58
- redis {} # noop to connect redis and print info
59
65
  logger.info "Running in #{RUBY_DESCRIPTION}"
60
66
  logger.info Sidekiq::LICENSE
61
67
 
68
+ fire_event(:startup)
69
+
62
70
  if !options[:daemon]
63
71
  logger.info 'Starting processing, hit Ctrl-C to stop'
64
72
  end
65
73
 
66
74
  require 'sidekiq/launcher'
67
75
  @launcher = Sidekiq::Launcher.new(options)
68
- launcher.procline(options[:tag] ? "#{options[:tag]} " : '')
69
76
 
70
77
  begin
71
- if options[:profile]
72
- require 'ruby-prof'
73
- RubyProf.start
74
- end
75
78
  launcher.run
76
79
 
77
80
  while readable_io = IO.select([self_read])
@@ -81,6 +84,7 @@ module Sidekiq
81
84
  rescue Interrupt
82
85
  logger.info 'Shutting down'
83
86
  launcher.stop
87
+ fire_event(:shutdown)
84
88
  # Explicitly exit so busy Processor threads can't block
85
89
  # process shutdown.
86
90
  exit(0)
@@ -89,17 +93,20 @@ module Sidekiq
89
93
 
90
94
  private
91
95
 
96
+ def fire_event(event)
97
+ Sidekiq.options[:lifecycle_events][event].each do |block|
98
+ begin
99
+ block.call
100
+ rescue => ex
101
+ handle_exception(ex, { :event => event })
102
+ end
103
+ end
104
+ end
105
+
92
106
  def handle_signal(sig)
93
107
  Sidekiq.logger.debug "Got #{sig} signal"
94
108
  case sig
95
109
  when 'INT'
96
- if Sidekiq.options[:profile]
97
- result = RubyProf.stop
98
- printer = RubyProf::GraphHtmlPrinter.new(result)
99
- File.open("profile.html", 'w') do |f|
100
- printer.print(f, :min_percent => 1)
101
- end
102
- end
103
110
  # Handle Ctrl-C in JRuby like MRI
104
111
  # http://jira.codehaus.org/browse/JRUBY-4637
105
112
  raise Interrupt
@@ -109,6 +116,7 @@ module Sidekiq
109
116
  when 'USR1'
110
117
  Sidekiq.logger.info "Received USR1, no longer accepting new work"
111
118
  launcher.manager.async.stop
119
+ fire_event(:quiet)
112
120
  when 'USR2'
113
121
  if Sidekiq.options[:logfile]
114
122
  Sidekiq.logger.info "Received USR2, reopening log file"
@@ -258,10 +266,6 @@ module Sidekiq
258
266
  opts[:index] = Integer(arg.match(/\d+/)[0])
259
267
  end
260
268
 
261
- o.on '-p', '--profile', "Profile all code run by Sidekiq" do |arg|
262
- opts[:profile] = arg
263
- end
264
-
265
269
  o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
266
270
  queue, weight = arg.split(",")
267
271
  parse_queue opts, queue, weight
@@ -310,7 +314,7 @@ module Sidekiq
310
314
  def initialize_logger
311
315
  Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
312
316
 
313
- Sidekiq.logger.level = Logger::DEBUG if options[:verbose]
317
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
314
318
  end
315
319
 
316
320
  def write_pid
@@ -25,6 +25,24 @@ module Sidekiq
25
25
  @chain
26
26
  end
27
27
 
28
+ attr_accessor :redis_pool
29
+
30
+ # Sidekiq::Client normally uses the default Redis pool but you may
31
+ # pass a custom ConnectionPool if you want to shard your
32
+ # Sidekiq jobs across several Redis instances (for scalability
33
+ # reasons, e.g.)
34
+ #
35
+ # Sidekiq::Client.new(ConnectionPool.new { Redis.new })
36
+ #
37
+ # Generally this is only needed for very large Sidekiq installs processing
38
+ # more than thousands jobs per second. I do not recommend sharding unless
39
+ # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
40
+ # Some features, like the API, do not support sharding: they are designed to work
41
+ # against a single Redis instance only.
42
+ def initialize(redis_pool=nil)
43
+ @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
44
+ end
45
+
28
46
  ##
29
47
  # The main method used to push a job to Redis. Accepts a number of options:
30
48
  #
@@ -77,23 +95,35 @@ module Sidekiq
77
95
  pushed ? payloads.collect { |payload| payload['jid'] } : nil
78
96
  end
79
97
 
98
+ # Allows sharding of jobs across any number of Redis instances. All jobs
99
+ # defined within the block will use the given Redis connection pool.
100
+ #
101
+ # pool = ConnectionPool.new { Redis.new }
102
+ # Sidekiq::Client.via(pool) do
103
+ # SomeWorker.perform_async(1,2,3)
104
+ # SomeOtherWorker.perform_async(1,2,3)
105
+ # end
106
+ #
107
+ # Generally this is only needed for very large Sidekiq installs processing
108
+ # more than thousands jobs per second. I do not recommend sharding unless
109
+ # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
110
+ # Some features, like the API, do not support sharding: they are designed to work
111
+ # against a single Redis instance.
112
+ def self.via(pool)
113
+ raise ArgumentError, "No pool given" if pool.nil?
114
+ raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if x = Thread.current[:sidekiq_via_pool] && x != pool
115
+ Thread.current[:sidekiq_via_pool] = pool
116
+ yield
117
+ ensure
118
+ Thread.current[:sidekiq_via_pool] = nil
119
+ end
120
+
80
121
  class << self
122
+
81
123
  def default
82
124
  @default ||= new
83
125
  end
84
126
 
85
- # deprecated
86
- def registered_workers
87
- puts "registered_workers is deprecated, please use Sidekiq::Workers.new"
88
- Sidekiq.redis { |x| x.smembers('workers') }
89
- end
90
-
91
- # deprecated
92
- def registered_queues
93
- puts "registered_queues is deprecated, please use Sidekiq::Queue.all"
94
- Sidekiq::Queue.all.map(&:name)
95
- end
96
-
97
127
  def push(item)
98
128
  default.push(item)
99
129
  end
@@ -147,7 +177,7 @@ module Sidekiq
147
177
 
148
178
  def raw_push(payloads)
149
179
  pushed = false
150
- Sidekiq.redis do |conn|
180
+ @redis_pool.with do |conn|
151
181
  if payloads.first['at']
152
182
  pushed = conn.zadd('schedule', payloads.map do |hash|
153
183
  at = hash.delete('at').to_s
@@ -168,7 +198,7 @@ module Sidekiq
168
198
  def process_single(worker_class, item)
169
199
  queue = item['queue']
170
200
 
171
- middleware.invoke(worker_class, item, queue) do
201
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
172
202
  item
173
203
  end
174
204
  end
@@ -6,7 +6,6 @@ rescue LoadError
6
6
  # Class#class_attribute helper.
7
7
  class Class
8
8
  def class_attribute(*attrs)
9
- instance_reader = true
10
9
  instance_writer = true
11
10
 
12
11
  attrs.each do |name|
@@ -29,14 +28,12 @@ rescue LoadError
29
28
  val
30
29
  end
31
30
 
32
- if instance_reader
33
- def #{name}
34
- defined?(@#{name}) ? @#{name} : self.class.#{name}
35
- end
31
+ def #{name}
32
+ defined?(@#{name}) ? @#{name} : self.class.#{name}
33
+ end
36
34
 
37
- def #{name}?
38
- !!#{name}
39
- end
35
+ def #{name}?
36
+ !!#{name}
40
37
  end
41
38
  RUBY
42
39
 
@@ -1,39 +1,30 @@
1
+ require 'sidekiq'
2
+
1
3
  module Sidekiq
2
4
  module ExceptionHandler
3
5
 
4
- def handle_exception(ex, ctxHash={})
5
- Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
6
- Sidekiq.logger.warn ex
7
- Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
8
- # This list of services is getting a bit ridiculous.
9
- # For future error services, please add your own
10
- # middleware like BugSnag does:
11
- # https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/sidekiq.rb
12
- send_to_airbrake(ctxHash, ex) if defined?(::Airbrake)
13
- send_to_honeybadger(ctxHash, ex) if defined?(::Honeybadger)
14
- send_to_exceptional(ctxHash, ex) if defined?(::Exceptional)
15
- send_to_exception_notifier(ctxHash, ex) if defined?(::ExceptionNotifier)
16
- end
17
-
18
- private
19
-
20
- def send_to_airbrake(hash, ex)
21
- ::Airbrake.notify_or_ignore(ex, :parameters => hash)
22
- end
6
+ class Logger
7
+ def call(ex, ctxHash)
8
+ Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
9
+ Sidekiq.logger.warn ex
10
+ Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
11
+ end
23
12
 
24
- def send_to_honeybadger(hash, ex)
25
- ::Honeybadger.notify_or_ignore(ex, :parameters => hash)
13
+ # Set up default handler which just logs the error
14
+ Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new
26
15
  end
27
16
 
28
- def send_to_exceptional(hash, ex)
29
- if ::Exceptional::Config.should_send_to_api?
30
- ::Exceptional.context(hash)
31
- ::Exceptional::Remote.error(::Exceptional::ExceptionData.new(ex))
17
+ def handle_exception(ex, ctxHash={})
18
+ Sidekiq.error_handlers.each do |handler|
19
+ begin
20
+ handler.call(ex, ctxHash)
21
+ rescue => ex
22
+ Sidekiq.logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
23
+ Sidekiq.logger.error ex
24
+ Sidekiq.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
25
+ end
32
26
  end
33
27
  end
34
28
 
35
- def send_to_exception_notifier(hash, ex)
36
- ::ExceptionNotifier.notify_exception(ex, :data => {:message => hash})
37
- end
38
29
  end
39
30
  end
@@ -140,10 +140,10 @@ module Sidekiq
140
140
  end
141
141
  end
142
142
 
143
- # Creating the Redis#blpop command takes into account any
144
- # configured queue weights. By default Redis#blpop returns
143
+ # Creating the Redis#brpop command takes into account any
144
+ # configured queue weights. By default Redis#brpop returns
145
145
  # data from the first queue that has pending elements. We
146
- # recreate the queue command each time we invoke Redis#blpop
146
+ # recreate the queue command each time we invoke Redis#brpop
147
147
  # to honor weights and avoid queue starvation.
148
148
  def queues_cmd
149
149
  queues = @strictly_ordered_queues ? @unique_queues.dup : @queues.shuffle.uniq
@@ -36,6 +36,8 @@ module Sidekiq
36
36
  watchdog('Launcher#run') do
37
37
  manager.async.start
38
38
  poller.async.poll(true)
39
+
40
+ start_heartbeat
39
41
  end
40
42
  end
41
43
 
@@ -51,12 +53,37 @@ module Sidekiq
51
53
 
52
54
  # Requeue everything in case there was a worker who grabbed work while stopped
53
55
  Sidekiq::Fetcher.strategy.bulk_requeue([], @options)
56
+
57
+ stop_heartbeat
54
58
  end
55
59
  end
56
60
 
57
- def procline(tag)
58
- $0 = manager.procline(tag)
59
- manager.after(5) { procline(tag) }
61
+ private
62
+
63
+ def start_heartbeat
64
+ key = identity
65
+ data = {
66
+ 'hostname' => hostname,
67
+ 'started_at' => Time.now.to_f,
68
+ 'pid' => $$,
69
+ 'tag' => @options[:tag] || '',
70
+ 'concurrency' => @options[:concurrency],
71
+ 'queues' => @options[:queues].uniq,
72
+ }
73
+ Sidekiq.redis do |conn|
74
+ conn.multi do
75
+ conn.sadd('processes', key)
76
+ conn.hset(key, 'info', Sidekiq.dump_json(data))
77
+ conn.expire(key, 60)
78
+ end
79
+ end
80
+ manager.heartbeat(key, data)
81
+ end
82
+
83
+ def stop_heartbeat
84
+ Sidekiq.redis do |conn|
85
+ conn.srem('processes', identity)
86
+ end
60
87
  end
61
88
  end
62
89
  end
@@ -26,7 +26,7 @@ module Sidekiq
26
26
  end
27
27
 
28
28
  def self.initialize_logger(log_target = STDOUT)
29
- oldlogger = @logger
29
+ oldlogger = defined?(@logger) ? @logger : nil
30
30
  @logger = Logger.new(log_target)
31
31
  @logger.level = Logger::INFO
32
32
  @logger.formatter = Pretty.new
@@ -35,7 +35,7 @@ module Sidekiq
35
35
  end
36
36
 
37
37
  def self.logger
38
- @logger || initialize_logger
38
+ defined?(@logger) ? @logger : initialize_logger
39
39
  end
40
40
 
41
41
  def self.logger=(log)
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require 'sidekiq/util'
2
3
  require 'sidekiq/actor'
3
4
  require 'sidekiq/processor'
@@ -49,7 +50,6 @@ module Sidekiq
49
50
  @ready.each { |x| x.terminate if x.alive? }
50
51
  @ready.clear
51
52
 
52
- clear_worker_set
53
53
  return if clean_up_for_graceful_shutdown
54
54
 
55
55
  hard_shutdown_in timeout if should_shutdown
@@ -132,27 +132,30 @@ module Sidekiq
132
132
  @threads[proxy_id] = thr
133
133
  end
134
134
 
135
- def procline(tag)
136
- "sidekiq #{Sidekiq::VERSION} #{tag}[#{@busy.size} of #{@count} busy]#{stopped? ? ' stopping' : ''}"
135
+ def heartbeat(key, data)
136
+ return if stopped?
137
+
138
+ $0 = "sidekiq #{Sidekiq::VERSION} #{data['tag']}[#{@busy.size} of #{data['concurrency']} busy]#{stopped? ? ' stopping' : ''}"
139
+ ❤(key)
140
+ after(5) do
141
+ heartbeat(key, data)
142
+ end
137
143
  end
138
144
 
139
145
  private
140
146
 
141
- def clear_worker_set
142
- # Clearing workers in Redis
143
- # NOTE: we do this before terminating worker threads because the
144
- # process will likely receive a hard shutdown soon anyway, which
145
- # means the threads will killed.
146
- logger.debug { "Clearing workers in redis" }
147
- Sidekiq.redis do |conn|
148
- workers = conn.smembers('workers')
149
- workers_to_remove = workers.select do |worker_name|
150
- worker_name =~ /:#{process_id}-/
147
+ def ❤(key)
148
+ begin
149
+ Sidekiq.redis do |conn|
150
+ conn.multi do
151
+ conn.hmset(key, 'busy', @busy.size, 'beat', Time.now.to_f)
152
+ conn.expire(key, 60)
153
+ end
151
154
  end
152
- conn.srem('workers', workers_to_remove) if !workers_to_remove.empty?
155
+ rescue => e
156
+ # ignore all redis/network issues
157
+ logger.error("heartbeat: #{e.message}")
153
158
  end
154
- rescue => ex
155
- Sidekiq.logger.warn("Unable to clear worker set while shutting down: #{ex.message}")
156
159
  end
157
160
 
158
161
  def hard_shutdown_in(delay)
@@ -54,7 +54,7 @@ module Sidekiq
54
54
  # to Redis:
55
55
  #
56
56
  # class MyClientHook
57
- # def call(worker_class, msg, queue)
57
+ # def call(worker_class, msg, queue, redis_pool)
58
58
  # puts "Before push"
59
59
  # result = yield
60
60
  # puts "After push"
@@ -2,7 +2,7 @@ module Sidekiq::Middleware::I18n
2
2
  # Get the current locale and store it in the message
3
3
  # to be sent to Sidekiq.
4
4
  class Client
5
- def call(worker_class, msg, queue)
5
+ def call(worker_class, msg, queue, redis_pool)
6
6
  msg['locale'] ||= I18n.locale
7
7
  yield
8
8
  end
@@ -108,17 +108,33 @@ module Sidekiq
108
108
 
109
109
  private
110
110
 
111
+ DEAD_JOB_TIMEOUT = 180 * 24 * 60 * 60 # 6 months
112
+ MAX_JOBS = 10_000
113
+
111
114
  def retries_exhausted(worker, msg)
112
115
  logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
113
- if worker.respond_to?(:retries_exhausted)
114
- logger.warn { "Defining #{worker.class.name}#retries_exhausted as a method is deprecated, use `sidekiq_retries_exhausted` callback instead http://git.io/Ijju8g" }
115
- worker.retries_exhausted(*msg['args'])
116
- elsif worker.sidekiq_retries_exhausted_block?
117
- worker.sidekiq_retries_exhausted_block.call(msg)
116
+ begin
117
+ if worker.sidekiq_retries_exhausted_block?
118
+ worker.sidekiq_retries_exhausted_block.call(msg)
119
+ end
120
+ rescue => e
121
+ handle_exception(e, { :context => "Error calling retries_exhausted" })
118
122
  end
119
123
 
120
- rescue Exception => e
121
- handle_exception(e, { :context => "Error calling retries_exhausted" })
124
+ send_to_morgue(msg)
125
+ end
126
+
127
+ def send_to_morgue(msg)
128
+ Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
129
+ payload = Sidekiq.dump_json(msg)
130
+ now = Time.now.to_f
131
+ Sidekiq.redis do |conn|
132
+ conn.multi do
133
+ conn.zadd('dead', now, payload)
134
+ conn.zremrangebyscore('dead', '-inf', now - DEAD_JOB_TIMEOUT)
135
+ conn.zremrangebyrank('dead', 0, -MAX_JOBS)
136
+ end
137
+ end
122
138
  end
123
139
 
124
140
  def retry_attempts_from(msg_retry, default)