sidekiq 4.2.10 → 5.2.8

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 +164 -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/api.rb +145 -57
  20. data/lib/sidekiq/cli.rb +120 -81
  21. data/lib/sidekiq/client.rb +25 -18
  22. data/lib/sidekiq/core_ext.rb +1 -119
  23. data/lib/sidekiq/ctl.rb +221 -0
  24. data/lib/sidekiq/delay.rb +42 -0
  25. data/lib/sidekiq/exception_handler.rb +2 -4
  26. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  27. data/lib/sidekiq/fetch.rb +1 -1
  28. data/lib/sidekiq/job_logger.rb +25 -0
  29. data/lib/sidekiq/job_retry.rb +262 -0
  30. data/lib/sidekiq/launcher.rb +19 -19
  31. data/lib/sidekiq/logging.rb +18 -2
  32. data/lib/sidekiq/manager.rb +5 -6
  33. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  34. data/lib/sidekiq/processor.rb +126 -48
  35. data/lib/sidekiq/rails.rb +8 -73
  36. data/lib/sidekiq/redis_connection.rb +43 -5
  37. data/lib/sidekiq/scheduled.rb +35 -8
  38. data/lib/sidekiq/testing.rb +16 -7
  39. data/lib/sidekiq/util.rb +5 -2
  40. data/lib/sidekiq/version.rb +1 -1
  41. data/lib/sidekiq/web/action.rb +2 -6
  42. data/lib/sidekiq/web/application.rb +33 -16
  43. data/lib/sidekiq/web/helpers.rb +69 -22
  44. data/lib/sidekiq/web/router.rb +10 -10
  45. data/lib/sidekiq/web.rb +4 -4
  46. data/lib/sidekiq/worker.rb +118 -19
  47. data/lib/sidekiq.rb +27 -27
  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
@@ -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) && defined?(::Rails::VERSION) && ::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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  require 'sidekiq/util'
3
3
  require 'sidekiq/fetch'
4
+ require 'sidekiq/job_logger'
5
+ require 'sidekiq/job_retry'
4
6
  require 'thread'
5
- require 'concurrent/map'
6
- require 'concurrent/atomic/atomic_fixnum'
7
7
 
8
8
  module Sidekiq
9
9
  ##
@@ -37,7 +37,8 @@ module Sidekiq
37
37
  @thread = nil
38
38
  @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
39
  @reloader = Sidekiq.options[:reloader]
40
- @executor = Sidekiq.options[:executor]
40
+ @logging = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
41
+ @retrier = Sidekiq::JobRetry.new
41
42
  end
42
43
 
43
44
  def terminate(wait=false)
@@ -86,7 +87,7 @@ module Sidekiq
86
87
  def get_one
87
88
  begin
88
89
  work = @strategy.retrieve_work
89
- (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
90
+ (logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }; @down = nil) if @down
90
91
  work
91
92
  rescue Sidekiq::Shutdown
92
93
  rescue => ex
@@ -106,95 +107,172 @@ module Sidekiq
106
107
 
107
108
  def handle_fetch_exception(ex)
108
109
  if !@down
109
- @down = Time.now
110
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
110
111
  logger.error("Error fetching job: #{ex}")
111
- ex.backtrace.each do |bt|
112
- logger.error(bt)
113
- end
112
+ handle_exception(ex)
114
113
  end
115
114
  sleep(1)
116
115
  nil
117
116
  end
118
117
 
118
+ def dispatch(job_hash, queue)
119
+ # since middleware can mutate the job hash
120
+ # we clone here so we report the original
121
+ # job structure to the Web UI
122
+ pristine = cloned(job_hash)
123
+
124
+ Sidekiq::Logging.with_job_hash_context(job_hash) do
125
+ @retrier.global(pristine, queue) do
126
+ @logging.call(job_hash, queue) do
127
+ stats(pristine, queue) do
128
+ # Rails 5 requires a Reloader to wrap code execution. In order to
129
+ # constantize the worker and instantiate an instance, we have to call
130
+ # the Reloader. It handles code loading, db connection management, etc.
131
+ # Effectively this block denotes a "unit of work" to Rails.
132
+ @reloader.call do
133
+ klass = constantize(job_hash['class'])
134
+ worker = klass.new
135
+ worker.jid = job_hash['jid']
136
+ @retrier.local(worker, pristine, queue) do
137
+ yield worker
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
119
146
  def process(work)
