sidekiq 6.3.1 → 7.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +140 -4
  3. data/LICENSE.txt +9 -0
  4. data/README.md +19 -13
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +71 -76
  7. data/bin/sidekiqmon +1 -1
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +267 -186
  13. data/lib/sidekiq/capsule.rb +110 -0
  14. data/lib/sidekiq/cli.rb +82 -78
  15. data/lib/sidekiq/client.rb +73 -80
  16. data/lib/sidekiq/{util.rb → component.rb} +13 -42
  17. data/lib/sidekiq/config.rb +271 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +22 -21
  21. data/lib/sidekiq/job.rb +375 -10
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +79 -56
  24. data/lib/sidekiq/job_util.rb +71 -0
  25. data/lib/sidekiq/launcher.rb +76 -82
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +134 -0
  31. data/lib/sidekiq/middleware/chain.rb +84 -42
  32. data/lib/sidekiq/middleware/current_attributes.rb +19 -13
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +1 -1
  36. data/lib/sidekiq/paginator.rb +16 -8
  37. data/lib/sidekiq/processor.rb +56 -59
  38. data/lib/sidekiq/rails.rb +10 -9
  39. data/lib/sidekiq/redis_client_adapter.rb +118 -0
  40. data/lib/sidekiq/redis_connection.rb +13 -82
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +75 -37
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +41 -68
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +27 -8
  49. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  50. data/lib/sidekiq/web/helpers.rb +22 -20
  51. data/lib/sidekiq/web.rb +6 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +85 -202
  54. data/sidekiq.gemspec +29 -5
  55. data/web/assets/javascripts/application.js +58 -26
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +166 -0
  60. data/web/assets/javascripts/dashboard.js +3 -240
  61. data/web/assets/javascripts/metrics.js +236 -0
  62. data/web/assets/stylesheets/application-dark.css +13 -17
  63. data/web/assets/stylesheets/application-rtl.css +2 -91
  64. data/web/assets/stylesheets/application.css +67 -300
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +52 -52
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +82 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +67 -67
  74. data/web/locales/he.yml +65 -64
  75. data/web/locales/hi.yml +59 -59
  76. data/web/locales/it.yml +53 -53
  77. data/web/locales/ja.yml +71 -68
  78. data/web/locales/ko.yml +52 -52
  79. data/web/locales/lt.yml +66 -66
  80. data/web/locales/nb.yml +61 -61
  81. data/web/locales/nl.yml +52 -52
  82. data/web/locales/pl.yml +45 -45
  83. data/web/locales/pt-br.yml +63 -55
  84. data/web/locales/pt.yml +51 -51
  85. data/web/locales/ru.yml +67 -66
  86. data/web/locales/sv.yml +53 -53
  87. data/web/locales/ta.yml +60 -60
  88. data/web/locales/uk.yml +62 -61
  89. data/web/locales/ur.yml +64 -64
  90. data/web/locales/vi.yml +67 -67
  91. data/web/locales/zh-cn.yml +37 -11
  92. data/web/locales/zh-tw.yml +42 -8
  93. data/web/views/_footer.erb +5 -2
  94. data/web/views/_nav.erb +1 -1
  95. data/web/views/_summary.erb +1 -1
  96. data/web/views/busy.erb +9 -4
  97. data/web/views/dashboard.erb +36 -4
  98. data/web/views/metrics.erb +80 -0
  99. data/web/views/metrics_for_job.erb +69 -0
  100. data/web/views/queue.erb +5 -1
  101. metadata +75 -27
  102. data/LICENSE +0 -9
  103. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  104. data/lib/sidekiq/delay.rb +0 -41
  105. data/lib/sidekiq/exception_handler.rb +0 -27
  106. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  107. data/lib/sidekiq/extensions/active_record.rb +0 -43
  108. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  109. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  110. data/lib/sidekiq/worker.rb +0 -311
data/lib/sidekiq/rails.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/worker"
3
+ require "sidekiq/job"
4
+ require "rails"
4
5
 
5
6
  module Sidekiq
6
7
  class Rails < ::Rails::Engine
@@ -22,7 +23,7 @@ module Sidekiq
22
23
 
23
24
  # By including the Options module, we allow AJs to directly control sidekiq features
24
25
  # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
25
- # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
26
+ # AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
26
27
  # manually retried, don't automatically die, etc.
27
28
  #
28
29
  # class SomeJob < ActiveJob::Base
@@ -33,17 +34,17 @@ module Sidekiq
33
34
  # end
34
35
  initializer "sidekiq.active_job_integration" do
