sidekiq 5.0.0 → 6.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 (79) 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 +1 -1
  5. data/.standard.yml +20 -0
  6. data/6.0-Upgrade.md +70 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +169 -1
  9. data/Ent-2.0-Upgrade.md +37 -0
  10. data/Ent-Changes.md +76 -0
  11. data/Gemfile +16 -21
  12. data/Gemfile.lock +196 -0
  13. data/LICENSE +1 -1
  14. data/Pro-4.0-Upgrade.md +35 -0
  15. data/Pro-5.0-Upgrade.md +25 -0
  16. data/Pro-Changes.md +137 -1
  17. data/README.md +18 -30
  18. data/Rakefile +6 -8
  19. data/bin/sidekiqload +28 -24
  20. data/bin/sidekiqmon +9 -0
  21. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  22. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  23. data/lib/generators/sidekiq/worker_generator.rb +12 -14
  24. data/lib/sidekiq.rb +69 -49
  25. data/lib/sidekiq/api.rb +216 -160
  26. data/lib/sidekiq/cli.rb +174 -207
  27. data/lib/sidekiq/client.rb +55 -51
  28. data/lib/sidekiq/delay.rb +24 -4
  29. data/lib/sidekiq/exception_handler.rb +12 -16
  30. data/lib/sidekiq/extensions/action_mailer.rb +10 -20
  31. data/lib/sidekiq/extensions/active_record.rb +9 -7
  32. data/lib/sidekiq/extensions/class_methods.rb +9 -7
  33. data/lib/sidekiq/extensions/generic_proxy.rb +4 -4
  34. data/lib/sidekiq/fetch.rb +5 -6
  35. data/lib/sidekiq/job_logger.rb +42 -14
  36. data/lib/sidekiq/job_retry.rb +71 -57
  37. data/lib/sidekiq/launcher.rb +74 -60
  38. data/lib/sidekiq/logger.rb +69 -0
  39. data/lib/sidekiq/manager.rb +12 -15
  40. data/lib/sidekiq/middleware/chain.rb +3 -2
  41. data/lib/sidekiq/middleware/i18n.rb +5 -7
  42. data/lib/sidekiq/monitor.rb +148 -0
  43. data/lib/sidekiq/paginator.rb +11 -12
  44. data/lib/sidekiq/processor.rb +126 -82
  45. data/lib/sidekiq/rails.rb +24 -32
  46. data/lib/sidekiq/redis_connection.rb +46 -14
  47. data/lib/sidekiq/scheduled.rb +50 -25
  48. data/lib/sidekiq/testing.rb +35 -27
  49. data/lib/sidekiq/testing/inline.rb +2 -1
  50. data/lib/sidekiq/util.rb +20 -14
  51. data/lib/sidekiq/version.rb +2 -1
  52. data/lib/sidekiq/web.rb +45 -53
  53. data/lib/sidekiq/web/action.rb +14 -10
  54. data/lib/sidekiq/web/application.rb +83 -58
  55. data/lib/sidekiq/web/helpers.rb +105 -67
  56. data/lib/sidekiq/web/router.rb +18 -15
  57. data/lib/sidekiq/worker.rb +144 -41
  58. data/sidekiq.gemspec +16 -27
  59. data/web/assets/javascripts/application.js +0 -0
  60. data/web/assets/javascripts/dashboard.js +21 -23
  61. data/web/assets/stylesheets/application.css +35 -2
  62. data/web/assets/stylesheets/bootstrap.css +2 -2
  63. data/web/locales/ar.yml +1 -0
  64. data/web/locales/en.yml +2 -0
  65. data/web/locales/es.yml +4 -3
  66. data/web/locales/ja.yml +7 -4
  67. data/web/views/_footer.erb +4 -1
  68. data/web/views/_nav.erb +3 -17
  69. data/web/views/busy.erb +5 -1
  70. data/web/views/layout.erb +1 -1
  71. data/web/views/queue.erb +1 -0
  72. data/web/views/queues.erb +2 -0
  73. data/web/views/retries.erb +4 -0
  74. metadata +25 -171
  75. data/.travis.yml +0 -18
  76. data/bin/sidekiqctl +0 -99
  77. data/lib/sidekiq/core_ext.rb +0 -119
  78. data/lib/sidekiq/logging.rb +0 -106
  79. data/lib/sidekiq/middleware/server/active_record.rb +0 -22
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require "sidekiq/worker"
4
+
2
5
  module Sidekiq