120
147
  jobstr = work.job
121
148
  queue = work.queue_name
122
149
 
123
- ack = false
150
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
151
+ job_hash = nil
124
152
  begin
125
153
  job_hash = Sidekiq.load_json(jobstr)
126
- @reloader.call do
127
- klass = job_hash['class'.freeze].constantize
128
- worker = klass.new
129
- worker.jid = job_hash['jid'.freeze]
130
-
131
- stats(worker, job_hash, queue) do
132
- Sidekiq::Logging.with_context(log_context(job_hash)) do
133
- ack = true
134
- Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
135
- @executor.call do
136
- # Only ack if we either attempted to start this job or
137
- # successfully completed it. This prevents us from
138
- # losing jobs if a middleware raises an exception before yielding
139
- execute_job(worker, cloned(job_hash['args'.freeze]))
140
- end
141
- end
142
- end
154
+ rescue => ex
155
+ handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
156
+ # we can't notify because the job isn't a valid hash payload.
157
+ DeadSet.new.kill(jobstr, notify_failure: false)
158
+ return work.acknowledge
159
+ end
160
+
161
+ ack = true
162
+ begin
163
+ dispatch(job_hash, queue) do |worker|
164
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
165
+ execute_job(worker, cloned(job_hash['args']))
143
166
  end
144
- ack = true
145
167
  end
146
168
  rescue Sidekiq::Shutdown
147
169
  # Had to force kill this job because it didn't finish
148
170
  # within the timeout. Don't acknowledge the work since
149
171
  # we didn't properly finish it.
150
172
  ack = false
173
+ rescue Sidekiq::JobRetry::Handled => h
174
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
175
+ # signals that we created a retry successfully. We can acknowlege the job.
176
+ e = h.cause ? h.cause : h
177
+ handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
178
+ raise e
151
179
  rescue Exception => ex
152
- handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
153
- raise
180
+ # Unexpected error! This is very bad and indicates an exception that got past
181
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
182
+ # so it can be rescued when using Sidekiq Pro.
183
+ ack = false
184
+ handle_exception(ex, { :context => "Internal exception!", :job => job_hash, :jobstr => jobstr })
185
+ raise e
154
186
  ensure
155
187
  work.acknowledge if ack
156
188
  end
157
189
  end
158
190
 
159
- # If we're using a wrapper class, like ActiveJob, use the "wrapped"
160
- # attribute to expose the underlying thing.
161
- def log_context(item)
162
- klass = item['wrapped'.freeze] || item['class'.freeze]
163
- "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
164
- end
165
-
166
191
  def execute_job(worker, cloned_args)
167
192
  worker.perform(*cloned_args)
168
193
  end
169
194
 
170
- def thread_identity
171
- @str ||= Thread.current.object_id.to_s(36)
195
+ # Ruby doesn't provide atomic counters out of the box so we'll
196
+ # implement something simple ourselves.
197
+ # https://bugs.ruby-lang.org/issues/14706
198
+ class Counter
199
+ def initialize
200
+ @value = 0
201
+ @lock = Mutex.new
202
+ end
203
+
204
+ def incr(amount=1)
205
+ @lock.synchronize { @value = @value + amount }
206
+ end
207
+
208
+ def reset
209
+ @lock.synchronize { val = @value; @value = 0; val }
210
+ end
172
211
  end
173
212
 
174
- WORKER_STATE = Concurrent::Map.new
175
- PROCESSED = Concurrent::AtomicFixnum.new
176
- FAILURE = Concurrent::AtomicFixnum.new
213
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
214
+ class SharedWorkerState
215
+ def initialize
216
+ @worker_state = {}
217
+ @lock = Mutex.new
218
+ end
177
219
 