35
36
  ActiveSupport.on_load(:active_job) do
36
- include ::Sidekiq::Worker::Options unless respond_to?(:sidekiq_options)
37
+ include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
37
38
  end
38
39
  end
39
40
 
40
41
  initializer "sidekiq.rails_logger" do
41
- Sidekiq.configure_server do |_|
42
- # This is the integration code necessary so that if code uses `Rails.logger.info "Hello"`,
42
+ Sidekiq.configure_server do |config|
43
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
43
44
  # it will appear in the Sidekiq console with all of the job context. See #5021 and
44
45
  # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
45
- unless ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(::Sidekiq.logger))
46
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
47
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
47
48
  end
48
49
  end
49
50
  end
@@ -53,8 +54,8 @@ module Sidekiq
53
54
  #
54
55
  # None of this matters on the client-side, only within the Sidekiq process itself.
55
56
  config.after_initialize do
56
- Sidekiq.configure_server do |_|
57
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
57
+ Sidekiq.configure_server do |config|
58
+ config[:reloader] = Sidekiq::Rails::Reloader.new
58
59
  end
59
60
  end
60
61
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_client"
4
+ require "redis_client/decorator"
5
+
6
+ module Sidekiq
7
+ class RedisClientAdapter
8
+ BaseError = RedisClient::Error
9
+ CommandError = RedisClient::CommandError
10
+
11
+ module CompatMethods
12
+ def info
13
+ @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
14
+ end
15
+
16
+ def evalsha(sha, keys, argv)
17
+ @client.call("EVALSHA", sha, keys.size, *keys, *argv)
18
+ end
19
+
20
+ private
21
+
22
+ # this allows us to use methods like `conn.hmset(...)` instead of having to use
23
+ # redis-client's native `conn.call("hmset", ...)`
24
+ def method_missing(*args, &block)
25
+ @client.call(*args, *block)
26
+ end
27
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
28
+
29
+ def respond_to_missing?(name, include_private = false)
30
+ super # Appease the linter. We can't tell what is a valid command.
31
+ end
32
+ end
33
+
34
+ CompatClient = RedisClient::Decorator.create(CompatMethods)
35
+
36
+ class CompatClient
37
+ # underscore methods are not official API
38
+ def _client
39
+ @client
40
+ end
41
+
42
+ def _config
43
+ @client.config
44
+ end
45
+
46
+ def message
47
+ yield nil, @queue.pop
48
+ end
49
+
50
+ # NB: this method does not return
51
+ def subscribe(chan)
52
+ @queue = ::Queue.new
53
+
54
+ pubsub = @client.pubsub
55
+ pubsub.call("subscribe", chan)
56
+
57
+ loop do
58
+ evt = pubsub.next_event
59
+ next if evt.nil?
60
+ next unless evt[0] == "message" && evt[1] == chan
61
+
62
+ (_, _, msg) = evt
63
+ @queue << msg
64
+ yield self
65
+ end
66
+ end
67
+ end
68
+
69
+ def initialize(options)
70
+ opts = client_opts(options)
71
+ @config = if opts.key?(:sentinels)
72
+ RedisClient.sentinel(**opts)
73
+ else
74
+ RedisClient.config(**opts)
75
+ end
76
+ end
77
+
78
+ def new_client
79
+ CompatClient.new(@config.new_client)
80
+ end
81
+
82
+ private
83
+
84
+ def client_opts(options)
85
+ opts = options.dup
86
+
87
+ if opts[:namespace]
88
+ raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature isn't supported by redis-client. " \
89
+ "Either use the redis adapter or remove the namespace."
90
+ end
91
+
92
+ opts.delete(:size)
93
+ opts.delete(:pool_timeout)
94
+
95
+ if opts[:network_timeout]
96
+ opts[:timeout] = opts[:network_timeout]
97
+ opts.delete(:network_timeout)
98
+ end
99
+
100
+ if opts[:driver]
101
+ opts[:driver] = opts[:driver].to_sym
102
+ end
103
+
104
+ opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
105
+ opts[:role] = opts[:role].to_sym if opts.key?(:role)
106
+ opts.delete(:url) if opts.key?(:sentinels)
107
+
108
+ # Issue #3303, redis-rb will silently retry an operation.
109
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
110
+ # is performed twice but I believe this is much, much rarer
111
+ # than the reconnect silently fixing a problem; we keep it
112
+ # on by default.
113
+ opts[:reconnect_attempts] ||= 1
114
+
115
+ opts
116
+ end
117
+ end
118
+ end
@@ -1,97 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "connection_pool"
4
- require "redis"
5
4
  require "uri"
