sidekiq 4.2.10 → 5.2.7

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 (75) 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 +3 -0
  5. data/.travis.yml +6 -13
  6. data/5.0-Upgrade.md +56 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +158 -1
  9. data/Ent-Changes.md +67 -2
  10. data/Gemfile +14 -20
  11. data/LICENSE +1 -1
  12. data/Pro-4.0-Upgrade.md +35 -0
  13. data/Pro-Changes.md +133 -2
  14. data/README.md +8 -6
  15. data/Rakefile +2 -5
  16. data/bin/sidekiqctl +13 -92
  17. data/bin/sidekiqload +5 -10
  18. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  19. data/lib/sidekiq.rb +27 -27
  20. data/lib/sidekiq/api.rb +145 -57
  21. data/lib/sidekiq/cli.rb +120 -81
  22. data/lib/sidekiq/client.rb +25 -18
  23. data/lib/sidekiq/core_ext.rb +1 -119
  24. data/lib/sidekiq/ctl.rb +221 -0
  25. data/lib/sidekiq/delay.rb +42 -0
  26. data/lib/sidekiq/exception_handler.rb +2 -4
  27. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  28. data/lib/sidekiq/fetch.rb +1 -1
  29. data/lib/sidekiq/job_logger.rb +25 -0
  30. data/lib/sidekiq/job_retry.rb +262 -0
  31. data/lib/sidekiq/launcher.rb +19 -19
  32. data/lib/sidekiq/logging.rb +18 -2
  33. data/lib/sidekiq/manager.rb +5 -6
  34. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  35. data/lib/sidekiq/processor.rb +126 -48
  36. data/lib/sidekiq/rails.rb +8 -73
  37. data/lib/sidekiq/redis_connection.rb +43 -5
  38. data/lib/sidekiq/scheduled.rb +35 -8
  39. data/lib/sidekiq/testing.rb +16 -7
  40. data/lib/sidekiq/util.rb +5 -2
  41. data/lib/sidekiq/version.rb +1 -1
  42. data/lib/sidekiq/web.rb +4 -4
  43. data/lib/sidekiq/web/action.rb +2 -6
  44. data/lib/sidekiq/web/application.rb +33 -16
  45. data/lib/sidekiq/web/helpers.rb +69 -22
  46. data/lib/sidekiq/web/router.rb +10 -10
  47. data/lib/sidekiq/worker.rb +118 -19
  48. data/sidekiq.gemspec +6 -17
  49. data/web/assets/javascripts/application.js +0 -0
  50. data/web/assets/javascripts/dashboard.js +32 -17
  51. data/web/assets/stylesheets/application-rtl.css +246 -0
  52. data/web/assets/stylesheets/application.css +371 -6
  53. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  54. data/web/assets/stylesheets/bootstrap.css +2 -2
  55. data/web/locales/ar.yml +81 -0
  56. data/web/locales/en.yml +2 -0
  57. data/web/locales/es.yml +4 -3
  58. data/web/locales/fa.yml +1 -0
  59. data/web/locales/he.yml +79 -0
  60. data/web/locales/ja.yml +5 -3
  61. data/web/locales/ur.yml +80 -0
  62. data/web/views/_footer.erb +5 -2
  63. data/web/views/_nav.erb +4 -18
  64. data/web/views/_paging.erb +1 -1
  65. data/web/views/busy.erb +9 -5
  66. data/web/views/dashboard.erb +1 -1
  67. data/web/views/layout.erb +11 -2
  68. data/web/views/morgue.erb +4 -4
  69. data/web/views/queue.erb +8 -7
  70. data/web/views/queues.erb +2 -0
  71. data/web/views/retries.erb +9 -5
  72. data/web/views/scheduled.erb +2 -2
  73. metadata +31 -160
  74. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  75. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq/scheduled'