178
- def stats(worker, job_hash, queue)
179
- tid = thread_identity
180
- WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
220
+ def set(tid, hash)
221
+ @lock.synchronize { @worker_state[tid] = hash }
222
+ end
223
+
224
+ def delete(tid)
225
+ @lock.synchronize { @worker_state.delete(tid) }
226
+ end
227
+
228
+ def dup
229
+ @lock.synchronize { @worker_state.dup }
230
+ end
231
+
232
+ def size
233
+ @lock.synchronize { @worker_state.size }
234
+ end
235
+
236
+ def clear
237
+ @lock.synchronize { @worker_state.clear }
238
+ end
239
+ end
240
+
241
+ PROCESSED = Counter.new
242
+ FAILURE = Counter.new
243
+ WORKER_STATE = SharedWorkerState.new
244
+
245
+ def stats(job_hash, queue)
246
+ tid = Sidekiq::Logging.tid
247
+ WORKER_STATE.set(tid, {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i })
181
248
 
182
249
  begin
183
250
  yield
184
251
  rescue Exception
185
- FAILURE.increment
252
+ FAILURE.incr
186
253
  raise
187
254
  ensure
188
255
  WORKER_STATE.delete(tid)
189
- PROCESSED.increment
256
+ PROCESSED.incr
190
257
  end
191
258
  end
192
259
 
193
260
  # Deep clone the arguments passed to the worker so that if
194
261
  # the job fails, what is pushed back onto Redis hasn't
195
262
  # been mutated by the worker.
196
- def cloned(ary)
197
- Marshal.load(Marshal.dump(ary))
263
+ def cloned(thing)
264
+ Marshal.load(Marshal.dump(thing))
265
+ end
266
+
267
+ def constantize(str)
268
+ names = str.split('::')
269
+ names.shift if names.empty? || names.first.empty?
270
+
271
+ names.inject(Object) do |constant, name|
272
+ # the false flag limits search for name to under the constant namespace
273
+ # which mimics Rails' behaviour
274
+ constant.const_defined?(name, false) ? constant.const_get(name, false) : constant.const_missing(name)
275
+ end
198
276
  end
199
277
 
200
278
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -1,36 +1,6 @@
1
1
  # frozen_string_literal: true
2
- module Sidekiq
3
- def self.hook_rails!
4
- return if defined?(@delay_removed)
5
-
6
- ActiveSupport.on_load(:active_record) do
7
- include Sidekiq::Extensions::ActiveRecord
8
- end
9
-
10
- ActiveSupport.on_load(:action_mailer) do
11
- extend Sidekiq::Extensions::ActionMailer
12
- end
13
-
14
- Module.__send__(:include, Sidekiq::Extensions::Klass)
15
- end
16
-
17
- # Removes the generic aliases which MAY clash with names of already
18
- # created methods by other applications. The methods `sidekiq_delay`,
19
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
- def self.remove_delay!
21
- @delay_removed = true
22
-
23
- [Extensions::ActiveRecord,
24
- Extensions::ActionMailer,
25
- Extensions::Klass].each do |mod|
26
- mod.module_eval do
27
- remove_method :delay if respond_to?(:delay)
28
- remove_method :delay_for if respond_to?(:delay_for)
29
- remove_method :delay_until if respond_to?(:delay_until)
30
- end
31
- end
32
- end
33
2
 
3
+ module Sidekiq
34
4
  class Rails < ::Rails::Engine
35
5
  # We need to setup this up before any application configuration which might
36
6
  # change Sidekiq middleware.
@@ -48,10 +18,6 @@ module Sidekiq
48
18
  end
49
19
  end
50
20
 
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
21
  config.after_initialize do
56
22
  # This hook happens after all initializers are run, just before returning
57
23
  # from config/environment.rb back to sidekiq/cli.rb.
@@ -62,40 +28,11 @@ module Sidekiq
62
28
  #
63
29
  Sidekiq.configure_server do |_|
64
30
  if ::Rails::VERSION::MAJOR >= 5
65
- # The reloader also takes care of ActiveRecord but is incompatible with
66
- # the ActiveRecord middleware so make sure it's not in the chain already.
67
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
68
- raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
69
- elsif ::Rails.application.config.cache_classes
70
- # The reloader API has proven to be troublesome under load in production.
71
- # We won't use it at all when classes are cached, see #3154
72
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
73
- Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
74
- else
75
- Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
76
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
77
- Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
78
- end
31
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
79
32
  end
80
33
  end
81
34
  end
82
35
 
83
- class Executor
84
- def initialize(app = ::Rails.application)
85
- @app = app
86
- end
87
-
88
- def call
89
- @app.executor.wrap do
90
- yield
91
- end
92
- end
93
-
94
- def inspect
95
- "#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
96
- end
97
- end
98
-
99
36
  class Reloader
