sidekiq-throttled 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|