3
6
  class Rails < ::Rails::Engine
4
- # We need to setup this up before any application configuration which might
5
- # change Sidekiq middleware.
7
+ # By including the Options module, we allow AJs to directly control sidekiq features
8
+ # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
9
+ # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
10
+ # manually retried, don't automatically die, etc.
6
11
  #
7
- # This hook happens after `Rails::Application` is inherited within
8
- # config/application.rb and before config is touched, usually within the
9
- # class block. Definitely before config/environments/*.rb and
10
- # config/initializers/*.rb.
11
- config.before_configuration do
12
- if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
13
- Sidekiq.server_middleware do |chain|
14
- require 'sidekiq/middleware/server/active_record'
15
- chain.add Sidekiq::Middleware::Server::ActiveRecord
16
- end
12
+ # class SomeJob < ActiveJob::Base
13
+ # queue_as :default
14
+ # sidekiq_options retry: 3, backtrace: 10
15
+ # def perform
16
+ # end
17
+ # end
18
+ initializer "sidekiq.active_job_integration" do
19
+ ActiveSupport.on_load(:active_job) do
20
+ include ::Sidekiq::Worker::Options unless respond_to?(:sidekiq_options)
17
21
  end
18
22
  end
19
23
 
24
+ # This hook happens after all initializers are run, just before returning
25
+ # from config/environment.rb back to sidekiq/cli.rb.
26
+ # We have to add the reloader after initialize to see if cache_classes has
27
+ # been turned on.
28
+ #
29
+ # None of this matters on the client-side, only within the Sidekiq process itself.
20
30
  config.after_initialize do
21
- # This hook happens after all initializers are run, just before returning
22
- # from config/environment.rb back to sidekiq/cli.rb.
23
- # We have to add the reloader after initialize to see if cache_classes has
24
- # been turned on.
25
- #
26
- # None of this matters on the client-side, only within the Sidekiq process itself.
27
- #
28
31
  Sidekiq.configure_server do |_|
29
- if ::Rails::VERSION::MAJOR >= 5
30
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
31
- Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
32
- end
32
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
33
33
  end
34
34
  end
35
35
 
@@ -48,13 +48,5 @@ module Sidekiq
48
48
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
49
49
  end
50
50
  end
51
-
52
- module PsychAutoload
53
- def resolve_class(klass_name)
54
- klass_name && klass_name.constantize
55
- rescue NameError
56
- super
57
- end
58
- end
59
- end if defined?(::Rails)
51
+ end
60
52
  end
@@ -1,25 +1,38 @@
1
1
  # frozen_string_literal: true
2
- require 'connection_pool'
3
- require 'redis'
4
- require 'uri'
2
+
3
+ require "connection_pool"
4
+ require "redis"
5
+ require "uri"
5
6
 
6
7
  module Sidekiq
7
8
  class RedisConnection
8
9
  class << self
10
+ def create(options = {})
11
+ options.keys.each do |key|
12
+ options[key.to_sym] = options.delete(key)
13
+ end
9
14
 
10
- def create(options={})
11
- options = options.symbolize_keys
12
-
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{::Process.pid}" unless options.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
+ # Give ourselves plenty of connections. pool is lazy
22
+ # so we won't create them until we need them.
23
+ Sidekiq.options[:concurrency] + 5
24
+ elsif ENV["RAILS_MAX_THREADS"]
25
+ Integer(ENV["RAILS_MAX_THREADS"])
26
+ else
27
+ 5
28
+ end
16
29
 