100
37
  def initialize(app = ::Rails.application)
101
38
  @app = app
@@ -111,13 +48,11 @@ module Sidekiq
111
48
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
112
49
  end
113
50
  end
114
-
115
- module PsychAutoload
116
- def resolve_class(klass_name)
117
- klass_name && klass_name.constantize
118
- rescue NameError
119
- super
120
- end
121
- end
122
51
  end if defined?(::Rails)
123
52
  end
53
+
54
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
55
+ $stderr.puts("**************************************************")
56
+ $stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
57
+ $stderr.puts("**************************************************")
58
+ end
@@ -8,11 +8,22 @@ module Sidekiq
8
8
  class << self
9
9
 
10
10
  def create(options={})
11
- options = options.symbolize_keys
11
+ options.keys.each do |key|
12
+ options[key.to_sym] = options.delete(key)
13
+ end
12
14
 
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
13
16
  options[:url] ||= determine_redis_provider
14
17
 
15
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
18
+ size = if options[:size]
19
+ options[:size]
20
+ elsif Sidekiq.server?
21
+ Sidekiq.options[:concurrency] + 5
22
+ elsif ENV['RAILS_MAX_THREADS']
23
+ Integer(ENV['RAILS_MAX_THREADS'])
24
+ else
25
+ 5
26
+ end
16
27
 
17
28
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
29
 
@@ -35,7 +46,7 @@ module Sidekiq
35
46
  # - enterprise's leader election
36
47
  # - enterprise's cron support
37
48
  def verify_sizing(size, concurrency)
38
- raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
49
+ raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size <= concurrency
39
50
  end
40
51
 
41
52
  def build_client(options)
@@ -67,7 +78,7 @@ module Sidekiq
67
78
  opts.delete(:network_timeout)
68
79
  end
69
80
 
70
- opts[:driver] ||= 'ruby'
81
+ opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
71
82
 
72
83
  # Issue #3303, redis-rb will silently retry an operation.
73
84
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -98,7 +109,34 @@ module Sidekiq
98
109
  end
99
110
 
100
111
  def determine_redis_provider
101
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
112
+ # If you have this in your environment:
113
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
114
+ # then set:
115
+ # REDIS_PROVIDER=MY_REDIS_URL
116
+ # and Sidekiq will find your custom URL variable with no custom
117
+ # initialization code at all.
118
+ p = ENV['REDIS_PROVIDER']
119
+ if p && p =~ /\:/
120
+ Sidekiq.logger.error <<-EOM
121
+
122
+ #################################################################################
123
+
124
+ REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
125
+ Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
126
+
127
+ REDIS_PROVIDER=REDISTOGO_URL
128
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
129
+
130
+ Use REDIS_URL if you wish to point Sidekiq to a URL directly.
131
+
132
+ This configuration error will crash starting in Sidekiq 5.3.
133
+
134
+ #################################################################################
135
+ EOM
136
+ end
137
+ ENV[
138
+ ENV['REDIS_PROVIDER'] || 'REDIS_URL'
139
+ ]
102
140
  end
103
141
 
104
142
  end
@@ -17,7 +17,7 @@ module Sidekiq
17
17
  # We need to go through the list one at a time to reduce the risk of something
18
18
  # going wrong between the time jobs are popped from the scheduled queue and when
19
19
  # they are pushed onto a work queue and losing the jobs.
20
- while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, now, :limit => [0, 1]).first do
20
+ while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
21
21
 
22
22
  # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
23
  # the queue, it's because another process already popped it so we can move on to the
@@ -79,9 +79,7 @@ module Sidekiq
79
79
  # Most likely a problem with redis networking.
80
80
  # Punt and try again at the next interval
81
81
  logger.error ex.message
82
- ex.backtrace.each do |bt|
83
- logger.error(bt)
84
- end
82
+ handle_exception(ex)
85
83
  end
86
84
  end
87
85
 
@@ -95,13 +93,38 @@ module Sidekiq
95
93
  # if poll_interval_average hasn't been calculated yet, we can
96
94
  # raise an error trying to reach Redis.
97
95
  logger.error ex.message
98
- logger.error ex.backtrace.first
96
+ handle_exception(ex)
99
97
  sleep 5
100
98
  end
101
99
 
