-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: Revive queues cooldown manager
Brings back feature that was removed in: e5ac585 See README.adoc for possible configuration Resolve: #160 Resolve: #157 Co-authored-by: Alexandr Elhovenko <freemanoid321@gmail.com> Signed-off-by: Alexey Zapparov <alexey@zapparov.com>
- Loading branch information
1 parent
0b42a9e
commit 736acc3
Showing
12 changed files
with
455 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# frozen_string_literal: true | ||
|
||
module Sidekiq | ||
module Throttled | ||
# Configuration object. | ||
class Config | ||
# Period in seconds to exclude queue from polling in case it returned | ||
# {#cooldown_threshold} amount of throttled jobs in a row. | ||
# | ||
# Set this to `nil` to disable cooldown completely. | ||
# | ||
# @return [Float, nil] | ||
attr_reader :cooldown_period | ||
|
||
# Amount of throttled jobs returned from the queue subsequently after | ||
# which queue will be excluded from polling for the durations of | ||
# {#cooldown_period}. | ||
# | ||
# @return [Integer] | ||
attr_reader :cooldown_threshold | ||
|
||
def initialize | ||
@cooldown_period = 2.0 | ||
@cooldown_threshold = 1 | ||
end | ||
|
||
# @!attribute [w] cooldown_period | ||
def cooldown_period=(value) | ||
raise TypeError, "unexpected type #{value.class}" unless value.nil? || value.is_a?(Float) | ||
raise ArgumentError, "period must be positive" unless value.nil? || value.positive? | ||
|
||
@cooldown_period = value | ||
end | ||
|
||
# @!attribute [w] cooldown_threshold | ||
def cooldown_threshold=(value) | ||
raise TypeError, "unexpected type #{value.class}" unless value.is_a?(Integer) | ||
raise ArgumentError, "threshold must be positive" unless value.positive? | ||
|
||
@cooldown_threshold = value | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# frozen_string_literal: true | ||
|
||
require "concurrent" | ||
|
||
require_relative "./expirable_set" | ||
|
||
module Sidekiq | ||
module Throttled | ||
# @api internal | ||
# | ||
# Queues cooldown manager. Tracks list of queues that should be temporarily | ||
# (for the duration of {Config#cooldown_period}) excluded from polling. | ||
class Cooldown | ||
class << self | ||
# Returns new {Cooldown} instance if {Config#cooldown_period} is not `nil`. | ||
# | ||
# @param config [Config] | ||
# @return [Cooldown, nil] | ||
def [](config) | ||
new(config) if config.cooldown_period | ||
end | ||
end | ||
|
||
# @param config [Config] | ||
def initialize(config) | ||
@queues = ExpirableSet.new(config.cooldown_period) | ||
@threshold = config.cooldown_threshold | ||
@tracker = Concurrent::Map.new | ||
end | ||
|
||
# Notify that given queue returned job that was throttled. | ||
# | ||
# @param queue [String] | ||
# @return [void] | ||
def notify_throttled(queue) | ||
@queues.add(queue) if @threshold <= @tracker.merge_pair(queue, 1, &:succ) | ||
end | ||
|
||
# Notify that given queue returned job that was not throttled. | ||
# | ||
# @param queue [String] | ||
# @return [void] | ||
def notify_admitted(queue) | ||
@tracker.delete(queue) | ||
end | ||
|
||
# List of queues that should not be polled | ||
# | ||
# @return [Array<String>] | ||
def queues | ||
@queues.to_a | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# frozen_string_literal: true | ||
|
||
require "concurrent" | ||
|
||
module Sidekiq | ||
module Throttled | ||
# @api internal | ||
# | ||
# Set of elements with expirations. | ||
# | ||
# @example | ||
# set = ExpirableSet.new(10.0) | ||
# set.add("a") | ||
# sleep(5) | ||
# set.add("b") | ||
# set.to_a # => ["a", "b"] | ||
# sleep(5) | ||
# set.to_a # => ["b"] | ||
class ExpirableSet | ||
include Enumerable | ||
|
||
# @param ttl [Float] expiration is seconds | ||
# @raise [ArgumentError] if `ttl` is not positive Float | ||
def initialize(ttl) | ||
raise ArgumentError, "ttl must be positive Float" unless ttl.is_a?(Float) && ttl.positive? | ||
|
||
@elements = Concurrent::Map.new | ||
@ttl = ttl | ||
end | ||
|
||
# @param element [Object] | ||
# @return [ExpirableSet] self | ||
def add(element) | ||
# cleanup expired elements to avoid mem-leak | ||
horizon = now | ||
expired = @elements.each_pair.select { |(_, sunset)| expired?(sunset, horizon) } | ||
expired.each { |pair| @elements.delete_pair(*pair) } | ||
|
||
# add new element | ||
@elements[element] = now + @ttl | ||
|
||
self | ||
end | ||
|
||
# @yield [Object] Gives each live (not expired) element to the block | ||
def each | ||
return to_enum __method__ unless block_given? | ||
|
||
horizon = now | ||
|
||
@elements.each_pair do |element, sunset| | ||
yield element unless expired?(sunset, horizon) | ||
end | ||
|
||
self | ||
end | ||
|
||
private | ||
|
||
# @return [Float] | ||
def now | ||
::Process.clock_gettime(::Process::CLOCK_MONOTONIC) | ||
end | ||
|
||
def expired?(sunset, horizon) | ||
sunset <= horizon | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# frozen_string_literal: true | ||
|
||
require "sidekiq/throttled/config" | ||
|
||
RSpec.describe Sidekiq::Throttled::Config do | ||
subject(:config) { described_class.new } | ||
|
||
describe "#cooldown_period" do | ||
subject { config.cooldown_period } | ||
|
||
it { is_expected.to eq 2.0 } | ||
end | ||
|
||
describe "#cooldown_period=" do | ||
it "updates #cooldown_period" do | ||
expect { config.cooldown_period = 42.0 } | ||
.to change { config.cooldown_period }.to(42.0) | ||
end | ||
|
||
it "allows setting value to `nil`" do | ||
expect { config.cooldown_period = nil } | ||
.to change { config.cooldown_period }.to(nil) | ||
end | ||
|
||
it "fails if given value is neither `NilClass` nor `Float`" do | ||
expect { config.cooldown_period = 42 } | ||
.to raise_error(TypeError, %r{unexpected type}) | ||
end | ||
|
||
it "fails if given value is not positive" do | ||
expect { config.cooldown_period = 0.0 } | ||
.to raise_error(ArgumentError, %r{must be positive}) | ||
end | ||
end | ||
|
||
describe "#cooldown_threshold" do | ||
subject { config.cooldown_threshold } | ||
|
||
it { is_expected.to eq 1 } | ||
end | ||
|
||
describe "#cooldown_threshold=" do | ||
it "updates #cooldown_threshold" do | ||
expect { config.cooldown_threshold = 42 } | ||
.to change { config.cooldown_threshold }.to(42) | ||
end | ||
|
||
it "fails if given value is not `Integer`" do | ||
expect { config.cooldown_threshold = 42.0 } | ||
.to raise_error(TypeError, %r{unexpected type}) | ||
end | ||
|
||
it "fails if given value is not positive" do | ||
expect { config.cooldown_threshold = 0 } | ||
.to raise_error(ArgumentError, %r{must be positive}) | ||
end | ||
end | ||
end |
Oops, something went wrong.