5
+ require "sidekiq/redis_client_adapter"
6
6
 
7
7
  module Sidekiq
8
- class RedisConnection
8
+ module RedisConnection
9
9
  class << self
10
10
  def create(options = {})
11
11
  symbolized_options = options.transform_keys(&:to_sym)
12
+ symbolized_options[:url] ||= determine_redis_provider
12
13
 
13
- if !symbolized_options[:url] && (u = determine_redis_provider)
14
- symbolized_options[:url] = u
15
- end
16
-
17
- size = if symbolized_options[:size]
18
- symbolized_options[:size]
19
- elsif Sidekiq.server?
20
- # Give ourselves plenty of connections. pool is lazy
21
- # so we won't create them until we need them.
22
- Sidekiq.options[:concurrency] + 5
23
- elsif ENV["RAILS_MAX_THREADS"]
24
- Integer(ENV["RAILS_MAX_THREADS"])
25
- else
26
- 5
27
- end
14
+ logger = symbolized_options.delete(:logger)
15
+ logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
28
16
 
29
- verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
17
+ size = symbolized_options.delete(:size) || 5
18
+ pool_timeout = symbolized_options.delete(:pool_timeout) || 1
19
+ pool_name = symbolized_options.delete(:pool_name)
30
20
 
31
- pool_timeout = symbolized_options[:pool_timeout] || 1
32
- log_info(symbolized_options)
33
-
34
- ConnectionPool.new(timeout: pool_timeout, size: size) do
35
- build_client(symbolized_options)
21
+ redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
22
+ ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
23
+ redis_config.new_client
36
24
  end
37
25
  end
38
26
 
39
27
  private
40
28
 
41
- # Sidekiq needs a lot of concurrent Redis connections.
42
- #
43
- # We need a connection for each Processor.
44
- # We need a connection for Pro's real-time change listener
45
- # We need a connection to various features to call Redis every few seconds:
46
- # - the process heartbeat.
47
- # - enterprise's leader election
48
- # - enterprise's cron support
49
- def verify_sizing(size, concurrency)
50
- 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)
51
- end
52
-
53
- def build_client(options)
54
- namespace = options[:namespace]
55
-
56
- client = Redis.new client_opts(options)
57
- if namespace
58
- begin
59
- require "redis/namespace"
60
- Redis::Namespace.new(namespace, redis: client)
61
- rescue LoadError
62
- Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
63
- "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
64
- exit(-127)
65
- end
66
- else
67
- client
68
- end
69
- end
70
-
71
- def client_opts(options)
72
- opts = options.dup
73
- if opts[:namespace]
74
- opts.delete(:namespace)
75
- end
76
-
77
- if opts[:network_timeout]
78
- opts[:timeout] = opts[:network_timeout]
79
- opts.delete(:network_timeout)
80
- end
81
-
82
- opts[:driver] ||= Redis::Connection.drivers.last || "ruby"
83
-
84
- # Issue #3303, redis-rb will silently retry an operation.
85
- # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
86
- # is performed twice but I believe this is much, much rarer
87
- # than the reconnect silently fixing a problem; we keep it
88
- # on by default.
89
- opts[:reconnect_attempts] ||= 1
90
-
91
- opts
92
- end
93
-
94
- def log_info(options)
29
+ def scrub(options)
95
30
  redacted = "REDACTED"
96
31
 
97
32
  # Deep clone so we can muck with these options all we want and exclude
@@ -109,11 +44,7 @@ module Sidekiq
109
44
  scrubbed_options[:sentinels]&.each do |sentinel|
110
45
  sentinel[:password] = redacted if sentinel[:password]
111
46
  end
112
- if Sidekiq.server?
113
- Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
114
- else
115
- Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
116
- end
47
+ scrubbed_options
117
48
  end
118
49
 
119
50
  def determine_redis_provider
@@ -0,0 +1,29 @@
1
+ require "forwardable"
2
+
3
+ module Sidekiq
4
+ class RingBuffer
5
+ include Enumerable
6
+ extend Forwardable
7
+ def_delegators :@buf, :[], :each, :size
8
+
9
+ def initialize(size, default = 0)
10
+ @size = size
11
+ @buf = Array.new(size, default)
12
+ @index = 0
13
+ end
14
+
15
+ def <<(element)
16
+ @buf[@index % @size] = element
17
+ @index += 1
18
+ element
19
+ end
20
+
21
+ def buffer
22
+ @buf
23
+ end
24
+
25
+ def reset(default = 0)
26
+ @buf.fill(default)
27
+ end
28
+ end
29
+ end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq"
4
- require "sidekiq/util"
5
- require "sidekiq/api"
4
+ require "sidekiq/component"
6
5
 