102
- # Calculates a random interval that is ±50% the desired average.
103
100
  def random_poll_interval
104
- poll_interval_average * rand + poll_interval_average.to_f / 2
101
+ # We want one Sidekiq process to schedule jobs every N seconds. We have M processes
102
+ # and **don't** want to coordinate.
103
+ #
104
+ # So in N*M second timespan, we want each process to schedule once. The basic loop is:
105
+ #
106
+ # * sleep a random amount within that N*M timespan
107
+ # * wake up and schedule
108
+ #
109
+ # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
110
+ # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
111
+ # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
112
+ # iteration could see each process sleep for 1 second, undercutting our average.
113
+ #
114
+ # So below 10 processes, we special case and ensure the processes sleep closer to the average.
115
+ # In the example above, each process should schedule every 10 seconds on average. We special
116
+ # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
117
+ # As we run more processes, the scheduling interval average will approach an even spread
118
+ # between 0 and poll interval so we don't need this artifical boost.
119
+ #
120
+ if process_count < 10
121
+ # For small clusters, calculate a random interval that is ±50% the desired average.
122
+ poll_interval_average * rand + poll_interval_average.to_f / 2
123
+ else
124
+ # With 10+ processes, we should have enough randomness to get decent polling
125
+ # across the entire timespan
126
+ poll_interval_average * rand
127
+ end
105
128
  end
106
129
 
107
130
  # We do our best to tune the poll interval to the size of the active Sidekiq
@@ -125,9 +148,13 @@ module Sidekiq
125
148
  # This minimizes a single point of failure by dispersing check-ins but without taxing
126
149
  # Redis if you run many Sidekiq processes.
127
150
  def scaled_poll_interval
151
+ process_count * Sidekiq.options[:average_scheduled_poll_interval]
152
+ end
153
+
154
+ def process_count
128
155
  pcount = Sidekiq::ProcessSet.new.size
129
156
  pcount = 1 if pcount == 0
130
- pcount * Sidekiq.options[:average_scheduled_poll_interval]
157
+ pcount
131
158
  end
132
159
 
133
160
  def initial_wait
@@ -55,6 +55,15 @@ module Sidekiq
55
55
  yield @server_chain if block_given?
56
56
  @server_chain
57
57
  end
58
+
59
+ def constantize(str)
60
+ names = str.split('::')
61
+ names.shift if names.empty? || names.first.empty?
62
+
63
+ names.inject(Object) do |constant, name|
64
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
65
+ end
66
+ end
58
67
  end
59
68
  end
60
69
 
@@ -63,9 +72,7 @@ module Sidekiq
63
72
 
64
73
  class EmptyQueueError < RuntimeError; end
65
74
 
66
- class Client
67
- alias_method :raw_push_real, :raw_push
68
-
75
+ module TestingClient
69
76
  def raw_push(payloads)
70
77
  if Sidekiq::Testing.fake?
71
78
  payloads.each do |job|
@@ -76,18 +83,20 @@ module Sidekiq
76
83
  true
77
84
  elsif Sidekiq::Testing.inline?
78
85
  payloads.each do |job|
79
- klass = job['class'].constantize
86
+ klass = Sidekiq::Testing.constantize(job['class'])
80
87
  job['id'] ||= SecureRandom.hex(12)
81
88
  job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
82
89
  klass.process_job(job_hash)
83
90
  end
84
91
  true
85
92
  else
86
- raw_push_real(payloads)
93
+ super
87
94
  end
88
95
  end
89
96
  end
90
97
 
98
+ Sidekiq::Client.prepend TestingClient
99
+
91
100
  module Queues
92
101
  ##
93
102
  # The Queues class is only for testing the fake queue implementation.
@@ -309,7 +318,7 @@ module Sidekiq
309
318
  worker_classes = jobs.map { |job| job["class"] }.uniq
310
319
 
311
320
  worker_classes.each do |worker_class|
312
- worker_class.constantize.drain
321
+ Sidekiq::Testing.constantize(worker_class).drain
313
322
  end
314
323
  end
315
324
  end
@@ -317,7 +326,7 @@ module Sidekiq
317
326
  end
318
327
  end
319
328
 
320
- if defined?(::Rails) && !Rails.env.test?
329
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
321
330
  puts("**************************************************")
322
331
  puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
323
332
  puts("**************************************************")