3
+ require 'sidekiq/api'
4
+
5
+ module Sidekiq
6
+ ##
7
+ # Automatically retry jobs that fail in Sidekiq.
8
+ # Sidekiq's retry support assumes a typical development lifecycle:
9
+ #
10
+ # 0. Push some code changes with a bug in it.
11
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
12
+ # the job and pushes it onto a retry queue.
13
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
14
+ # an exponential delay, the job continues to fail.
15
+ # 3. After a few days, a developer deploys a fix. The job is
16
+ # reprocessed successfully.
17
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
18
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
19
+ # manually in the Web UI.
20
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
21
+ #
22
+ # A job looks like:
23
+ #
24
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
25
+ #
26
+ # The 'retry' option also accepts a number (in place of 'true'):
27
+ #
28
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
+ #
30
+ # The job will be retried this number of times before giving up. (If simply
31
+ # 'true', Sidekiq retries 25 times)
32
+ #
33
+ # We'll add a bit more data to the job to support retries:
34
+ #
35
+ # * 'queue' - the queue to use
36
+ # * 'retry_count' - number of times we've retried so far.
37
+ # * 'error_message' - the message from the exception
38
+ # * 'error_class' - the exception class
39
+ # * 'failed_at' - the first time it failed
40
+ # * 'retried_at' - the last time it was retried
41
+ # * 'backtrace' - the number of lines of error backtrace to store
42
+ #
43
+ # We don't store the backtrace by default as that can add a lot of overhead
44
+ # to the job and everyone is using an error service, right?
45
+ #
46
+ # The default number of retries is 25 which works out to about 3 weeks
47
+ # You can change the default maximum number of retries in your initializer:
48
+ #
49
+ # Sidekiq.options[:max_retries] = 7
50
+ #
51
+ # or limit the number of retries for a particular worker with:
52
+ #
53
+ # class MyWorker
54
+ # include Sidekiq::Worker
55
+ # sidekiq_options :retry => 10
56
+ # end
57
+ #
58
+ class JobRetry
59
+ class Handled < ::RuntimeError; end
60
+ class Skip < Handled; end
61
+
62
+ include Sidekiq::Util
63
+
64
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
65
+
66
+ def initialize(options = {})
67
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
68
+ end
69
+
70
+ # The global retry handler requires only the barest of data.
71
+ # We want to be able to retry as much as possible so we don't
72
+ # require the worker to be instantiated.
73
+ def global(msg, queue)
74
+ yield
75
+ rescue Handled => ex
76
+ raise ex
77
+ rescue Sidekiq::Shutdown => ey
78
+ # ignore, will be pushed back onto queue during hard_shutdown
79
+ raise ey
80
+ rescue Exception => e
81
+ # ignore, will be pushed back onto queue during hard_shutdown
82
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
83
+
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
97
+ end
98
+
99
+
100
+ # The local retry support means that any errors that occur within
101
+ # this block can be associated with the given worker instance.
102
+ # This is required to support the `sidekiq_retries_exhausted` block.
103
+ #
104
+ # Note that any exception from the block is wrapped in the Skip
105
+ # exception so the global block does not reprocess the error. The
106
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
107
+ # calling the handle_exception handlers.
108
+ def local(worker, msg, queue)
109
+ yield
110
+ rescue Handled => ex
111
+ raise ex
112
+ rescue Sidekiq::Shutdown => ey
113
+ # ignore, will be pushed back onto queue during hard_shutdown
114
+ raise ey
115
+ rescue Exception => e
116
+ # ignore, will be pushed back onto queue during hard_shutdown
117
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
118
+
119
+ if msg['retry'] == nil
120
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
121
+ end
122
+
123
+ raise e unless msg['retry']
124
+ attempt_retry(worker, msg, queue, e)
125
+ # We've handled this error associated with this job, don't
126
+ # need to handle it at the global level
127
+ raise Skip
128
+ end
129
+
130
+ private
131
+
132
+ # Note that +worker+ can be nil here if an error is raised before we can
133
+ # instantiate the worker instance. All access must be guarded and
134
+ # best effort.
135
+ def attempt_retry(worker, msg, queue, exception)
136
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
137
+
138
+ msg['queue'] = if msg['retry_queue']
139
+ msg['retry_queue']
140
+ else
141
+ queue
142
+ end
143
+
144
+ m = exception_message(exception)
145
+ if m.respond_to?(:scrub!)
146
+ m.force_encoding("utf-8")
147
+ m.scrub!
148
+ end
149
+
150
+ msg['error_message'] = m
151
+ msg['error_class'] = exception.class.name
152
+ count = if msg['retry_count']
153
+ msg['retried_at'] = Time.now.to_f
154
+ msg['retry_count'] += 1
155
+ else
156
+ msg['failed_at'] = Time.now.to_f
157
+ msg['retry_count'] = 0
158
+ end
159
+
160
+ if msg['backtrace'] == true
161
+ msg['error_backtrace'] = exception.backtrace
162
+ elsif !msg['backtrace']
163
+ # do nothing
164
+ elsif msg['backtrace'].to_i != 0
165
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
166
+ end
167
+
168
+ if count < max_retry_attempts
169
+ delay = delay_for(worker, count, exception)
170
+ # Logging here can break retries if the logging device raises ENOSPC #3979
171
+ #logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
172
+ retry_at = Time.now.to_f + delay
173
+ payload = Sidekiq.dump_json(msg)
174
+ Sidekiq.redis do |conn|
175
+ conn.zadd('retry', retry_at.to_s, payload)
176
+ end
177
+ else
178
+ # Goodbye dear message, you (re)tried your best I'm sure.
179
+ retries_exhausted(worker, msg, exception)
180
+ end
181
+ end
182
+
183
+ def retries_exhausted(worker, msg, exception)
184
+ begin
185
+ block = worker && worker.sidekiq_retries_exhausted_block
186
+ block.call(msg, exception) if block
187
+ rescue => e
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
197
+ end
198
+
199
+ send_to_morgue(msg) unless msg['dead'] == false
200
+ end
201
+
202
+ def send_to_morgue(msg)
203
+ logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
204
+ payload = Sidekiq.dump_json(msg)
205
+ DeadSet.new.kill(payload, notify_failure: false)
206
+ end
207
+
208
+ def retry_attempts_from(msg_retry, default)
209
+ if msg_retry.is_a?(Integer)
210
+ msg_retry
211
+ else
212
+ default
213
+ end
214
+ end
215
+
216
+ def delay_for(worker, count, exception)
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)
222
+ end
223
+
224
+ # delayed_job uses the same basic formula
225
+ def seconds_to_delay(count)
226
+ (count ** 4) + 15 + (rand(30)*(count+1))
227
+ end
228
+
229
+ def retry_in(worker, count, exception)
230
+ begin
231
+ worker.sidekiq_retry_in_block.call(count, exception)
232
+ rescue Exception => e
233
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
234
+ nil
235
+ end
236
+ end
237
+
238
+ def exception_caused_by_shutdown?(e, checked_causes = [])
239
+ return false unless e.cause
240
+
241
+ # Handle circular causes
242
+ checked_causes << e.object_id
243
+ return false if checked_causes.include?(e.cause.object_id)
244
+
245
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
246
+ exception_caused_by_shutdown?(e.cause, checked_causes)
247
+ end
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
+
261
+ end
262
+ end
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
  require 'sidekiq/manager'