7
6
  module Sidekiq
8
7
  module Scheduled
9
8
  SETS = %w[retry schedule]
10
9
 
11
10
  class Enq
11
+ include Sidekiq::Component
12
+
12
13
  LUA_ZPOPBYSCORE = <<~LUA
13
14
  local key, now = KEYS[1], ARGV[1]
14
15
  local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1)
@@ -18,34 +19,43 @@ module Sidekiq
18
19
  end
19
20
  LUA
20
21
 
21
- def initialize
22
+ def initialize(container)
23
+ @config = container
24
+ @client = Sidekiq::Client.new(config: container)
25
+ @done = false
22
26
  @lua_zpopbyscore_sha = nil
23
27
  end
24
28
 
25
- def enqueue_jobs(now = Time.now.to_f.to_s, sorted_sets = SETS)
29
+ def enqueue_jobs(sorted_sets = SETS)
26
30
  # A job's "score" in Redis is the time at which it should be processed.
27
31
  # Just check Redis for the set of jobs with a timestamp before now.
28
- Sidekiq.redis do |conn|
32
+ redis do |conn|
29
33
  sorted_sets.each do |sorted_set|
30
34
  # Get next item in the queue with score (time to execute) <= now.
31
35
  # We need to go through the list one at a time to reduce the risk of something
32
36
  # going wrong between the time jobs are popped from the scheduled queue and when
33
37
  # they are pushed onto a work queue and losing the jobs.
34
- while (job = zpopbyscore(conn, keys: [sorted_set], argv: [now]))
35
- Sidekiq::Client.push(Sidekiq.load_json(job))
36
- Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
38
+ while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s]))
39
+ @client.push(Sidekiq.load_json(job))
40
+ logger.debug { "enqueued #{sorted_set}: #{job}" }
37
41
  end
38
42
  end
39
43
  end
40
44
  end
41
45
 
46
+ def terminate
47
+ @done = true
48
+ end
49
+
42
50
  private
43
51
 
44
52
  def zpopbyscore(conn, keys: nil, argv: nil)
45
- @lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE) if @lua_zpopbyscore_sha.nil?
53
+ if @lua_zpopbyscore_sha.nil?
54
+ @lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
55
+ end
46
56
 
47
- conn.evalsha(@lua_zpopbyscore_sha, keys: keys, argv: argv)
48
- rescue Redis::CommandError => e
57
+ conn.evalsha(@lua_zpopbyscore_sha, keys, argv)
58
+ rescue RedisClient::CommandError => e
49
59
  raise unless e.message.start_with?("NOSCRIPT")
50
60
 
51
61
  @lua_zpopbyscore_sha = nil
@@ -59,12 +69,13 @@ module Sidekiq
59
69
  # just pops the job back onto its original queue so the
60
70
  # workers can pick it up like any other job.
61
71
  class Poller
62
- include Util
72
+ include Sidekiq::Component
63
73
 
64
74
  INITIAL_WAIT = 10
65
75
 
66
- def initialize
67
- @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
76
+ def initialize(config)
77
+ @config = config
78
+ @enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
68
79
  @sleeper = ConnectionPool::TimedStack.new
69
80
  @done = false
70
81
  @thread = nil
@@ -74,12 +85,10 @@ module Sidekiq
74
85
  # Shut down this instance, will pause until the thread is dead.
75
86
  def terminate
76
87
  @done = true
77
- if @thread
78
- t = @thread
79
- @thread = nil
80
- @sleeper << 0
81
- t.value
82
- end
88
+ @enq.terminate
89
+
90
+ @sleeper << 0
91
+ @thread&.value
83
92
  end
84
93
 
85
94
  def start
@@ -90,7 +99,7 @@ module Sidekiq
90
99
  enqueue
91
100
  wait
92
101
  end
93
- Sidekiq.logger.info("Scheduler exiting...")
102
+ logger.info("Scheduler exiting...")
94
103
  }
95
104
  end
96
105
 
@@ -137,13 +146,16 @@ module Sidekiq
137
146
  # As we run more processes, the scheduling interval average will approach an even spread
138
147
  # between 0 and poll interval so we don't need this artifical boost.
139
148
  #
140
- if process_count < 10
149
+ count = process_count
150
+ interval = poll_interval_average(count)
151
+
152
+ if count < 10
141
153
  # For small clusters, calculate a random interval that is ±50% the desired average.