17
30
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
31
 
19
32
  pool_timeout = options[:pool_timeout] || 1
20
33
  log_info(options)
21
34
 
22
- ConnectionPool.new(:timeout => pool_timeout, :size => size) do
35
+ ConnectionPool.new(timeout: pool_timeout, size: size) do
23
36
  build_client(options)
24
37
  end
25
38
  end
@@ -35,7 +48,7 @@ module Sidekiq
35
48
  # - enterprise's leader election
36
49
  # - enterprise's cron support
37
50
  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
51
+ 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 + 2)
39
52
  end
40
53
 
41
54
  def build_client(options)
@@ -44,8 +57,8 @@ module Sidekiq
44
57
  client = Redis.new client_opts(options)
45
58
  if namespace
46
59
  begin
47
- require 'redis/namespace'
48
- Redis::Namespace.new(namespace, :redis => client)
60
+ require "redis/namespace"
61
+ Redis::Namespace.new(namespace, redis: client)
49
62
  rescue LoadError
50
63
  Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
51
64
  "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
@@ -67,7 +80,7 @@ module Sidekiq
67
80
  opts.delete(:network_timeout)
68
81
  end
69
82
 
70
- opts[:driver] ||= 'ruby'.freeze
83
+ opts[:driver] ||= Redis::Connection.drivers.last || "ruby"
71
84
 
72
85
  # Issue #3303, redis-rb will silently retry an operation.
73
86
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -98,9 +111,28 @@ module Sidekiq
98
111
  end
99
112
 
100
113
  def determine_redis_provider
101
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
102
- end
114
+ # If you have this in your environment:
115
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
116
+ # then set:
117
+ # REDIS_PROVIDER=MY_REDIS_URL
118
+ # and Sidekiq will find your custom URL variable with no custom
119
+ # initialization code at all.
120
+ #
121
+ p = ENV["REDIS_PROVIDER"]
122
+ if p && p =~ /\:/
123
+ raise <<~EOM
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 will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.:
126
+
127
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
128
+ REDIS_PROVIDER=REDISTOGO_URL
129
+ EOM
130
+ end
103
131
 
132
+ ENV[
133
+ p || "REDIS_URL"
134
+ ]
135
+ end
104
136
  end
105
137
  end
106
138
  end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
- require 'sidekiq/util'
4
- require 'sidekiq/api'
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/util"
5
+ require "sidekiq/api"
5
6
 
6
7
  module Sidekiq
7
8
  module Scheduled
8
- SETS = %w(retry schedule)
9
+ SETS = %w[retry schedule]
9
10
 
10
11
  class Enq
11
- def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
12
+ def enqueue_jobs(now = Time.now.to_f.to_s, sorted_sets = SETS)
12
13
  # A job's "score" in Redis is the time at which it should be processed.
13
14
  # Just check Redis for the set of jobs with a timestamp before now.
14
15
  Sidekiq.redis do |conn|
@@ -17,14 +18,14 @@ module Sidekiq
17
18
  # We need to go through the list one at a time to reduce the risk of something
18
19
  # going wrong between the time jobs are popped from the scheduled queue and when
19
20
  # 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
21
+ while (job = conn.zrangebyscore(sorted_set, "-inf", now, limit: [0, 1]).first)
21
22
 
22
23
  # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
24
  # the queue, it's because another process already popped it so we can move on to the
24
25
  # next one.
25
26
  if conn.zrem(sorted_set, job)
26
27
  Sidekiq::Client.push(Sidekiq.load_json(job))
27
- Sidekiq::Logging.logger.debug { "enqueued #{sorted_set}: #{job}" }
28
+ Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
28
29
  end
29
30
  end
30
31
  end
@@ -61,28 +62,24 @@ module Sidekiq
61
62
  end
62
63
 
63
64
  def start
