sidekiq-throttler 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.
- data/.gitignore +18 -0
- data/.pryrc +11 -0
- data/.travis.yml +18 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +79 -0
- data/Rakefile +18 -0
- data/lib/sidekiq/throttler/rate_limit.rb +202 -0
- data/lib/sidekiq/throttler/version.rb +5 -0
- data/lib/sidekiq/throttler.rb +41 -0
- data/lib/sidekiq-throttler.rb +1 -0
- data/sidekiq-throttler.gemspec +40 -0
- data/spec/app/workers/custom_key_worker.rb +9 -0
- data/spec/app/workers/lolz_worker.rb +9 -0
- data/spec/app/workers/proc_worker.rb +9 -0
- data/spec/app/workers/regular_worker.rb +7 -0
- data/spec/sidekiq/throttler/rate_limit_spec.rb +301 -0
- data/spec/sidekiq/throttler_spec.rb +52 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/sidekiq.rb +20 -0
- metadata +364 -0
data/.gitignore
ADDED
data/.pryrc
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
- jruby-19mode
|
5
|
+
- rbx-19mode
|
6
|
+
- 2.0.0
|
7
|
+
branches:
|
8
|
+
only:
|
9
|
+
- master
|
10
|
+
notifications:
|
11
|
+
email:
|
12
|
+
recipients:
|
13
|
+
- gabriel@codeconcoction.com
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: jruby-19mode
|
17
|
+
- rvm: rbx-19mode
|
18
|
+
- rvm: 2.0.0
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
guard 'bundler' do
|
2
|
+
watch('Gemfile')
|
3
|
+
watch('sidekiq-throttler.gemspec')
|
4
|
+
end
|
5
|
+
|
6
|
+
guard 'rspec' do
|
7
|
+
watch(%r{^spec/app/.+_worker\.rb$}) { 'spec' }
|
8
|
+
watch(%r{^spec/.+_spec\.rb$})
|
9
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
10
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
11
|
+
end
|
12
|
+
|
13
|
+
guard 'yard' do
|
14
|
+
watch(%r{app/.+\.rb})
|
15
|
+
watch(%r{lib/.+\.rb})
|
16
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Gabriel Evans
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# Sidekiq::Throttler
|
2
|
+
|
3
|
+
[](http://travis-ci.org/gevans/sidekiq-throttler)
|
4
|
+
[](https://gemnasium.com/gevans/sidekiq-throttler)
|
5
|
+
|
6
|
+
Sidekiq::Throttler is a middleware for Sidekiq that adds the ability to rate
|
7
|
+
limit job execution on a per-worker basis.
|
8
|
+
|
9
|
+
## Compatibility
|
10
|
+
|
11
|
+
Sidekiq::Throttler is tested against MRI 1.9.3.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'sidekiq-throttler'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install sidekiq-throttler
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
In a Rails initializer or wherever you've configured Sidekiq, add
|
30
|
+
Sidekiq::Throttler to your server middleware:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
Sidekiq.configure_server do |config|
|
34
|
+
config.server_middleware do |chain|
|
35
|
+
chain.add Sidekiq::Throttler
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
In a worker, specify a threshold (maximum jobs) and period for throttling:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class MyWorker
|
46
|
+
include Sidekiq::Worker
|
47
|
+
|
48
|
+
sidekiq_options throttle: { threshold: 50, period: 1.hour }
|
49
|
+
|
50
|
+
def perform(user_id)
|
51
|
+
# Do some heavy API interactions.
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
In the above example, when the number of executed jobs for the worker exceeds
|
57
|
+
50 in an hour, remaining jobs will be delayed.
|
58
|
+
|
59
|
+
If throttling is per-user, for example, you can specify a `Proc` for `key` which
|
60
|
+
accepts the arguments passed to your worker's `perform` method:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
sidekiq_options throttle: { threshold: 20, period: 1.day, key: ->{ |user_id| user_id } }
|
64
|
+
```
|
65
|
+
|
66
|
+
In the above example, jobs are throttled for each user when they exceed 20 in a
|
67
|
+
day.
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
|
71
|
+
1. Fork it
|
72
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
73
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
74
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
75
|
+
5. Create new Pull Request
|
76
|
+
|
77
|
+
## License
|
78
|
+
|
79
|
+
MIT Licensed. See LICENSE.txt for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
5
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
require 'yard'
|
11
|
+
YARD::Rake::YardocTask.new
|
12
|
+
|
13
|
+
desc 'Start Pry with runtime dependencies loaded'
|
14
|
+
task :console, :script do |t, args|
|
15
|
+
command = 'bundle exec pry'
|
16
|
+
command += "-r #{args[:script]}" if args[:script]
|
17
|
+
sh command
|
18
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
class Throttler
|
3
|
+
##
|
4
|
+
# Handles the tracking of rate limits.
|
5
|
+
#
|
6
|
+
# TODO: Consider reducing `threshold` and `period` to smooth out job
|
7
|
+
# executions so that "24 jobs every 1 hour" becomes "1 job every 2 minutes
|
8
|
+
# and 30 seconds"
|
9
|
+
class RateLimit
|
10
|
+
|
11
|
+
##
|
12
|
+
# @return [Sidekiq::Worker]
|
13
|
+
# The worker to rate limit.
|
14
|
+
attr_reader :worker
|
15
|
+
|
16
|
+
##
|
17
|
+
# @return [Array]
|
18
|
+
# The message payload for the current job.
|
19
|
+
attr_reader :payload
|
20
|
+
|
21
|
+
##
|
22
|
+
# @return [String]
|
23
|
+
# The queue to rate limit.
|
24
|
+
attr_reader :queue
|
25
|
+
|
26
|
+
##
|
27
|
+
# @param [Sidekiq::Worker] worker
|
28
|
+
# The worker to rate limit.
|
29
|
+
#
|
30
|
+
# @param [Array<Object>] payload
|
31
|
+
# The message payload for the current job.
|
32
|
+
#
|
33
|
+
# @param [String] queue
|
34
|
+
# The queue to rate limit.
|
35
|
+
def initialize(worker, payload, queue)
|
36
|
+
@worker = worker
|
37
|
+
@payload = payload
|
38
|
+
@queue = queue
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Fetch the number of jobs executed.
|
43
|
+
#
|
44
|
+
# @return [Integer]
|
45
|
+
# The current number of jobs executed.
|
46
|
+
def count
|
47
|
+
self.class.count(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Increment the count of jobs executed.
|
52
|
+
#
|
53
|
+
# @return [Integer]
|
54
|
+
# The current number of jobs executed.
|
55
|
+
def increment
|
56
|
+
self.class.increment(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Returns the rate limit options for the current running worker.
|
61
|
+
#
|
62
|
+
# @return [{String => Float, Integer}]
|
63
|
+
def options
|
64
|
+
@options ||= (worker.class.get_sidekiq_options['throttle'] || {}).stringify_keys
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# @return [Integer]
|
69
|
+
# The number of jobs that are allowed within the `period`.
|
70
|
+
def threshold
|
71
|
+
@threshold ||= options['threshold'].to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# @return [Float]
|
76
|
+
# The number of seconds in the rate limit period.
|
77
|
+
def period
|
78
|
+
@period ||= options['period'].to_f
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# @return [Symbol]
|
83
|
+
# The key name used when storing counters for jobs.
|
84
|
+
def key
|
85
|
+
@key ||= if options['key']
|
86
|
+
options['key'].respond_to?(:call) ? options['key'].call(*payload) : options['key']
|
87
|
+
else
|
88
|
+
"#{@worker.class.to_s.underscore.gsub('/', ':')}:#{@queue}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Check if rate limiting options were correctly specified on the worker.
|
94
|
+
#
|
95
|
+
# @return [true, false]
|
96
|
+
def can_throttle?
|
97
|
+
[threshold, period].select(&:zero?).empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Check if rate limit has exceeded the threshold.
|
102
|
+
#
|
103
|
+
# @return [true, false]
|
104
|
+
def exceeded?
|
105
|
+
count >= threshold
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Check if rate limit is within the threshold.
|
110
|
+
#
|
111
|
+
# @return [true, false]
|
112
|
+
def within_bounds?
|
113
|
+
!exceeded?
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Set a callback to be executed when {#execute} is called and the rate
|
118
|
+
# limit has not exceeded the threshold.
|
119
|
+
def within_bounds(&block)
|
120
|
+
@within_bounds = block
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Set a callback to be executed when {#execute} is called and the rate
|
125
|
+
# limit has exceeded the threshold.
|
126
|
+
#
|
127
|
+
# @yieldparam [Integer] delay
|
128
|
+
# Delay in seconds to requeue job for.
|
129
|
+
def exceeded(&block)
|
130
|
+
@exceeded = block
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Executes a callback ({#within_bounds}, or {#exceeded}) depending on the
|
135
|
+
# state of the rate limit.
|
136
|
+
def execute
|
137
|
+
return @within_bounds.call unless can_throttle?
|
138
|
+
|
139
|
+
if exceeded?
|
140
|
+
@exceeded.call(period)
|
141
|
+
else
|
142
|
+
increment
|
143
|
+
@within_bounds.call
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Reset the tracking of job executions.
|
149
|
+
def self.reset!
|
150
|
+
@executions = Hash.new { |hash, key| hash[key] = [] }
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
##
|
156
|
+
# Fetch the number of jobs executed by the provided `RateLimit`.
|
157
|
+
#
|
158
|
+
# @param [RateLimit] limiter
|
159
|
+
#
|
160
|
+
# @return [Integer]
|
161
|
+
# The current number of jobs executed.
|
162
|
+
def self.count(limiter)
|
163
|
+
Thread.exclusive do
|
164
|
+
prune(limiter)
|
165
|
+
executions[limiter.key].length
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Increment the count of jobs executed by the provided `RateLimit`.
|
171
|
+
#
|
172
|
+
# @param [RateLimit] limiter
|
173
|
+
#
|
174
|
+
# @return [Integer]
|
175
|
+
# The current number of jobs executed.
|
176
|
+
def self.increment(limiter)
|
177
|
+
Thread.exclusive do
|
178
|
+
executions[limiter.key] << Time.now
|
179
|
+
end
|
180
|
+
count(limiter)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# A hash storing job executions as timestamps for each throttled worker.
|
185
|
+
def self.executions
|
186
|
+
@executions || reset!
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Remove old entries for the provided `RateLimit`.
|
191
|
+
#
|
192
|
+
# @param [RateLimit] limiter
|
193
|
+
# The rate limit to prune.
|
194
|
+
def self.prune(limiter)
|
195
|
+
executions[limiter.key].select! do |execution|
|
196
|
+
(Time.now - execution) < limiter.period
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
end # RateLimit
|
201
|
+
end # Throttler
|
202
|
+
end # Sidekiq
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
|
5
|
+
require 'sidekiq/throttler/version'
|
6
|
+
require 'sidekiq/throttler/rate_limit'
|
7
|
+
|
8
|
+
module Sidekiq
|
9
|
+
##
|
10
|
+
# Sidekiq server middleware. Throttles jobs when they exceed limits specified
|
11
|
+
# on the worker. Jobs that exceed the limit are requeued with a delay.
|
12
|
+
class Throttler
|
13
|
+
|
14
|
+
##
|
15
|
+
# Passes the worker, arguments, and queue to {RateLimit} and either yields
|
16
|
+
# or requeues the job depending on whether the worker is throttled.
|
17
|
+
#
|
18
|
+
# @param [Sidekiq::Worker] worker
|
19
|
+
# The worker the job belongs to.
|
20
|
+
#
|
21
|
+
# @param [Hash] msg
|
22
|
+
# The job message.
|
23
|
+
#
|
24
|
+
# @param [String] queue
|
25
|
+
# The current queue.
|
26
|
+
def call(worker, msg, queue)
|
27
|
+
rate_limit = RateLimit.new(worker, msg['args'], queue)
|
28
|
+
|
29
|
+
rate_limit.within_bounds do
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
|
33
|
+
rate_limit.exceeded do |delay|
|
34
|
+
worker.class.perform_in(delay, *msg['args'])
|
35
|
+
end
|
36
|
+
|
37
|
+
rate_limit.execute
|
38
|
+
end
|
39
|
+
|
40
|
+
end # Throttler
|
41
|
+
end # Sidekiq
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'sidekiq/throttler'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sidekiq/throttler/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'sidekiq-throttler'
|
8
|
+
gem.version = Sidekiq::Throttler::VERSION
|
9
|
+
gem.authors = ['Gabriel Evans']
|
10
|
+
gem.email = ['gabriel@codeconcoction.com']
|
11
|
+
gem.description = %q{Sidekiq middleware that adds the ability to rate limit job execution.}
|
12
|
+
gem.summary = %q{Sidekiq::Throttler is a middleware for Sidekiq that adds the ability to rate limit job execution on a per-worker basis.}
|
13
|
+
gem.homepage = 'https://github.com/gevans/sidekiq-throttler'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
|
20
|
+
gem.add_dependency 'activesupport'
|
21
|
+
gem.add_dependency 'sidekiq', '>= 2.5', '< 3.0'
|
22
|
+
|
23
|
+
gem.add_development_dependency 'rake'
|
24
|
+
gem.add_development_dependency 'pry'
|
25
|
+
gem.add_development_dependency 'yard'
|
26
|
+
gem.add_development_dependency 'redcarpet'
|
27
|
+
|
28
|
+
gem.add_development_dependency 'rspec'
|
29
|
+
gem.add_development_dependency 'rspec-redis_helper'
|
30
|
+
gem.add_development_dependency 'timecop'
|
31
|
+
gem.add_development_dependency 'simplecov'
|
32
|
+
|
33
|
+
gem.add_development_dependency 'guard'
|
34
|
+
gem.add_development_dependency 'guard-bundler'
|
35
|
+
gem.add_development_dependency 'guard-rspec'
|
36
|
+
gem.add_development_dependency 'guard-yard'
|
37
|
+
gem.add_development_dependency 'rb-fsevent'
|
38
|
+
gem.add_development_dependency 'rb-inotify'
|
39
|
+
gem.add_development_dependency 'growl'
|
40
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::Throttler::RateLimit do
|
4
|
+
|
5
|
+
let(:worker_class) do
|
6
|
+
LolzWorker
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:worker) do
|
10
|
+
worker_class.new
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:payload) do
|
14
|
+
['world']
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:queue) do
|
18
|
+
'meow'
|
19
|
+
end
|
20
|
+
|
21
|
+
subject(:rate_limit) do
|
22
|
+
described_class.new(worker, payload, 'meow')
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '.new' do
|
26
|
+
|
27
|
+
it 'initializes with a provided worker' do
|
28
|
+
rate_limit.worker.should eq(worker)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'initializes with provided payload' do
|
32
|
+
rate_limit.payload.should eq(payload)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'initializes with a provided queue' do
|
36
|
+
rate_limit.queue.should eq('meow')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#options' do
|
41
|
+
|
42
|
+
it 'retrieves throttle options from the worker' do
|
43
|
+
worker_class.get_sidekiq_options.should_receive(:[]).with('throttle')
|
44
|
+
rate_limit.options
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'stringifies the option keys' do
|
48
|
+
worker_class.get_sidekiq_options['throttle'].should_receive(:stringify_keys)
|
49
|
+
rate_limit.options
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'caches the returned options' do
|
53
|
+
rate_limit.options.object_id.should eq(rate_limit.options.object_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when the worker specifies no throttle options' do
|
57
|
+
|
58
|
+
let(:worker_class) do
|
59
|
+
Class.new do
|
60
|
+
include Sidekiq::Worker
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns an empty hash' do
|
65
|
+
rate_limit.options.should eq({})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#threshold' do
|
71
|
+
|
72
|
+
it 'retrieves the threshold from #options' do
|
73
|
+
rate_limit.options['threshold'] = 26
|
74
|
+
rate_limit.threshold.should eq(26)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'converts the threshold to an integer' do
|
78
|
+
rate_limit.options['threshold'] = '33'
|
79
|
+
rate_limit.threshold.should be_a(Integer)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'caches the returned integer' do
|
83
|
+
rate_limit.threshold.object_id.should eq(rate_limit.threshold.object_id)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#period' do
|
88
|
+
|
89
|
+
it 'retrieves the period from #options' do
|
90
|
+
rate_limit.options['period'] = 10.0
|
91
|
+
rate_limit.period.should eq(10.0)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'converts the period to a float' do
|
95
|
+
rate_limit.options['period'] = 27
|
96
|
+
rate_limit.period.should be_a(Float)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'caches the returned float' do
|
100
|
+
rate_limit.period.object_id.should eq(rate_limit.period.object_id)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#key' do
|
105
|
+
|
106
|
+
let(:worker_class) do
|
107
|
+
CustomKeyWorker
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'caches the key from the worker' do
|
111
|
+
rate_limit.key.object_id.should eq(rate_limit.key.object_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when key is a string' do
|
115
|
+
|
116
|
+
it 'returns the key as a symbol' do
|
117
|
+
rate_limit.key.should eq('winning')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when key is a Proc' do
|
122
|
+
|
123
|
+
let(:worker_class) do
|
124
|
+
ProcWorker
|
125
|
+
end
|
126
|
+
|
127
|
+
let(:payload) do
|
128
|
+
['wat', 'is', 'this']
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'returns the result of the called Proc' do
|
132
|
+
rate_limit.key.should eq('wat:is:this')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#can_throttle?' do
|
138
|
+
|
139
|
+
context 'when options are correctly specified' do
|
140
|
+
|
141
|
+
it 'returns true' do
|
142
|
+
rate_limit.can_throttle?.should be_true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
%w(threshold period).each do |method|
|
147
|
+
|
148
|
+
context "when ##{method} is zero" do
|
149
|
+
|
150
|
+
it 'returns false' do
|
151
|
+
rate_limit.stub(method.to_sym).and_return(0)
|
152
|
+
rate_limit.can_throttle?.should be_false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#exceeded?' do
|
159
|
+
|
160
|
+
context 'when #count is equal to #threshold' do
|
161
|
+
|
162
|
+
it 'returns true' do
|
163
|
+
rate_limit.should_receive(:count).and_return(rate_limit.threshold)
|
164
|
+
rate_limit.should be_exceeded
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when #count is greater than #threshold' do
|
169
|
+
|
170
|
+
it 'returns true' do
|
171
|
+
rate_limit.should_receive(:count).and_return(rate_limit.threshold + 1)
|
172
|
+
rate_limit.should be_exceeded
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'when #count is less than #threshold' do
|
177
|
+
|
178
|
+
it 'returns false' do
|
179
|
+
rate_limit.should_receive(:count).and_return(0)
|
180
|
+
rate_limit.should_not be_exceeded
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe '#within_bounds?' do
|
186
|
+
|
187
|
+
it 'returns the opposite of #exceeded?' do
|
188
|
+
rate_limit.should_receive(:exceeded?).and_return(true)
|
189
|
+
rate_limit.should_not be_within_bounds
|
190
|
+
rate_limit.should_receive(:exceeded?).and_return(false)
|
191
|
+
rate_limit.should be_within_bounds
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe '#exceeded' do
|
196
|
+
|
197
|
+
it 'accepts a block as a callback' do
|
198
|
+
rate_limit.exceeded { 'rawr' }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe '#within_bounds' do
|
203
|
+
|
204
|
+
it 'accepts a block as a callback' do
|
205
|
+
rate_limit.within_bounds { 'grr' }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe '#execute' do
|
210
|
+
|
211
|
+
context 'when rate limit cannot be throttled' do
|
212
|
+
|
213
|
+
before do
|
214
|
+
rate_limit.should_receive(:can_throttle?).and_return(false)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'calls the within bounds callback' do
|
218
|
+
callback = Proc.new {}
|
219
|
+
callback.should_receive(:call)
|
220
|
+
|
221
|
+
rate_limit.within_bounds(&callback)
|
222
|
+
rate_limit.execute
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'does not increment the counter' do
|
226
|
+
rate_limit.within_bounds {}
|
227
|
+
|
228
|
+
rate_limit.should_not_receive(:increment)
|
229
|
+
rate_limit.execute
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context 'when rate limit is exceeded' do
|
234
|
+
|
235
|
+
before do
|
236
|
+
rate_limit.should_receive(:exceeded?).and_return(true)
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'calls the exceeded callback with the configured #period' do
|
240
|
+
callback = Proc.new {}
|
241
|
+
callback.should_receive(:call).with(rate_limit.period)
|
242
|
+
|
243
|
+
rate_limit.exceeded(&callback)
|
244
|
+
rate_limit.execute
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'when rate limit is within bounds' do
|
249
|
+
|
250
|
+
it 'increments the counter' do
|
251
|
+
rate_limit.within_bounds {}
|
252
|
+
|
253
|
+
rate_limit.should_receive(:increment)
|
254
|
+
rate_limit.execute
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'calls the within bounds callback' do
|
258
|
+
callback = Proc.new {}
|
259
|
+
callback.should_receive(:call)
|
260
|
+
|
261
|
+
rate_limit.within_bounds(&callback)
|
262
|
+
rate_limit.execute
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
describe '#count' do
|
268
|
+
|
269
|
+
context 'when no jobs have executed' do
|
270
|
+
|
271
|
+
it 'returns 0' do
|
272
|
+
rate_limit.count.should be_zero
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe '#increment' do
|
278
|
+
|
279
|
+
it 'increments #count by one' do
|
280
|
+
Timecop.freeze do
|
281
|
+
expect { rate_limit.increment }.to change{ rate_limit.count }.by(1)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
context 'when #period has passed' do
|
286
|
+
|
287
|
+
it 'removes old increments' do
|
288
|
+
rate_limit.options['period'] = 5
|
289
|
+
|
290
|
+
Timecop.freeze
|
291
|
+
|
292
|
+
20.times do
|
293
|
+
Timecop.travel(1.second.from_now)
|
294
|
+
rate_limit.increment
|
295
|
+
end
|
296
|
+
|
297
|
+
rate_limit.count.should eq(5)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::Throttler do
|
4
|
+
|
5
|
+
subject(:throttler) do
|
6
|
+
described_class.new
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:worker) do
|
10
|
+
LolzWorker.new
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:message) do
|
14
|
+
{
|
15
|
+
args: 'Clint Eastwood'
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:queue) do
|
20
|
+
'default'
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#call' do
|
24
|
+
|
25
|
+
it 'instantiates a rate limit with the worker, args, and queue' do
|
26
|
+
rate_limit = Sidekiq::Throttler::RateLimit.new(worker, message['args'], queue)
|
27
|
+
Sidekiq::Throttler::RateLimit.should_receive(:new).with(
|
28
|
+
worker, message['args'], queue
|
29
|
+
).and_return(rate_limit)
|
30
|
+
|
31
|
+
throttler.call(worker, message, queue) {}
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'yields in RateLimit#within_bounds' do
|
35
|
+
expect { |b| throttler.call(worker, message, queue, &b) }.to yield_with_no_args
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'calls RateLimit#execute' do
|
39
|
+
Sidekiq::Throttler::RateLimit.any_instance.should_receive(:execute)
|
40
|
+
throttler.call(worker, message, queue)
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when rate limit is exceeded' do
|
44
|
+
|
45
|
+
it 'requeues the job with a delay' do
|
46
|
+
Sidekiq::Throttler::RateLimit.any_instance.should_receive(:exceeded?).and_return(true)
|
47
|
+
worker.class.should_receive(:perform_in).with(1.minute, *message['args'])
|
48
|
+
throttler.call(worker, message, queue)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
WORKERS = File.join(File.dirname(__FILE__), 'app', 'workers')
|
5
|
+
$LOAD_PATH.unshift(WORKERS)
|
6
|
+
|
7
|
+
unless ENV['CI']
|
8
|
+
require 'simplecov'
|
9
|
+
SimpleCov.start do
|
10
|
+
add_filter '/spec/'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rspec'
|
15
|
+
require 'timecop'
|
16
|
+
|
17
|
+
require 'sidekiq/throttler'
|
18
|
+
|
19
|
+
# Autoload every worker for the test suite that sits in spec/app/workers
|
20
|
+
Dir[File.join(WORKERS, '*.rb')].sort.each do |file|
|
21
|
+
name = File.basename(file, '.rb')
|
22
|
+
autoload name.camelize.to_sym, name
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec.configure do |config|
|
26
|
+
# Run specs in random order to surface order dependencies. If you find an
|
27
|
+
# order dependency and want to debug it, you can fix the order by providing
|
28
|
+
# the seed, which is printed after each run.
|
29
|
+
# --seed 1234
|
30
|
+
config.order = 'random'
|
31
|
+
|
32
|
+
config.before(:each) do
|
33
|
+
Sidekiq::Throttler::RateLimit.reset!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Requires supporting files with custom matchers and macros, etc,
|
38
|
+
# in ./support/ and its subdirectories.
|
39
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'sidekiq/util'
|
2
|
+
Sidekiq.logger.level = Logger::ERROR
|
3
|
+
|
4
|
+
require 'rspec-redis_helper'
|
5
|
+
RSpec::RedisHelper::CONFIG = { :url => 'redis://localhost/15', :namespace => 'testy' }
|
6
|
+
|
7
|
+
require 'sidekiq/redis_connection'
|
8
|
+
REDIS = Sidekiq::RedisConnection.create(RSpec::RedisHelper::CONFIG)
|
9
|
+
|
10
|
+
RSpec.configure do |spec|
|
11
|
+
spec.include RSpec::RedisHelper, redis: true
|
12
|
+
|
13
|
+
# clean the Redis database around each run
|
14
|
+
# @see https://www.relishapp.com/rspec/rspec-core/docs/hooks/around-hooks
|
15
|
+
spec.around( :each, redis: true ) do |example|
|
16
|
+
with_clean_redis do
|
17
|
+
example.run
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,364 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq-throttler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gabriel Evans
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sidekiq
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.5'
|
38
|
+
- - <
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '2.5'
|
49
|
+
- - <
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '3.0'
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rake
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: pry
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
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
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: yard
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: redcarpet
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
- !ruby/object:Gem::Dependency
|
117
|
+
name: rspec
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
name: rspec-redis_helper
|
134
|
+
requirement: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
type: :development
|
141
|
+
prerelease: false
|
142
|
+
version_requirements: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ! '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
- !ruby/object:Gem::Dependency
|
149
|
+
name: timecop
|
150
|
+
requirement: !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
type: :development
|
157
|
+
prerelease: false
|
158
|
+
version_requirements: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
- !ruby/object:Gem::Dependency
|
165
|
+
name: simplecov
|
166
|
+
requirement: !ruby/object:Gem::Requirement
|
167
|
+
none: false
|
168
|
+
requirements:
|
169
|
+
- - ! '>='
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
type: :development
|
173
|
+
prerelease: false
|
174
|
+
version_requirements: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ! '>='
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
- !ruby/object:Gem::Dependency
|
181
|
+
name: guard
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
183
|
+
none: false
|
184
|
+
requirements:
|
185
|
+
- - ! '>='
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
none: false
|
192
|
+
requirements:
|
193
|
+
- - ! '>='
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
- !ruby/object:Gem::Dependency
|
197
|
+
name: guard-bundler
|
198
|
+
requirement: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
type: :development
|
205
|
+
prerelease: false
|
206
|
+
version_requirements: !ruby/object:Gem::Requirement
|
207
|
+
none: false
|
208
|
+
requirements:
|
209
|
+
- - ! '>='
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
- !ruby/object:Gem::Dependency
|
213
|
+
name: guard-rspec
|
214
|
+
requirement: !ruby/object:Gem::Requirement
|
215
|
+
none: false
|
216
|
+
requirements:
|
217
|
+
- - ! '>='
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: '0'
|
220
|
+
type: :development
|
221
|
+
prerelease: false
|
222
|
+
version_requirements: !ruby/object:Gem::Requirement
|
223
|
+
none: false
|
224
|
+
requirements:
|
225
|
+
- - ! '>='
|
226
|
+
- !ruby/object:Gem::Version
|
227
|
+
version: '0'
|
228
|
+
- !ruby/object:Gem::Dependency
|
229
|
+
name: guard-yard
|
230
|
+
requirement: !ruby/object:Gem::Requirement
|
231
|
+
none: false
|
232
|
+
requirements:
|
233
|
+
- - ! '>='
|
234
|
+
- !ruby/object:Gem::Version
|
235
|
+
version: '0'
|
236
|
+
type: :development
|
237
|
+
prerelease: false
|
238
|
+
version_requirements: !ruby/object:Gem::Requirement
|
239
|
+
none: false
|
240
|
+
requirements:
|
241
|
+
- - ! '>='
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
- !ruby/object:Gem::Dependency
|
245
|
+
name: rb-fsevent
|
246
|
+
requirement: !ruby/object:Gem::Requirement
|
247
|
+
none: false
|
248
|
+
requirements:
|
249
|
+
- - ! '>='
|
250
|
+
- !ruby/object:Gem::Version
|
251
|
+
version: '0'
|
252
|
+
type: :development
|
253
|
+
prerelease: false
|
254
|
+
version_requirements: !ruby/object:Gem::Requirement
|
255
|
+
none: false
|
256
|
+
requirements:
|
257
|
+
- - ! '>='
|
258
|
+
- !ruby/object:Gem::Version
|
259
|
+
version: '0'
|
260
|
+
- !ruby/object:Gem::Dependency
|
261
|
+
name: rb-inotify
|
262
|
+
requirement: !ruby/object:Gem::Requirement
|
263
|
+
none: false
|
264
|
+
requirements:
|
265
|
+
- - ! '>='
|
266
|
+
- !ruby/object:Gem::Version
|
267
|
+
version: '0'
|
268
|
+
type: :development
|
269
|
+
prerelease: false
|
270
|
+
version_requirements: !ruby/object:Gem::Requirement
|
271
|
+
none: false
|
272
|
+
requirements:
|
273
|
+
- - ! '>='
|
274
|
+
- !ruby/object:Gem::Version
|
275
|
+
version: '0'
|
276
|
+
- !ruby/object:Gem::Dependency
|
277
|
+
name: growl
|
278
|
+
requirement: !ruby/object:Gem::Requirement
|
279
|
+
none: false
|
280
|
+
requirements:
|
281
|
+
- - ! '>='
|
282
|
+
- !ruby/object:Gem::Version
|
283
|
+
version: '0'
|
284
|
+
type: :development
|
285
|
+
prerelease: false
|
286
|
+
version_requirements: !ruby/object:Gem::Requirement
|
287
|
+
none: false
|
288
|
+
requirements:
|
289
|
+
- - ! '>='
|
290
|
+
- !ruby/object:Gem::Version
|
291
|
+
version: '0'
|
292
|
+
description: Sidekiq middleware that adds the ability to rate limit job execution.
|
293
|
+
email:
|
294
|
+
- gabriel@codeconcoction.com
|
295
|
+
executables: []
|
296
|
+
extensions: []
|
297
|
+
extra_rdoc_files: []
|
298
|
+
files:
|
299
|
+
- .gitignore
|
300
|
+
- .pryrc
|
301
|
+
- .travis.yml
|
302
|
+
- .yardopts
|
303
|
+
- CHANGELOG.md
|
304
|
+
- Gemfile
|
305
|
+
- Guardfile
|
306
|
+
- LICENSE.txt
|
307
|
+
- README.md
|
308
|
+
- Rakefile
|
309
|
+
- lib/sidekiq-throttler.rb
|
310
|
+
- lib/sidekiq/throttler.rb
|
311
|
+
- lib/sidekiq/throttler/rate_limit.rb
|
312
|
+
- lib/sidekiq/throttler/version.rb
|
313
|
+
- sidekiq-throttler.gemspec
|
314
|
+
- spec/app/workers/custom_key_worker.rb
|
315
|
+
- spec/app/workers/lolz_worker.rb
|
316
|
+
- spec/app/workers/proc_worker.rb
|
317
|
+
- spec/app/workers/regular_worker.rb
|
318
|
+
- spec/sidekiq/throttler/rate_limit_spec.rb
|
319
|
+
- spec/sidekiq/throttler_spec.rb
|
320
|
+
- spec/spec.opts
|
321
|
+
- spec/spec_helper.rb
|
322
|
+
- spec/support/sidekiq.rb
|
323
|
+
homepage: https://github.com/gevans/sidekiq-throttler
|
324
|
+
licenses: []
|
325
|
+
post_install_message:
|
326
|
+
rdoc_options: []
|
327
|
+
require_paths:
|
328
|
+
- lib
|
329
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
330
|
+
none: false
|
331
|
+
requirements:
|
332
|
+
- - ! '>='
|
333
|
+
- !ruby/object:Gem::Version
|
334
|
+
version: '0'
|
335
|
+
segments:
|
336
|
+
- 0
|
337
|
+
hash: -2112495299373871691
|
338
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
339
|
+
none: false
|
340
|
+
requirements:
|
341
|
+
- - ! '>='
|
342
|
+
- !ruby/object:Gem::Version
|
343
|
+
version: '0'
|
344
|
+
segments:
|
345
|
+
- 0
|
346
|
+
hash: -2112495299373871691
|
347
|
+
requirements: []
|
348
|
+
rubyforge_project:
|
349
|
+
rubygems_version: 1.8.24
|
350
|
+
signing_key:
|
351
|
+
specification_version: 3
|
352
|
+
summary: Sidekiq::Throttler is a middleware for Sidekiq that adds the ability to rate
|
353
|
+
limit job execution on a per-worker basis.
|
354
|
+
test_files:
|
355
|
+
- spec/app/workers/custom_key_worker.rb
|
356
|
+
- spec/app/workers/lolz_worker.rb
|
357
|
+
- spec/app/workers/proc_worker.rb
|
358
|
+
- spec/app/workers/regular_worker.rb
|
359
|
+
- spec/sidekiq/throttler/rate_limit_spec.rb
|
360
|
+
- spec/sidekiq/throttler_spec.rb
|
361
|
+
- spec/spec.opts
|
362
|
+
- spec/spec_helper.rb
|
363
|
+
- spec/support/sidekiq.rb
|
364
|
+
has_rdoc:
|