142
- poll_interval_average * rand + poll_interval_average.to_f / 2
154
+ interval * rand + interval.to_f / 2
143
155
  else
144
156
  # With 10+ processes, we should have enough randomness to get decent polling
145
157
  # across the entire timespan
146
- poll_interval_average * rand
158
+ interval * rand
147
159
  end
148
160
  end
149
161
 
@@ -160,38 +172,64 @@ module Sidekiq
160
172
  # the same time: the thundering herd problem.
161
173
  #
162
174
  # We only do this if poll_interval_average is unset (the default).
163
- def poll_interval_average
164
- Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
175
+ def poll_interval_average(count)
176
+ @config[:poll_interval_average] || scaled_poll_interval(count)
165
177
  end
166
178
 
167
179
  # Calculates an average poll interval based on the number of known Sidekiq processes.
168
180
  # This minimizes a single point of failure by dispersing check-ins but without taxing
169
181
  # Redis if you run many Sidekiq processes.
170
- def scaled_poll_interval
171
- process_count * Sidekiq.options[:average_scheduled_poll_interval]
182
+ def scaled_poll_interval(process_count)
183
+ process_count * @config[:average_scheduled_poll_interval]
172
184
  end
173
185
 
174
186
  def process_count
175
- # The work buried within Sidekiq::ProcessSet#cleanup can be
176
- # expensive at scale. Cut it down by 90% with this counter.
177
- # NB: This method is only called by the scheduler thread so we
178
- # don't need to worry about the thread safety of +=.
179
- pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
187
+ pcount = Sidekiq.redis { |conn| conn.scard("processes") }
180
188
  pcount = 1 if pcount == 0
181
- @count_calls += 1
182
189
  pcount
183
190
  end
184
191
 
192
+ # A copy of Sidekiq::ProcessSet#cleanup because server
193
+ # should never depend on sidekiq/api.
194
+ def cleanup
195
+ # dont run cleanup more than once per minute
196
+ return 0 unless redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
197
+
198
+ count = 0
199
+ redis do |conn|
200
+ procs = conn.sscan("processes").to_a
201
+ heartbeats = conn.pipelined { |pipeline|
202
+ procs.each do |key|
203
+ pipeline.hget(key, "info")
204
+ end
205
+ }
206
+
207
+ # the hash named key has an expiry of 60 seconds.
208
+ # if it's not found, that means the process has not reported
209
+ # in to Redis and probably died.
210
+ to_prune = procs.select.with_index { |proc, i|
211
+ heartbeats[i].nil?
212
+ }
213
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
214
+ end
215
+ count
216
+ end
217
+
185
218
  def initial_wait
186
- # Have all processes sleep between 5-15 seconds. 10 seconds
187
- # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
219
+ # Have all processes sleep between 5-15 seconds. 10 seconds to give time for
220
+ # the heartbeat to register (if the poll interval is going to be calculated by the number
188
221
  # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
189
222
  total = 0
190
- total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
223
+ total += INITIAL_WAIT unless @config[:poll_interval_average]
191
224
  total += (5 * rand)
192
225
 
193
226
  @sleeper.pop(total)
194
227
  rescue Timeout::Error
228
+ ensure
229
+ # periodically clean out the `processes` set in Redis which can collect
230
+ # references to dead processes over time. The process count affects how
231
+ # often we scan for scheduled jobs.
232
+ cleanup
195
233
  end
196
234
  end
197
235
  end
@@ -4,7 +4,7 @@ require "sidekiq/testing"
4
4
 
5
5
  ##
6
6
  # The Sidekiq inline infrastructure overrides perform_async so that it
7
- # actually calls perform instead. This allows workers to be run inline in a
7
+ # actually calls perform instead. This allows jobs to be run inline in a
8
8
  # testing environment.
9
9
  #
10
10
  # This is similar to `Resque.inline = true` functionality.
@@ -15,8 +15,8 @@ require "sidekiq/testing"
15
15
  #
16
16
  # $external_variable = 0
17
17
  #
18
- # class ExternalWorker
19
- # include Sidekiq::Worker
18
+ # class ExternalJob
19
+ # include Sidekiq::Job
20
20
  #
21
21
  # def perform
22
22
  # $external_variable = 1
@@ -24,7 +24,7 @@ require "sidekiq/testing"
24
24
  # end
25
25
  #
26
26
  # assert_equal 0, $external_variable
27
- # ExternalWorker.perform_async
27
+ # ExternalJob.perform_async
28
28
  # assert_equal 1, $external_variable
29
29
  #
30
30
  Sidekiq::Testing.inline!