diff --git a/lib/sidekiq.rb b/lib/sidekiq.rb index 157ee1fc..5f05df9a 100644 --- a/lib/sidekiq.rb +++ b/lib/sidekiq.rb @@ -20,6 +20,8 @@ module Sidekiq require: '.', environment: nil, timeout: 8, + poll_interval_average: nil, + global_poll_interval_average: 15, error_handlers: [], lifecycle_events: { startup: [], @@ -127,9 +129,13 @@ module Sidekiq Sidekiq::Logging.logger = log end + # When set, overrides Sidekiq.options[:global_poll_interval_average] and sets the + # average interval that this process will delay before checking for scheduled jobs + # or job retries that are ready to run. + # # See sidekiq/scheduled.rb for an in-depth explanation of this value def self.poll_interval=(interval) - self.options[:poll_interval] = interval + self.options[:poll_interval_average] = interval end # Register a proc to handle any error which occurs within the Sidekiq process. diff --git a/lib/sidekiq/scheduled.rb b/lib/sidekiq/scheduled.rb index 3f043ea6..951e8bce 100644 --- a/lib/sidekiq/scheduled.rb +++ b/lib/sidekiq/scheduled.rb @@ -68,7 +68,7 @@ module Sidekiq # Calculates a random interval that is ±50% the desired average. def random_poll_interval - poll_interval * rand + poll_interval.to_f / 2 + poll_interval_average * rand + poll_interval_average.to_f / 2 end # We do our best to tune poll_interval to the size of the active Sidekiq @@ -84,13 +84,17 @@ module Sidekiq # the same time: the thundering herd problem. # # We only do this if poll_interval is unset (the default). - def poll_interval - Sidekiq.options[:poll_interval] ||= begin - ps = Sidekiq::ProcessSet.new - pcount = ps.size - pcount = 1 if pcount == 0 - pcount * 15 - end + def poll_interval_average + Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval + end + + # Calculates an average poll interval based on the number of known Sidekiq processes. + # This minimizes a single point of failure by dispersing check-ins but without taxing + # Redis if you run many Sidekiq processes. + def scaled_poll_interval + pcount = Sidekiq::ProcessSet.new.size + pcount = 1 if pcount == 0 + pcount * Sidekiq.options[:global_poll_interval_average] end def initial_wait diff --git a/test/test_scheduled.rb b/test/test_scheduled.rb index 2809601a..b3576bd7 100644 --- a/test/test_scheduled.rb +++ b/test/test_scheduled.rb @@ -82,5 +82,47 @@ class TestScheduled < Sidekiq::Test assert_equal 1, @scheduled.size end end + + def with_sidekiq_option(name, value) + _original, Sidekiq.options[name] = Sidekiq.options[name], value + begin + yield + ensure + Sidekiq.options[name] = _original + end + end + + it 'generates random intervals that target a configured average' do + with_sidekiq_option(:poll_interval_average, 10) do + i = 500 + intervals = i.times.map{ @poller.send(:random_poll_interval) } + + assert intervals.all?{|i| i >= 5} + assert intervals.all?{|i| i <= 15} + assert_in_delta 10, intervals.reduce(&:+).to_f / i, 0.5 + end + end + + it 'calculates an average poll interval based on the number of known Sidekiq processes' do + with_sidekiq_option(:global_poll_interval_average, 10) do + begin + 3.times do |i| + Sidekiq.redis do |conn| + conn.sadd("processes", "process-#{i}") + conn.hset("process-#{i}", "info", nil) + end + end + + assert_equal 30, @poller.send(:scaled_poll_interval) + ensure + 3.times do |i| + Sidekiq.redis do |conn| + conn.srem("processes", "process-#{i}") + conn.del("process-#{i}") + end + end + end + end + end end end