4
3
  require 'sidekiq/fetch'
@@ -14,6 +13,8 @@ module Sidekiq
14
13
 
15
14
  attr_accessor :manager, :poller, :fetcher
16
15
 
16
+ STATS_TTL = 5*365*24*60*60
17
+
17
18
  def initialize(options)
18
19
  @manager = Sidekiq::Manager.new(options)
19
20
  @poller = Sidekiq::Scheduled::Poller.new
@@ -39,7 +40,7 @@ module Sidekiq
39
40
  # return until all work is complete and cleaned up.
40
41
  # It can take up to the timeout to complete.
41
42
  def stop
42
- deadline = Time.now + @options[:timeout]
43
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
43
44
 
44
45
  @done = true
45
46
  @manager.quiet
@@ -61,8 +62,6 @@ module Sidekiq
61
62
 
62
63
  private unless $TESTING
63
64
 
64
- JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
65
-
66
65
  def heartbeat
67
66
  results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
68
67
  results.compact!
@@ -75,19 +74,24 @@ module Sidekiq
75
74
  key = identity
76
75
  fails = procd = 0
77
76
  begin
78
- Processor::FAILURE.update {|curr| fails = curr; 0 }
79
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
77
+ fails = Processor::FAILURE.reset
78
+ procd = Processor::PROCESSED.reset
79
+ curstate = Processor::WORKER_STATE.dup
80
80
 
81
- workers_key = "#{key}:workers".freeze
82
- nowdate = Time.now.utc.strftime("%Y-%m-%d".freeze)
81
+ workers_key = "#{key}:workers"
82
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
83
83
  Sidekiq.redis do |conn|
84
84
  conn.multi do
85
- conn.incrby("stat:processed".freeze, procd)
85
+ conn.incrby("stat:processed", procd)
86
86
  conn.incrby("stat:processed:#{nowdate}", procd)
87
- conn.incrby("stat:failed".freeze, fails)
87
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
88
+
89
+ conn.incrby("stat:failed", fails)
88
90
  conn.incrby("stat:failed:#{nowdate}", fails)
91
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
+
89
93
  conn.del(workers_key)
90
- Processor::WORKER_STATE.each_pair do |tid, hash|
94
+ curstate.each_pair do |tid, hash|
91
95
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
92
96
  end