64
- @thread ||= safe_thread("scheduler") do
65
+ @thread ||= safe_thread("scheduler") {
65
66
  initial_wait
66
67
 
67
- while !@done
68
+ until @done
68
69
  enqueue
69
70
  wait
70
71
  end
71
72
  Sidekiq.logger.info("Scheduler exiting...")
72
- end
73
+ }
73
74
  end
74
75
 
75
76
  def enqueue
76
- begin
77
- @enq.enqueue_jobs
78
- rescue => ex
79
- # Most likely a problem with redis networking.
80
- # Punt and try again at the next interval
81
- logger.error ex.message
82
- ex.backtrace.each do |bt|
83
- logger.error(bt)
84
- end
85
- end
77
+ @enq.enqueue_jobs
78
+ rescue => ex
79
+ # Most likely a problem with redis networking.
80
+ # Punt and try again at the next interval
81
+ logger.error ex.message
82
+ handle_exception(ex)
86
83
  end
87
84
 
88
85
  private
@@ -95,13 +92,38 @@ module Sidekiq
95
92
  # if poll_interval_average hasn't been calculated yet, we can
96
93
  # raise an error trying to reach Redis.
97
94
  logger.error ex.message
98
- logger.error ex.backtrace.first
95
+ handle_exception(ex)
99
96
  sleep 5
100
97
  end
101
98
 
102
- # Calculates a random interval that is ±50% the desired average.
103
99
  def random_poll_interval
104
- poll_interval_average * rand + poll_interval_average.to_f / 2
100
+ # We want one Sidekiq process to schedule jobs every N seconds. We have M processes
101
+ # and **don't** want to coordinate.
102
+ #
103
+ # So in N*M second timespan, we want each process to schedule once. The basic loop is:
104
+ #
105
+ # * sleep a random amount within that N*M timespan
106
+ # * wake up and schedule
107
+ #
108
+ # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
109
+ # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
110
+ # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
111
+ # iteration could see each process sleep for 1 second, undercutting our average.
112
+ #
113
+ # So below 10 processes, we special case and ensure the processes sleep closer to the average.
114
+ # In the example above, each process should schedule every 10 seconds on average. We special
115
+ # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
116
+ # As we run more processes, the scheduling interval average will approach an even spread
117
+ # between 0 and poll interval so we don't need this artifical boost.
118
+ #
119
+ if process_count < 10
120
+ # For small clusters, calculate a random interval that is ±50% the desired average.
121
+ poll_interval_average * rand + poll_interval_average.to_f / 2
122
+ else
123
+ # With 10+ processes, we should have enough randomness to get decent polling
124
+ # across the entire timespan
125
+ poll_interval_average * rand
126
+ end
105
127
  end
106
128
 
107
129
  # We do our best to tune the poll interval to the size of the active Sidekiq
@@ -125,9 +147,13 @@ module Sidekiq
125
147
  # This minimizes a single point of failure by dispersing check-ins but without taxing
126
148
  # Redis if you run many Sidekiq processes.
127
149
  def scaled_poll_interval
150
+ process_count * Sidekiq.options[:average_scheduled_poll_interval]
151
+ end
152
+
153
+ def process_count
128
154
  pcount = Sidekiq::ProcessSet.new.size
129
155
  pcount = 1 if pcount == 0
130
- pcount * Sidekiq.options[:average_scheduled_poll_interval]
156
+ pcount
131
157
  end
132
158
 
133
159
  def initial_wait
@@ -141,7 +167,6 @@ module Sidekiq
141
167
  @sleeper.pop(total)
142
168
  rescue Timeout::Error
143
169
  end
144
-
145
170
  end
146
171
  end
147
172
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
- require 'securerandom'
3
- require 'sidekiq'
4
2
 
5
- module Sidekiq
3
+ require "securerandom"
4
+ require "sidekiq"
6
5
 
6
+ module Sidekiq
7
7
  class Testing
8
8
  class << self
9
9
  attr_accessor :__test_mode
10
10
 
11
11
  def __set_test_mode(mode)
