sidekiq-throttled 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +5 -0
- data/.rubocop.yml +72 -0
- data/.travis.yml +4 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.md +21 -0
- data/README.md +74 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/sidekiq/throttled.rb +72 -0
- data/lib/sidekiq/throttled/basic_fetch.rb +63 -0
- data/lib/sidekiq/throttled/errors.rb +6 -0
- data/lib/sidekiq/throttled/middleware.rb +19 -0
- data/lib/sidekiq/throttled/registry.rb +73 -0
- data/lib/sidekiq/throttled/strategy.rb +60 -0
- data/lib/sidekiq/throttled/strategy/concurrency.lua +3 -0
- data/lib/sidekiq/throttled/strategy/concurrency.rb +59 -0
- data/lib/sidekiq/throttled/strategy/script.rb +45 -0
- data/lib/sidekiq/throttled/strategy/threshold.lua +3 -0
- data/lib/sidekiq/throttled/strategy/threshold.rb +68 -0
- data/lib/sidekiq/throttled/testing.rb +10 -0
- data/lib/sidekiq/throttled/version.rb +6 -0
- data/lib/sidekiq/throttled/web.rb +33 -0
- data/lib/sidekiq/throttled/web/index.html.erb +38 -0
- data/lib/sidekiq/throttled/web/stats.rb +72 -0
- data/lib/sidekiq/throttled/worker.rb +126 -0
- data/sidekiq-throttled.gemspec +35 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 76a708f284e2c4b3c3a65477f8893420854aeca4
|
4
|
+
data.tar.gz: 8c5c83a1d64063af01170067819bf167fe710bfe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e703df521b67bccb01d0e022dd83376ac6d9cd68d57e7603f36212b7e8969a0128e5119df0e6224c9da813308a743e8ffb1302f44142adfc82b43b163c82978
|
7
|
+
data.tar.gz: b0b142ad1f692d962c7ffa63027c309350d5c0258a05d5b9cb1adcc2eb1a491244d0d9dd1191ef1348ed2930f020a1f6a66bc902ff6ab4bd2814c0b98a30233e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
## Styles ######################################################################
|
2
|
+
|
3
|
+
Style/AlignParameters:
|
4
|
+
EnforcedStyle: with_fixed_indentation
|
5
|
+
|
6
|
+
Style/BracesAroundHashParameters:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
# Broken (2014-12-15). Use `yardstick` gem instead.
|
10
|
+
# See: https://github.com/bbatsov/rubocop/issues/947
|
11
|
+
# TODO: Enable back once cop is fixed.
|
12
|
+
Style/Documentation:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/EmptyLineBetweenDefs:
|
16
|
+
AllowAdjacentOneLineDefs: true
|
17
|
+
|
18
|
+
Style/Encoding:
|
19
|
+
EnforcedStyle: when_needed
|
20
|
+
|
21
|
+
Style/HashSyntax:
|
22
|
+
EnforcedStyle: hash_rockets
|
23
|
+
|
24
|
+
Style/IndentHash:
|
25
|
+
EnforcedStyle: consistent
|
26
|
+
|
27
|
+
# New lambda syntax is as ugly to me as new syntax of Hash.
|
28
|
+
Style/Lambda:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Style/MultilineOperationIndentation:
|
32
|
+
EnforcedStyle: indented
|
33
|
+
|
34
|
+
# IMHO `%r{foo/bar}` looks way more cleaner than `/foo\/bar/`.
|
35
|
+
# Enabling this cop also makes Guardfile (which is full of pathname regexps)
|
36
|
+
# look absolutley (style) inconsistent and terrible. Thus it should be on
|
37
|
+
# developer's choice whenever to user `%r` or not. Just like we don't enforce
|
38
|
+
# to use `["foo"]` over `%w(foo)` and so on.
|
39
|
+
Style/RegexpLiteral:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
# A bit useless restriction, that makes impossible aligning code like this:
|
43
|
+
#
|
44
|
+
# redis do |conn|
|
45
|
+
# conn.hset :k1, now
|
46
|
+
# conn.hincrby :k2, 123
|
47
|
+
# end
|
48
|
+
Style/SingleSpaceBeforeFirstArg:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Style/StringLiterals:
|
52
|
+
EnforcedStyle: double_quotes
|
53
|
+
|
54
|
+
# Not all trivial readers/writers can be defined with attr_* methods
|
55
|
+
#
|
56
|
+
# class Example < SimpleDelegator
|
57
|
+
# def __getobj__
|
58
|
+
# @obj
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# def __setobj__(obj)
|
62
|
+
# @obj = obj
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
Style/TrivialAccessors:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
## Metrics #####################################################################
|
69
|
+
|
70
|
+
Metrics/MethodLength:
|
71
|
+
CountComments: false
|
72
|
+
Max: 15
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private - LICENSE.md
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 SensorTower Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Sidekiq::Throttled
|
2
|
+
|
3
|
+
Concurrency and threshold throttling for Sidekiq.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "sidekiq-throttled"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
```
|
17
|
+
$ bundle
|
18
|
+
```
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
```
|
23
|
+
$ gem install sidekiq-throttled
|
24
|
+
```
|
25
|
+
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Add somewhere in your app's bootstrap (e.g. `config/initializers/sidekiq.rb` if
|
30
|
+
you are using Rails):
|
31
|
+
|
32
|
+
``` ruby
|
33
|
+
require "sidekiq/throttled"
|
34
|
+
Sidekiq::Throttled.setup!
|
35
|
+
```
|
36
|
+
|
37
|
+
Once you've done that you can include `Sidekiq::Throttled::Worker` to your
|
38
|
+
job classes and configure throttling:
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
class MyWorker
|
42
|
+
include Sidekiq::Worker
|
43
|
+
include Sidekiq::Throttled::Worker
|
44
|
+
|
45
|
+
sidekiq_options :queue => :my_queue
|
46
|
+
|
47
|
+
sidekiq_throttle({
|
48
|
+
# Allow maximum 10 concurrent jobs of this class at a time.
|
49
|
+
:concurrency => { :limit => 10 },
|
50
|
+
# Allow maximum 1K jobs being processed within one hour window.
|
51
|
+
:threshold => { :limit => 1_000, :period => 1.hour }
|
52
|
+
})
|
53
|
+
|
54
|
+
def perform
|
55
|
+
# ...
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
* Fork sidekiq-throttled on GitHub
|
64
|
+
* Make your changes
|
65
|
+
* Ensure all tests pass (`bundle exec rake`)
|
66
|
+
* Send a pull request
|
67
|
+
* If we like them we'll merge them
|
68
|
+
* If we've accepted a patch, feel free to ask for commit access!
|
69
|
+
|
70
|
+
|
71
|
+
## Copyright
|
72
|
+
|
73
|
+
Copyright (c) 2015 SensorTower Inc.
|
74
|
+
See LICENSE.md for further details.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sidekiq/throttled"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# 3rd party
|
2
|
+
require "sidekiq"
|
3
|
+
|
4
|
+
# internal
|
5
|
+
require "sidekiq/version"
|
6
|
+
require "sidekiq/throttled/registry"
|
7
|
+
require "sidekiq/throttled/worker"
|
8
|
+
|
9
|
+
# @see https://github.com/mperham/sidekiq/
|
10
|
+
module Sidekiq
|
11
|
+
# Concurrency and threshold throttling for Sidekiq.
|
12
|
+
#
|
13
|
+
# Just add somewhere in your bootstrap:
|
14
|
+
#
|
15
|
+
# require "sidekiq/throttled"
|
16
|
+
# Sidekiq::Throttled.setup!
|
17
|
+
#
|
18
|
+
# Once you've done that you can include {Sidekiq::Throttled::Worker} to your
|
19
|
+
# job classes and configure throttling:
|
20
|
+
#
|
21
|
+
# class MyWorker
|
22
|
+
# include Sidekiq::Worker
|
23
|
+
# include Sidekiq::Throttled::Worker
|
24
|
+
#
|
25
|
+
# sidekiq_options :queue => :my_queue
|
26
|
+
#
|
27
|
+
# sidekiq_throttle({
|
28
|
+
# # Allow maximum 10 concurrent jobs of this class at a time.
|
29
|
+
# :concurrency => { :limit => 10 },
|
30
|
+
# # Allow maximum 1K jobs being processed within one hour window.
|
31
|
+
# :threshold => { :limit => 1_000, :period => 1.hour }
|
32
|
+
# })
|
33
|
+
#
|
34
|
+
# def perform
|
35
|
+
# # ...
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
module Throttled
|
39
|
+
class << self
|
40
|
+
# Hooks throttler into sidekiq.
|
41
|
+
# @return [void]
|
42
|
+
def setup!
|
43
|
+
Sidekiq.configure_server do |config|
|
44
|
+
require "sidekiq/throttled/basic_fetch"
|
45
|
+
Sidekiq.options[:fetch] = Sidekiq::Throttled::BasicFetch
|
46
|
+
|
47
|
+
require "sidekiq/throttled/middleware"
|
48
|
+
config.server_middleware do |chain|
|
49
|
+
chain.add Sidekiq::Throttled::Middleware
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param [String] message JSON payload of job
|
55
|
+
# @return [TrueClass] if job is not allowed to be processed now
|
56
|
+
# @return [FalseClass] otherwise
|
57
|
+
def throttled?(message)
|
58
|
+
message = JSON.parse message
|
59
|
+
job = message.fetch("class".freeze) { return false }
|
60
|
+
jid = message.fetch("jid".freeze) { return false }
|
61
|
+
|
62
|
+
Registry.get job do |strategy|
|
63
|
+
return strategy.throttled? jid
|
64
|
+
end
|
65
|
+
|
66
|
+
false
|
67
|
+
rescue
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
# 3rd party
|
5
|
+
require "celluloid"
|
6
|
+
require "sidekiq"
|
7
|
+
require "sidekiq/fetch"
|
8
|
+
|
9
|
+
module Sidekiq
|
10
|
+
module Throttled
|
11
|
+
# Throttled version of `Sidekiq::BasicFetch` fetcher strategy.
|
12
|
+
class BasicFetch < ::Sidekiq::BasicFetch
|
13
|
+
# Class constructor
|
14
|
+
def initialize(*args)
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@suspended = []
|
17
|
+
|
18
|
+
super(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Sidekiq::BasicFetch::UnitOfWork, nil]
|
22
|
+
def retrieve_work
|
23
|
+
work = brpop
|
24
|
+
return unless work
|
25
|
+
|
26
|
+
work = ::Sidekiq::BasicFetch::UnitOfWork.new(*work)
|
27
|
+
return work unless Throttled.throttled? work.message
|
28
|
+
|
29
|
+
queue = "queue:#{work.queue_name}"
|
30
|
+
|
31
|
+
@mutex.synchronize { @suspended << queue }
|
32
|
+
Sidekiq.redis { |conn| conn.lpush(queue, work.message) }
|
33
|
+
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Tries to pop pair of `queue` and job `message` out of sidekiq queue.
|
40
|
+
# @return [Array<String, String>, nil]
|
41
|
+
def brpop
|
42
|
+
if @strictly_ordered_queues
|
43
|
+
queues = @unique_queues.dup
|
44
|
+
else
|
45
|
+
queues = @queues.shuffle.uniq
|
46
|
+
end
|
47
|
+
|
48
|
+
@mutex.synchronize do
|
49
|
+
next if @suspended.empty?
|
50
|
+
queues -= @suspended
|
51
|
+
@suspended.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
if queues.empty?
|
55
|
+
sleep Sidekiq::Fetcher::TIMEOUT
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
Sidekiq.redis { |conn| conn.brpop(*queues, Sidekiq::Fetcher::TIMEOUT) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/registry"
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Throttled
|
6
|
+
# Server middleware that notifies strategy that job was finished.
|
7
|
+
# @private
|
8
|
+
class Middleware
|
9
|
+
# Called within Sidekiq job processing
|
10
|
+
def call(_worker, msg, _queue)
|
11
|
+
yield
|
12
|
+
ensure
|
13
|
+
Registry.get msg["class".freeze] do |strategy|
|
14
|
+
strategy.finalize! msg["jid".freeze]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/strategy"
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Throttled
|
6
|
+
# Registred strategies.
|
7
|
+
module Registry
|
8
|
+
@strategies = {}
|
9
|
+
@aliases = {}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Adds strategy to the registry.
|
13
|
+
#
|
14
|
+
# @note prints a warning to STDERR upon duplicate strategy name
|
15
|
+
# @param (see Strategy#initialize)
|
16
|
+
# @return [Strategy]
|
17
|
+
def add(name, **kwargs)
|
18
|
+
name = name.to_s
|
19
|
+
|
20
|
+
warn "Duplicate strategy name: #{name}" if @strategies[name]
|
21
|
+
|
22
|
+
@strategies[name] = Strategy.new(name, **kwargs)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Adds alias for existing strategy.
|
26
|
+
#
|
27
|
+
# @note prints a warning to STDERR upon duplicate strategy name
|
28
|
+
# @param (#to_s) new_name
|
29
|
+
# @param (#to_s) old_name
|
30
|
+
# @raise [RuntimeError] if no strategy found with `old_name`
|
31
|
+
# @return [Strategy]
|
32
|
+
def add_alias(new_name, old_name)
|
33
|
+
new_name = new_name.to_s
|
34
|
+
old_name = old_name.to_s
|
35
|
+
|
36
|
+
warn "Duplicate strategy name: #{new_name}" if @strategies[new_name]
|
37
|
+
fail "Strategy not found: #{old_name}" unless @strategies[old_name]
|
38
|
+
|
39
|
+
@aliases[new_name] = @strategies[old_name]
|
40
|
+
end
|
41
|
+
|
42
|
+
# @overload get(name)
|
43
|
+
# @param [#to_s] name
|
44
|
+
# @return [Strategy, nil] registred strategy
|
45
|
+
#
|
46
|
+
# @overload get(name, &block)
|
47
|
+
# Yields control to the block if requested strategy was found.
|
48
|
+
# @yieldparam [Strategy] strategy
|
49
|
+
# @yield [strategy] Gives found strategy to the block
|
50
|
+
# @return result of a block
|
51
|
+
def get(name)
|
52
|
+
strategy = @strategies[name.to_s] || @aliases[name.to_s]
|
53
|
+
return yield strategy if strategy && block_given?
|
54
|
+
strategy
|
55
|
+
end
|
56
|
+
|
57
|
+
# @overload each()
|
58
|
+
# @return [Enumerator]
|
59
|
+
#
|
60
|
+
# @overload each(&block)
|
61
|
+
# @yieldparam [String] name
|
62
|
+
# @yieldparam [Strategy] strategy
|
63
|
+
# @yield [strategy] Gives strategy to the block
|
64
|
+
# @return [Registry]
|
65
|
+
def each
|
66
|
+
return to_enum(__method__) unless block_given?
|
67
|
+
@strategies.each { |*args| yield(*args) }
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/errors"
|
3
|
+
require "sidekiq/throttled/strategy/concurrency"
|
4
|
+
require "sidekiq/throttled/strategy/threshold"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Throttled
|
8
|
+
# Meta-strategy that couples {Concurrency} and {Threshold} strategies.
|
9
|
+
class Strategy
|
10
|
+
# @!attribute [r] concurrency
|
11
|
+
# @return [Strategy::Concurrency, nil]
|
12
|
+
attr_reader :concurrency
|
13
|
+
|
14
|
+
# @!attribute [r] threshold
|
15
|
+
# @return [Strategy::Threshold, nil]
|
16
|
+
attr_reader :threshold
|
17
|
+
|
18
|
+
# @param [#to_s] key
|
19
|
+
# @param [Hash] concurrency Concurrency options.
|
20
|
+
# See {Strategy::Concurrency#initialize} for details.
|
21
|
+
# @param [Hash] threshold Threshold options.
|
22
|
+
# See {Strategy::Threshold#initialize} for details.
|
23
|
+
def initialize(key, concurrency: nil, threshold: nil)
|
24
|
+
base_key = "throttled:#{key}"
|
25
|
+
|
26
|
+
@concurrency = concurrency && Concurrency.new(base_key, concurrency)
|
27
|
+
@threshold = threshold && Threshold.new(base_key, threshold)
|
28
|
+
|
29
|
+
return if @concurrency || @threshold
|
30
|
+
|
31
|
+
fail ArgumentError, "Neither :concurrency nor :threshold given"
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Boolean] whenever job is throttled or not.
|
35
|
+
def throttled?(jid)
|
36
|
+
return true if @concurrency && @concurrency.throttled?(jid)
|
37
|
+
|
38
|
+
if @threshold && @threshold.throttled?
|
39
|
+
finalize! jid
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Marks job as being processed.
|
47
|
+
# @return [void]
|
48
|
+
def finalize!(jid)
|
49
|
+
@concurrency && @concurrency.finalize!(jid)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Resets count of jobs of all avaliable strategies
|
53
|
+
# @return [void]
|
54
|
+
def reset!
|
55
|
+
@concurrency && @concurrency.reset!
|
56
|
+
@threshold && @threshold.reset!
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/strategy/script"
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Throttled
|
6
|
+
class Strategy
|
7
|
+
# Concurrency throttling strategy
|
8
|
+
class Concurrency
|
9
|
+
# LUA script used to limit fetch concurrency.
|
10
|
+
# Logic behind the scene can be described in following pseudo code:
|
11
|
+
#
|
12
|
+
# return 1 if @limit <= LLEN(@key)
|
13
|
+
#
|
14
|
+
# PUSH(@key, @jid)
|
15
|
+
# return 0
|
16
|
+
SCRIPT = Script.new File.read "#{__dir__}/concurrency.lua"
|
17
|
+
private_constant :SCRIPT
|
18
|
+
|
19
|
+
# @!attribute [r] limit
|
20
|
+
# @return [Integer] Amount of allwoed concurrent job processors
|
21
|
+
attr_reader :limit
|
22
|
+
|
23
|
+
# @param [#to_s] base_key
|
24
|
+
# @param [Hash] opts
|
25
|
+
# @option opts [#to_i] :limit Amount of allwoed concurrent jobs
|
26
|
+
# processors running for given key
|
27
|
+
# @option opts [#to_i] :ttl (15.minutes) Concurrency lock TTL
|
28
|
+
def initialize(base_key, opts)
|
29
|
+
@key = "#{base_key}:concurrency".freeze
|
30
|
+
@keys = [@key]
|
31
|
+
@limit = opts.fetch(:limit).to_i
|
32
|
+
@ttl = opts.fetch(:ttl, 900).to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean] whenever job is throttled or not
|
36
|
+
def throttled?(jid)
|
37
|
+
1 == SCRIPT.eval(@keys, [@limit, @ttl, jid.to_s])
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Integer] Current count of jobs
|
41
|
+
def count
|
42
|
+
Sidekiq.redis { |conn| conn.scard(@key) }.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
# Resets count of jobs
|
46
|
+
# @return [void]
|
47
|
+
def reset!
|
48
|
+
Sidekiq.redis { |conn| conn.del(@key) }.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove jid from the pool of jobs in progress
|
52
|
+
# @return [void]
|
53
|
+
def finalize!(jid)
|
54
|
+
Sidekiq.redis { |conn| conn.srem(@key, jid.to_s) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Throttled
|
3
|
+
class Strategy
|
4
|
+
# Lua script executor for redis.
|
5
|
+
#
|
6
|
+
# Instead of executing script with `EVAL` everytime - loads script once
|
7
|
+
# and then runs it with `EVALSHA`.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
class Script
|
11
|
+
# Script load command
|
12
|
+
LOAD = "load".freeze
|
13
|
+
private_constant :LOAD
|
14
|
+
|
15
|
+
# Redis error fired when script ID is unkown
|
16
|
+
NOSCRIPT = "NOSCRIPT".freeze
|
17
|
+
private_constant :NOSCRIPT
|
18
|
+
|
19
|
+
# @param [#to_s] source Lua script
|
20
|
+
def initialize(source)
|
21
|
+
@source = source.to_s.strip.freeze
|
22
|
+
@sha = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Executes script and returns result of execution
|
26
|
+
def eval(*args)
|
27
|
+
Sidekiq.redis { |conn| conn.evalsha(@sha, *args) }
|
28
|
+
rescue => e
|
29
|
+
raise unless e.message.include? NOSCRIPT
|
30
|
+
load_and_eval(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Loads script into redis cache and executes it.
|
36
|
+
def load_and_eval(*args)
|
37
|
+
Sidekiq.redis do |conn|
|
38
|
+
@sha = conn.script(LOAD, @source)
|
39
|
+
conn.evalsha(@sha, *args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/strategy/script"
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Throttled
|
6
|
+
class Strategy
|
7
|
+
# Threshold throttling strategy
|
8
|
+
# @todo Use redis TIME command instead of sending current timestamp from
|
9
|
+
# sidekiq manager. See: http://redis.io/commands/time
|
10
|
+
class Threshold
|
11
|
+
# LUA script used to limit fetch threshold.
|
12
|
+
# Logic behind the scene can be described in following pseudo code:
|
13
|
+
#
|
14
|
+
# def exceeded?
|
15
|
+
# @limit <= LLEN(@key) && NOW - LINDEX(@key, -1) < @period
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def increase!
|
19
|
+
# LPUSH(@key, NOW)
|
20
|
+
# LTRIM(@key, 0, @limit - 1)
|
21
|
+
# EXPIRE(@key, @period)
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# return 1 if exceeded?
|
25
|
+
#
|
26
|
+
# increase!
|
27
|
+
# return 0
|
28
|
+
SCRIPT = Script.new File.read "#{__dir__}/threshold.lua"
|
29
|
+
private_constant :SCRIPT
|
30
|
+
|
31
|
+
# @!attribute [r] limit
|
32
|
+
# @return [Integer] Amount of jobs allowed per period
|
33
|
+
attr_reader :limit
|
34
|
+
|
35
|
+
# @!attribute [r] period
|
36
|
+
# @return [Float] Period in seconds
|
37
|
+
attr_reader :period
|
38
|
+
|
39
|
+
# @param [#to_s] base_key
|
40
|
+
# @param [Hash] opts
|
41
|
+
# @option opts [#to_i] :limit Amount of jobs allowed per period
|
42
|
+
# @option opts [#to_f] :period Period in seconds
|
43
|
+
def initialize(base_key, opts)
|
44
|
+
@key = "#{base_key}:threshold".freeze
|
45
|
+
@keys = [@key]
|
46
|
+
@limit = opts.fetch(:limit).to_i
|
47
|
+
@period = opts.fetch(:period).to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Boolean] whenever job is throttled or not
|
51
|
+
def throttled?
|
52
|
+
1 == SCRIPT.eval(@keys, [@limit, @period, Time.now.to_f])
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Integer] Current count of jobs
|
56
|
+
def count
|
57
|
+
Sidekiq.redis { |conn| conn.llen(@key) }.to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
# Resets count of jobs
|
61
|
+
# @return [void]
|
62
|
+
def reset!
|
63
|
+
Sidekiq.redis { |conn| conn.del(@key) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "pathname"
|
3
|
+
|
4
|
+
# 3rd party
|
5
|
+
require "sidekiq"
|
6
|
+
require "sidekiq/web"
|
7
|
+
|
8
|
+
# internal
|
9
|
+
require "sidekiq/throttled/registry"
|
10
|
+
require "sidekiq/throttled/web/stats"
|
11
|
+
|
12
|
+
module Sidekiq
|
13
|
+
module Throttled
|
14
|
+
# Provides Sidekiq tab to monitor and reset throttled stats.
|
15
|
+
# @private
|
16
|
+
module Web
|
17
|
+
class << self
|
18
|
+
def registered(app)
|
19
|
+
template = Pathname.new(__FILE__).join("../web/index.html.erb").read
|
20
|
+
app.get("/throttled") { erb template.dup }
|
21
|
+
|
22
|
+
app.delete("/throttled/:id") do
|
23
|
+
Registry.get(params[:id], &:reset!)
|
24
|
+
redirect "#{root_path}throttled"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Sidekiq::Web.register Sidekiq::Throttled::Web
|
33
|
+
Sidekiq::Web.tabs["Throttled"] = "throttled"
|
@@ -0,0 +1,38 @@
|
|
1
|
+
<div class="col-sm-12">
|
2
|
+
<div class="row header">
|
3
|
+
<div class="col-sm-8 pull-left">
|
4
|
+
<h3>Throttled</h3>
|
5
|
+
</div>
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<table class="table table-hover table-bordered table-striped table-white">
|
10
|
+
<thead>
|
11
|
+
<tr>
|
12
|
+
<th>Name</th>
|
13
|
+
<th style="text-align:center;">Concurrency</th>
|
14
|
+
<th style="text-align:center;">Threshold</th>
|
15
|
+
<th></th>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<% Sidekiq::Throttled::Registry.each do |name, strategy| %>
|
20
|
+
<tr>
|
21
|
+
<td style="vertical-align:middle;"><%= name %></td>
|
22
|
+
<td style="vertical-align:middle;text-align:center;">
|
23
|
+
<%= Sidekiq::Throttled::Web::Stats.new(strategy.concurrency).to_html %>
|
24
|
+
</td>
|
25
|
+
<td style="vertical-align:middle;text-align:center;">
|
26
|
+
<%= Sidekiq::Throttled::Web::Stats.new(strategy.threshold).to_html %>
|
27
|
+
</td>
|
28
|
+
<td style="vertical-align:middle;text-align:center;">
|
29
|
+
<form action="<%= root_path %>throttled/<%= name %>" method="POST">
|
30
|
+
<%= csrf_tag %>
|
31
|
+
<input type="hidden" name="_method" value="delete" />
|
32
|
+
<button class="btn btn-danger" type="submit">Reset</button>
|
33
|
+
</form>
|
34
|
+
</td>
|
35
|
+
</tr>
|
36
|
+
<% end %>
|
37
|
+
</tbody>
|
38
|
+
</table>
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Throttled
|
3
|
+
module Web
|
4
|
+
# Throttle strategy stats generation helper
|
5
|
+
# @private
|
6
|
+
class Stats
|
7
|
+
TIME_CONVERSION = [
|
8
|
+
[60 * 60 * 24, "day", "days"],
|
9
|
+
[60 * 60, "hour", "hours"],
|
10
|
+
[60, "minute", "minutes"],
|
11
|
+
[1, "second", "seconds"]
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
# @param [Strategy::Concurrency, Strategy::Threshold] strategy
|
15
|
+
def initialize(strategy)
|
16
|
+
@strategy = strategy
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def to_html
|
21
|
+
return "" unless @strategy
|
22
|
+
|
23
|
+
html = humanize_integer(@strategy.limit) << " jobs"
|
24
|
+
|
25
|
+
if @strategy.respond_to? :period
|
26
|
+
html << " per " << humanize_duration(@strategy.period)
|
27
|
+
end
|
28
|
+
|
29
|
+
html << "<br />" << colorize_count(@strategy.count, @strategy.limit)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def colorize_count(int, max)
|
36
|
+
percentile = 100.00 * int / max
|
37
|
+
lvl = case
|
38
|
+
when 80 <= percentile then "danger"
|
39
|
+
when 60 <= percentile then "warning"
|
40
|
+
else "success"
|
41
|
+
end
|
42
|
+
|
43
|
+
%(<span class="label label-#{lvl}">#{int}</span>)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
def humanize_duration(int)
|
48
|
+
arr = []
|
49
|
+
|
50
|
+
TIME_CONVERSION.each do |(dimension, unit, units)|
|
51
|
+
count = (int / dimension).to_i
|
52
|
+
next unless 0 < count
|
53
|
+
int -= count * dimension
|
54
|
+
arr << "#{count} #{1 == count ? unit : units}"
|
55
|
+
end
|
56
|
+
|
57
|
+
arr.join " "
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String]
|
61
|
+
def humanize_integer(int)
|
62
|
+
digits = int.to_s.split ""
|
63
|
+
str = digits.shift(digits.count % 3).join("")
|
64
|
+
|
65
|
+
str << " " << digits.shift(3).join("") while 0 < digits.count
|
66
|
+
|
67
|
+
str.strip
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# internal
|
2
|
+
require "sidekiq/throttled/registry"
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Throttled
|
6
|
+
# Adds helpers to your worker classes
|
7
|
+
#
|
8
|
+
# @example Usage
|
9
|
+
#
|
10
|
+
# class MyWorker
|
11
|
+
# include Sidekiq::Worker
|
12
|
+
# include Sidekiq::Throttled::Worker
|
13
|
+
#
|
14
|
+
# sidkiq_options :queue => :my_queue
|
15
|
+
# sidekiq_throttle :threshold => { :limit => 123, :period => 1.hour }
|
16
|
+
#
|
17
|
+
# def perform
|
18
|
+
# # ...
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @see ClassMethods
|
23
|
+
module Worker
|
24
|
+
# Extends worker class with {ClassMethods}.
|
25
|
+
#
|
26
|
+
# @note Using `included` hook with extending worker with {ClassMethods}
|
27
|
+
# in order to make API inline with `include Sidekiq::Worker`.
|
28
|
+
#
|
29
|
+
# @private
|
30
|
+
def self.included(worker)
|
31
|
+
worker.send(:extend, ClassMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Helper methods added to the singleton class of destination
|
35
|
+
module ClassMethods
|
36
|
+
# Registers some strategy for the worker.
|
37
|
+
#
|
38
|
+
# @example Allow max 123 MyWorker jobs per hour
|
39
|
+
#
|
40
|
+
# class MyWorker
|
41
|
+
# include Sidekiq::Worker
|
42
|
+
# include Sidekiq::Throttled::Worker
|
43
|
+
#
|
44
|
+
# sidekiq_throttle({
|
45
|
+
# :threshold => { :limit => 123, :period => 1.hour }
|
46
|
+
# })
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @example Allow max 10 concurrently running MyWorker jobs
|
50
|
+
#
|
51
|
+
# class MyWorker
|
52
|
+
# include Sidekiq::Worker
|
53
|
+
# include Sidekiq::Throttled::Worker
|
54
|
+
#
|
55
|
+
# sidekiq_throttle({
|
56
|
+
# :concurrency => { :limit => 10 }
|
57
|
+
# })
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @example Allow max 10 concurrent MyWorker jobs and max 123 per hour
|
61
|
+
#
|
62
|
+
# class MyWorker
|
63
|
+
# include Sidekiq::Worker
|
64
|
+
# include Sidekiq::Throttled::Worker
|
65
|
+
#
|
66
|
+
# sidekiq_throttle({
|
67
|
+
# :threshold => { :limit => 123, :period => 1.hour },
|
68
|
+
# :concurrency => { :limit => 10 }
|
69
|
+
# })
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @see Registry.add
|
73
|
+
# @return [void]
|
74
|
+
def sidekiq_throttle(**kwargs)
|
75
|
+
Registry.add(self, **kwargs)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds current worker to preconfigured throtttling strtegy. Allows
|
79
|
+
# sharing same pool for multiple workers.
|
80
|
+
#
|
81
|
+
# First of all we need to create shared throttling strategy:
|
82
|
+
#
|
83
|
+
# # Create google_api throttling strategy
|
84
|
+
# Sidekiq::Throttled::Registry.add(:google_api, {
|
85
|
+
# :threshold => { :limit => 123, :period => 1.hour },
|
86
|
+
# :concurrency => { :limit => 123 }
|
87
|
+
# })
|
88
|
+
#
|
89
|
+
# Now we can assign it to our workers:
|
90
|
+
#
|
91
|
+
# class FetchProfileJob
|
92
|
+
# include Sidekiq::Worker
|
93
|
+
# include Sidekiq::Throttled::Worker
|
94
|
+
#
|
95
|
+
# sidekiq_throttle_as :google_api
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# class FetchCommentsJob
|
99
|
+
# include Sidekiq::Worker
|
100
|
+
# include Sidekiq::Throttled::Worker
|
101
|
+
#
|
102
|
+
# sidekiq_throttle_as :google_api
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# With the above configuration we ensure that there are maximum 10
|
106
|
+
# concurrently running jobs of FetchProfileJob or FetchCommentsJob
|
107
|
+
# allowed. And only 123 jobs of those are executed per hour.
|
108
|
+
#
|
109
|
+
# In other words, it will allow:
|
110
|
+
#
|
111
|
+
# - only `X` concurrent `FetchProfileJob`s
|
112
|
+
# - max `XX` `FetchProfileJob` per hour
|
113
|
+
# - only `Y` concurrent `FetchCommentsJob`s
|
114
|
+
# - max `YY` `FetchCommentsJob` per hour
|
115
|
+
#
|
116
|
+
# Where `(X + Y) == 10` and `(XX + YY) == 123`
|
117
|
+
#
|
118
|
+
# @see Registry.add_alias
|
119
|
+
# @return [void]
|
120
|
+
def sidekiq_throttle_as(name)
|
121
|
+
Registry.add_alias(self, name)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require "sidekiq/throttled/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "sidekiq-throttled"
|
10
|
+
spec.version = Sidekiq::Throttled::VERSION
|
11
|
+
spec.authors = ["Alexey V Zapparov"]
|
12
|
+
spec.email = ["ixti@member.fsf.org"]
|
13
|
+
|
14
|
+
spec.summary = "Concurrency and threshold throttling for Sidekiq."
|
15
|
+
spec.description = "Concurrency and threshold throttling for Sidekiq."
|
16
|
+
spec.homepage = "https://github.com/sensortower/sidekiq-throttled"
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "sidekiq", "< 4"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec"
|
31
|
+
spec.add_development_dependency "timecop"
|
32
|
+
spec.add_development_dependency "rubocop"
|
33
|
+
spec.add_development_dependency "rack-test"
|
34
|
+
spec.add_development_dependency "sinatra", "~> 1.4", ">= 1.4.6"
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq-throttled
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexey V Zapparov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sidekiq
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "<"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "<"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack-test
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sinatra
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.4'
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 1.4.6
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - "~>"
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '1.4'
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.4.6
|
131
|
+
description: Concurrency and threshold throttling for Sidekiq.
|
132
|
+
email:
|
133
|
+
- ixti@member.fsf.org
|
134
|
+
executables: []
|
135
|
+
extensions: []
|
136
|
+
extra_rdoc_files: []
|
137
|
+
files:
|
138
|
+
- ".gitignore"
|
139
|
+
- ".rspec"
|
140
|
+
- ".rubocop.yml"
|
141
|
+
- ".travis.yml"
|
142
|
+
- ".yardopts"
|
143
|
+
- Gemfile
|
144
|
+
- LICENSE.md
|
145
|
+
- README.md
|
146
|
+
- Rakefile
|
147
|
+
- bin/console
|
148
|
+
- bin/setup
|
149
|
+
- lib/sidekiq/throttled.rb
|
150
|
+
- lib/sidekiq/throttled/basic_fetch.rb
|
151
|
+
- lib/sidekiq/throttled/errors.rb
|
152
|
+
- lib/sidekiq/throttled/middleware.rb
|
153
|
+
- lib/sidekiq/throttled/registry.rb
|
154
|
+
- lib/sidekiq/throttled/strategy.rb
|
155
|
+
- lib/sidekiq/throttled/strategy/concurrency.lua
|
156
|
+
- lib/sidekiq/throttled/strategy/concurrency.rb
|
157
|
+
- lib/sidekiq/throttled/strategy/script.rb
|
158
|
+
- lib/sidekiq/throttled/strategy/threshold.lua
|
159
|
+
- lib/sidekiq/throttled/strategy/threshold.rb
|
160
|
+
- lib/sidekiq/throttled/testing.rb
|
161
|
+
- lib/sidekiq/throttled/version.rb
|
162
|
+
- lib/sidekiq/throttled/web.rb
|
163
|
+
- lib/sidekiq/throttled/web/index.html.erb
|
164
|
+
- lib/sidekiq/throttled/web/stats.rb
|
165
|
+
- lib/sidekiq/throttled/worker.rb
|
166
|
+
- sidekiq-throttled.gemspec
|
167
|
+
homepage: https://github.com/sensortower/sidekiq-throttled
|
168
|
+
licenses:
|
169
|
+
- MIT
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.2.3
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: Concurrency and threshold throttling for Sidekiq.
|
191
|
+
test_files: []
|