93
97
  conn.expire(workers_key, 60)
@@ -99,7 +103,7 @@ module Sidekiq
99
103
  conn.multi do
100
104
  conn.sadd('processes', key)
101
105
  conn.exists(key)
102
- conn.hmset(key, 'info', to_json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
106
+ conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
103
107
  conn.expire(key, 60)
104
108
  conn.rpop("#{key}-signals")
105
109
  end
@@ -110,17 +114,13 @@ module Sidekiq
110
114
 
111
115
  return unless msg
112
116
 
113
- if JVM_RESERVED_SIGNALS.include?(msg)
114
- Sidekiq::CLI.instance.handle_signal(msg)
115
- else
116
- ::Process.kill(msg, $$)
117
- end
117
+ ::Process.kill(msg, $$)
118
118
  rescue => e
119
119
  # ignore all redis/network issues
120
120
  logger.error("heartbeat: #{e.message}")
121
121
  # don't lose the counts if there was a network issue
122
- Processor::PROCESSED.increment(procd)
123
- Processor::FAILURE.increment(fails)
122
+ Processor::PROCESSED.incr(procd)
123
+ Processor::FAILURE.incr(fails)
124
124
  end
125
125
  end
126
126
 
@@ -11,7 +11,7 @@ module Sidekiq
11
11
 
12
12
  # Provide a call() method that returns the formatted message.
13
13
  def call(severity, time, program_name, message)
14
- "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
14
+ "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
15
15
  end
16
16
 
17
17
  def context
@@ -22,10 +22,26 @@ module Sidekiq
22
22
 
23
23
  class WithoutTimestamp < Pretty
24
24
  def call(severity, time, program_name, message)
25
- "#{::Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
25
+ "#{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
26
26
  end
27
27
  end
28
28
 
29
+ def self.tid
30
+ Thread.current['sidekiq_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
31
+ end
32
+
33
+ def self.job_hash_context(job_hash)
34
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
35
+ # attribute to expose the underlying thing.
36
+ klass = job_hash['wrapped'] || job_hash["class"]
37
+ bid = job_hash['bid']
38
+ "#{klass} JID-#{job_hash['jid']}#{" BID-#{bid}" if bid}"
39
+ end
40
+
41
+ def self.with_job_hash_context(job_hash, &block)
42
+ with_context(job_hash_context(job_hash), &block)
43
+ end
44
+
29
45
  def self.with_context(msg)
30
46
  Thread.current[:sidekiq_context] ||= []
31
47
  Thread.current[:sidekiq_context] << msg
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
  require 'sidekiq/util'
4
3
  require 'sidekiq/processor'
@@ -31,7 +30,7 @@ module Sidekiq
31
30
  def initialize(options={})
32
31
  logger.debug { options.inspect }
33
32
  @options = options
34
- @count = options[:concurrency] || 25
33
+ @count = options[:concurrency] || 10
35
34
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
36
35
 
37
36
  @done = false
@@ -54,7 +53,7 @@ module Sidekiq
54
53
 
55
54
  logger.info { "Terminating quiet workers" }
56
55
  @workers.each { |x| x.terminate }
57
- fire_event(:quiet, true)
56
+ fire_event(:quiet, reverse: true)
58
57
  end
59
58
 
60
59
  # hack for quicker development / testing environment #2774
@@ -62,7 +61,7 @@ module Sidekiq
62
61
 
63
62
  def stop(deadline)
64
63
  quiet
65
- fire_event(:shutdown, true)
64
+ fire_event(:shutdown, reverse: true)
66
65
 
67
66
  # some of the shutdown events can be async,
68
67
  # we don't have any way to know when they're done but
@@ -71,11 +70,11 @@ module Sidekiq
71
70
  return if @workers.empty?
72
71
 
73
72
  logger.info { "Pausing to allow workers to finish..." }
74
- remaining = deadline - Time.now
73
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
75
74
  while remaining > PAUSE_TIME
76
75
  return if @workers.empty?
77
76
  sleep PAUSE_TIME
78
- remaining = deadline - Time.now
77
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
79
78
  end
80
79
  return if @workers.empty?
81
80
 
@@ -1,7 +1,17 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  module Middleware
3
4
  module Server
4
5
  class ActiveRecord
6
+
7
+ def initialize
8
+ # With Rails 5+ we must use the Reloader **always**.
9
+ # The reloader handles code loading and db connection management.
10
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR >= 5
11
+ raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
12
+ end
13
+ end
14
+
5
15
  def call(*args)
6
16
  yield
7
17
  ensure