12
12
  if block_given?
13
- current_mode = self.__test_mode
13
+ current_mode = __test_mode
14
14
  begin
15
15
  self.__test_mode = mode
16
16
  yield
@@ -35,19 +35,19 @@ module Sidekiq
35
35
  end
36
36
 
37
37
  def enabled?
38
- self.__test_mode != :disable
38
+ __test_mode != :disable
39
39
  end
40
40
 
41
41
  def disabled?
42
- self.__test_mode == :disable
42
+ __test_mode == :disable
43
43
  end
44
44
 
45
45
  def fake?
46
- self.__test_mode == :fake
46
+ __test_mode == :fake
47
47
  end
48
48
 
49
49
  def inline?
50
- self.__test_mode == :inline
50
+ __test_mode == :inline
51
51
  end
52
52
 
53
53
  def server_middleware
@@ -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,31 +72,31 @@ 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|
72
79
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
73
- job.merge!('enqueued_at' => Time.now.to_f) unless job['at']
74
- Queues.push(job['queue'], job['class'], job)
80
+ job["enqueued_at"] = Time.now.to_f unless job["at"]
81
+ Queues.push(job["queue"], job["class"], job)
75
82
  end
76
83
  true
77
84
  elsif Sidekiq::Testing.inline?
78
85
  payloads.each do |job|
79
- klass = job['class'].constantize
80
- job['id'] ||= SecureRandom.hex(12)
86
+ klass = Sidekiq::Testing.constantize(job["class"])
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.
@@ -246,27 +255,26 @@ module Sidekiq
246
255
  # Then I should receive a welcome email to "foo@example.com"
247
256
  #
248
257
  module ClassMethods
249
-
250
258
  # Queue for this worker
251
259
  def queue
252
- self.sidekiq_options["queue"]
260
+ get_sidekiq_options["queue"]
253
261
  end
254
262
 
255
263
  # Jobs queued for this worker
256
264
  def jobs
257
- Queues.jobs_by_worker[self.to_s]
265
+ Queues.jobs_by_worker[to_s]
258
266
  end
259
267
 
260
268
  # Clear all jobs for this worker
261
269
  def clear
262
- Queues.clear_for(queue, self.to_s)
270
+ Queues.clear_for(queue, to_s)
263
271
  end
264
272
 
265
273
  # Drain and run all jobs for this worker
266
274
  def drain
267
275
  while jobs.any?
268
276
  next_job = jobs.first
269
- Queues.delete_for(next_job["jid"], next_job["queue"], self.to_s)
277
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
270
278
  process_job(next_job)
271
279
  end
272
280
  end
@@ -275,16 +283,16 @@ module Sidekiq
275
283
  def perform_one
276
284
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
277
285
  next_job = jobs.first
278
- Queues.delete_for(next_job["jid"], queue, self.to_s)
286
+ Queues.delete_for(next_job["jid"], queue, to_s)
279
287
  process_job(next_job)
280
288
  end
281
289
 
282
290
  def process_job(job)
283
291
  worker = new
284
- worker.jid = job['jid']
285
- worker.bid = job['bid'] if worker.respond_to?(:bid=)
286
- Sidekiq::Testing.server_middleware.invoke(worker, job, job['queue']) do
287
- execute_job(worker, job['args'])
292
+ worker.jid = job["jid"]
293
+ worker.bid = job["bid"] if worker.respond_to?(:bid=)
294
+ Sidekiq::Testing.server_middleware.invoke(worker, job, job["queue"]) do
295
+ execute_job(worker, job["args"])
288
296
  end
289
297
  end
290
298
 
@@ -309,7 +317,7 @@ module Sidekiq
309
317
  worker_classes = jobs.map { |job| job["class"] }.uniq
310
318
 
311
319
  worker_classes.each do |worker_class|
312
- worker_class.constantize.drain
320
+ Sidekiq::Testing.constantize(worker_class).drain
313
321
  end
314
322
  end
315
323
  end