simple_throttle 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT_LICENSE.txt +22 -0
- data/README.md +24 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/lib/simple_throttle.rb +150 -0
- data/simple_throttle.gemspec +25 -0
- data/spec/simple_throttle_spec.rb +55 -0
- data/spec/spec_helper.rb +14 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c5d75f7ea1ae6ba61f7ec9894e4247259d3195f8
|
4
|
+
data.tar.gz: b1cb14758b9d5729ab9cae5baa22c8cbd40eb9b1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78c8fd5675425a14094a04ff7f745a8da481d51741da8605ff3f460d77160b6421b30ae6ebba9bb412c45a28a0df8bc502c4a2dd19bd4cf8f87056ba753dcee3
|
7
|
+
data.tar.gz: 3553b9f03f048bb96b83935ff93fede2c034d83e9656d32e314e9c8cacec8bf681715c593663e4ce357ba41c59d075b153ecce581af853755c1479565c1a386d
|
data/MIT_LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 WHI, Inc.
|
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,24 @@
|
|
1
|
+
This gem provides a very simple throttling mechanism backed by redis for limiting access to a resource.
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
# Initialize Redis client
|
7
|
+
SimpleThrottle.set_redis(Redis.new)
|
8
|
+
|
9
|
+
# Add a global throttle (max of 10 requests in 60 seconds)
|
10
|
+
SimpleThrottle.add(:things, limit: 10, ttl: 60)
|
11
|
+
|
12
|
+
# Throttle a resource
|
13
|
+
if SimpleThrottle[:things].allowed!
|
14
|
+
do_somthing
|
15
|
+
else
|
16
|
+
raise "Too many requests. Resource available in #{SimpleThrottle[:things].wait_time} seconds"
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
Calling `allow!` will return true if the throttle limit has not yet been reached and will also start tracking a new call if it returned true. There is no way to release a throttled call (that's why it's called SimpleThrottle).
|
21
|
+
|
22
|
+
The throttle data is kept in redis as a list of timestamps and will be auto expired if it falls out of use. The thottles time windows are rolling time windows and more calls will be allowed as soon as possible.
|
23
|
+
|
24
|
+
Redis server 2.6 or greater is required.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc 'Default: run unit tests.'
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
desc 'RVM likes to call it tests'
|
7
|
+
task :tests => :test
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'rspec'
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
desc 'Run the unit tests'
|
13
|
+
RSpec::Core::RakeTask.new(:test)
|
14
|
+
rescue LoadError
|
15
|
+
task :test do
|
16
|
+
STDERR.puts "You must have rspec 2.0 installed to run the tests"
|
17
|
+
end
|
18
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
# Create a simple throttle that can be used to limit the number of request for a resouce
|
4
|
+
# per time period. These objects are thread safe.
|
5
|
+
class SimpleThrottle
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# Add a new throttle that can be referenced later with the [] method.
|
9
|
+
def add(name, limit:, ttl:)
|
10
|
+
@throttles ||= {}
|
11
|
+
@throttles[name.to_s] = new(name, limit: limit, ttl: ttl)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a globally defined throttle with the specfied name.
|
15
|
+
def [](name)
|
16
|
+
if defined?(@throttles) && @throttles
|
17
|
+
@throttles[name.to_s]
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set the Redis instance to use for maintaining the throttle. This can either be set
|
24
|
+
# with a hard coded value or by the value yielded by a block. If the block form is used
|
25
|
+
# it will be invoked at runtime to get the instance. Use this method if your Redis instance
|
26
|
+
# isn't constant (for example if you're in a forking environment and re-initialize connections
|
27
|
+
# on fork)
|
28
|
+
def set_redis(client = nil, &block)
|
29
|
+
@redis_client = (client || block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return the Redis instance where the throttles are stored.
|
33
|
+
def redis
|
34
|
+
if @redis_client.is_a?(Proc)
|
35
|
+
@redis_client.call
|
36
|
+
else
|
37
|
+
@redis_client
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :name, :limit, :ttl
|
43
|
+
|
44
|
+
# Create a new throttle with the given name. The ttl argument specifies the time
|
45
|
+
# range that is being used for measuring in seconds while the limit specifies how
|
46
|
+
# many calls are allowed in that range.
|
47
|
+
def initialize(name, limit:, ttl:)
|
48
|
+
@name = name.to_s
|
49
|
+
@name.freeze unless @name.frozen?
|
50
|
+
@limit = limit
|
51
|
+
@ttl = ttl
|
52
|
+
@script_sha_1 = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns true if the limit for the throttle has not been reached yet. This method
|
56
|
+
# will also track the throttled resource as having been invoked on each call.
|
57
|
+
def allowed!
|
58
|
+
size = current_size(true)
|
59
|
+
if size < limit
|
60
|
+
true
|
61
|
+
else
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Reset a throttle back to zero.
|
67
|
+
def reset!
|
68
|
+
self.class.redis.del(redis_key)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Peek at the current number for throttled calls being tracked.
|
72
|
+
def peek
|
73
|
+
current_size(false)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns when the next resource call should be allowed. Note that this doesn't guarantee that
|
77
|
+
# calling allow! will return true if the wait time is zero since other processes or threads can
|
78
|
+
# claim the resource.
|
79
|
+
def wait_time
|
80
|
+
if peek < limit
|
81
|
+
0.0
|
82
|
+
else
|
83
|
+
first = self.class.redis.lindex(redis_key, 0).to_f / 1000.0
|
84
|
+
delta = Time.now.to_f - first
|
85
|
+
delta = 0.0 if delta < 0
|
86
|
+
delta
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Evaluate and execute a Lua script on the redis server that returns the number calls currently being tracked.
|
93
|
+
# If push is set to true then a new item will be added to the list.
|
94
|
+
def current_size(push)
|
95
|
+
redis = self.class.redis
|
96
|
+
@script_sha_1 ||= redis.script(:load, lua_script)
|
97
|
+
begin
|
98
|
+
push_arg = (push ? 1 : 0)
|
99
|
+
time_ms = (Time.now.to_f * 1000).round
|
100
|
+
ttl_ms = ttl * 1000
|
101
|
+
redis.evalsha(@script_sha_1, [], [redis_key, limit, ttl_ms, time_ms, push_arg])
|
102
|
+
rescue Redis::CommandError => e
|
103
|
+
if e.message.include?('NOSCRIPT'.freeze)
|
104
|
+
@script_sha_1 = redis.script(:load, lua_script)
|
105
|
+
retry
|
106
|
+
else
|
107
|
+
raise e
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def redis_key
|
113
|
+
"simple_throttle.#{name}"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Server side Lua script that maintains the throttle in redis. The throttle is stored as a list
|
117
|
+
# of timestamps in milliseconds. When the script is invoked it will scan the oldest entries
|
118
|
+
# removing any that should be expired from the list. If the list is below the specified limit
|
119
|
+
# then the current entry will be added. The list is marked to expire with the oldest entry so
|
120
|
+
# there's no need to cleanup the lists.
|
121
|
+
def lua_script
|
122
|
+
<<-LUA
|
123
|
+
local list_key = ARGV[1]
|
124
|
+
local limit = tonumber(ARGV[2])
|
125
|
+
local ttl = tonumber(ARGV[3])
|
126
|
+
local now = ARGV[4]
|
127
|
+
local push = tonumber(ARGV[5])
|
128
|
+
|
129
|
+
local size = redis.call('llen', list_key)
|
130
|
+
if size >= limit then
|
131
|
+
local expired = tonumber(now) - ttl
|
132
|
+
while size > 0 do
|
133
|
+
local t = redis.call('lpop', list_key)
|
134
|
+
if tonumber(t) > expired then
|
135
|
+
redis.call('lpush', list_key, t)
|
136
|
+
break
|
137
|
+
end
|
138
|
+
size = size - 1
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
if push > 0 and size < limit then
|
143
|
+
redis.call('rpush', list_key, now)
|
144
|
+
redis.call('pexpire', list_key, ttl)
|
145
|
+
end
|
146
|
+
|
147
|
+
return size
|
148
|
+
LUA
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "simple_throttle"
|
7
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).chomp
|
8
|
+
spec.authors = ["We Heart It", "Brian Durand"]
|
9
|
+
spec.email = ["dev@weheartit.com", "bbdurand@gmail.com"]
|
10
|
+
spec.summary = "Simple redis backed throttling mechanism to limit access to a resource"
|
11
|
+
spec.description = "Simple redis backed throttling mechanism to limit access to a resource."
|
12
|
+
spec.homepage = "https://github.com/weheartit/sidekiq_fast_enq"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency('redis')
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SimpleThrottle do
|
4
|
+
|
5
|
+
it "should tell if a call is allowed" do
|
6
|
+
throttle = SimpleThrottle.new("test_simple_throttle", limit: 3, ttl: 1)
|
7
|
+
throttle.reset!
|
8
|
+
other_throttle = SimpleThrottle.new("test_simple_throttle_2", limit: 3, ttl: 1)
|
9
|
+
other_throttle.reset!
|
10
|
+
|
11
|
+
expect(throttle.peek).to eq 0
|
12
|
+
expect(throttle.allowed!).to eq true
|
13
|
+
expect(throttle.peek).to eq 1
|
14
|
+
expect(throttle.allowed!).to eq true
|
15
|
+
expect(throttle.peek).to eq 2
|
16
|
+
expect(throttle.allowed!).to eq true
|
17
|
+
expect(throttle.peek).to eq 3
|
18
|
+
expect(throttle.allowed!).to eq false
|
19
|
+
expect(throttle.peek).to eq 3
|
20
|
+
expect(throttle.allowed!).to eq false
|
21
|
+
wait_time = throttle.wait_time
|
22
|
+
expect(wait_time).to be > 0.0
|
23
|
+
expect(wait_time).to be <= 1.0
|
24
|
+
|
25
|
+
expect(other_throttle.allowed!).to eq true
|
26
|
+
expect(other_throttle.peek).to eq 1
|
27
|
+
expect(other_throttle.wait_time).to eq 0.0
|
28
|
+
|
29
|
+
sleep(1.1)
|
30
|
+
|
31
|
+
expect(throttle.allowed!).to eq true
|
32
|
+
sleep(0.3)
|
33
|
+
expect(throttle.allowed!).to eq true
|
34
|
+
sleep(0.3)
|
35
|
+
expect(throttle.allowed!).to eq true
|
36
|
+
sleep(0.3)
|
37
|
+
expect(throttle.allowed!).to eq false
|
38
|
+
sleep(0.3)
|
39
|
+
expect(throttle.allowed!).to eq true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be able to add global throttles" do
|
43
|
+
SimpleThrottle.add(:test_1, limit: 4, ttl: 60)
|
44
|
+
SimpleThrottle.add(:test_2, limit: 10, ttl: 3600)
|
45
|
+
t1 = SimpleThrottle["test_1"]
|
46
|
+
expect(t1.name).to eq "test_1"
|
47
|
+
expect(t1.limit).to eq 4
|
48
|
+
expect(t1.ttl).to eq 60
|
49
|
+
t1 = SimpleThrottle[:test_2]
|
50
|
+
expect(t1.name).to eq "test_2"
|
51
|
+
expect(t1.limit).to eq 10
|
52
|
+
expect(t1.ttl).to eq 3600
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path('../../lib/simple_throttle', __FILE__)
|
2
|
+
|
3
|
+
SimpleThrottle.set_redis(Redis.new)
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.run_all_when_everything_filtered = true
|
7
|
+
config.filter_run :focus
|
8
|
+
|
9
|
+
# Run specs in random order to surface order dependencies. If you find an
|
10
|
+
# order dependency and want to debug it, you can fix the order by providing
|
11
|
+
# the seed, which is printed after each run.
|
12
|
+
# --seed 1234
|
13
|
+
config.order = 'random'
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_throttle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- We Heart It
|
8
|
+
- Brian Durand
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-05-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.3'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.3'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Simple redis backed throttling mechanism to limit access to a resource.
|
71
|
+
email:
|
72
|
+
- dev@weheartit.com
|
73
|
+
- bbdurand@gmail.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- MIT_LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- VERSION
|
82
|
+
- lib/simple_throttle.rb
|
83
|
+
- simple_throttle.gemspec
|
84
|
+
- spec/simple_throttle_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
homepage: https://github.com/weheartit/sidekiq_fast_enq
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.4.5
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Simple redis backed throttling mechanism to limit access to a resource
|
110
|
+
test_files:
|
111
|
+
- spec/simple_throttle_spec.rb
|
112
|
+
- spec/spec_helper.rb
|