ihasa 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile.lock +44 -0
- data/LICENSE.txt +20 -0
- data/README.md +139 -0
- data/ihasa.gemspec +44 -0
- data/lib/ihasa.rb +19 -0
- data/lib/ihasa/bucket.rb +123 -0
- data/lib/ihasa/version.rb +14 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 46f87a6594af9e83c6ff6d56f7d897d376325993
|
4
|
+
data.tar.gz: 13437815218c853f71cae4a93b0af469bfaee9bb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd880e51552ae7a23d07b7f6ea93e1cd333e3ee82bdeed6ea207c343deb71e026c9ddb4caa2c24e68e3993f879e37444117523927c887d85f78a2cc7e9e42c5b
|
7
|
+
data.tar.gz: 9316733742bf6224bc8bf137ab804970530a69e6be3bb66e19361812fdfa21dd5afe3a7de69f2c8b3a91e4e44c3f04ff685675677805e1869311329a3706321d
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
ast (2.2.0)
|
5
|
+
byebug (8.2.2)
|
6
|
+
diff-lcs (1.2.5)
|
7
|
+
parser (2.3.1.0)
|
8
|
+
ast (~> 2.2)
|
9
|
+
powerpack (0.1.1)
|
10
|
+
rainbow (2.1.0)
|
11
|
+
redis (3.3.0)
|
12
|
+
rspec (3.4.0)
|
13
|
+
rspec-core (~> 3.4.0)
|
14
|
+
rspec-expectations (~> 3.4.0)
|
15
|
+
rspec-mocks (~> 3.4.0)
|
16
|
+
rspec-core (3.4.4)
|
17
|
+
rspec-support (~> 3.4.0)
|
18
|
+
rspec-expectations (3.4.0)
|
19
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
+
rspec-support (~> 3.4.0)
|
21
|
+
rspec-mocks (3.4.1)
|
22
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
+
rspec-support (~> 3.4.0)
|
24
|
+
rspec-support (3.4.1)
|
25
|
+
rubocop (0.40.0)
|
26
|
+
parser (>= 2.3.1.0, < 3.0)
|
27
|
+
powerpack (~> 0.1)
|
28
|
+
rainbow (>= 1.99.1, < 3.0)
|
29
|
+
ruby-progressbar (~> 1.7)
|
30
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
31
|
+
ruby-progressbar (1.8.1)
|
32
|
+
unicode-display_width (1.0.5)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
byebug
|
39
|
+
redis
|
40
|
+
rspec
|
41
|
+
rubocop
|
42
|
+
|
43
|
+
BUNDLED WITH
|
44
|
+
1.11.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012-14 Bozhidar Batsov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
No more pounding on your APIs !
|
2
|
+
|
3
|
+
Ihasa is a ruby implementation of the [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) backed-up by Redis.
|
4
|
+
|
5
|
+
It provides a way to share your rate/burst limit across multiple servers, as well as a simple interface.
|
6
|
+
|
7
|
+
*Why use Ihasa?*
|
8
|
+
1. It's easy to use ([go check the usage section](#usage)
|
9
|
+
2. It supports rate AND burst
|
10
|
+
3. It does not reset all rate limit consumption each new second/minute/hour
|
11
|
+
4. It has [namespaces](namespaces)
|
12
|
+
|
13
|
+
|
14
|
+
- [Installation](#installation)
|
15
|
+
- [Usage](#usage)
|
16
|
+
- [Advanced](#advanced)
|
17
|
+
- [Example](#example)
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Installation is standard:
|
22
|
+
|
23
|
+
```
|
24
|
+
$ gem install ihasa
|
25
|
+
```
|
26
|
+
|
27
|
+
You can as well include it in you `Gemfile`:
|
28
|
+
|
29
|
+
```
|
30
|
+
gem 'rubocop', require: false
|
31
|
+
```
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
Be sure to require Ihasa:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'ihasa'
|
39
|
+
```
|
40
|
+
|
41
|
+
To create a new bucket accepting 5 requests per second with an allowed burst of 10
|
42
|
+
requests per second (the default values), use the `Ihasa.bucket` method:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
bucket = Ihasa.bucket
|
46
|
+
```
|
47
|
+
|
48
|
+
Please note that the default redis connection is built from the `REDIS_URL`
|
49
|
+
environment variable, or use the default constructor of redis-rb
|
50
|
+
(`redis://localhost:6379`).
|
51
|
+
|
52
|
+
Now, you can use your token bucket to check if an incoming request can be handled
|
53
|
+
or must be turned down:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def process(request)
|
57
|
+
if @bucket.accept?
|
58
|
+
# Do very interesting things with the request
|
59
|
+
# ...
|
60
|
+
else
|
61
|
+
puts "Could not process request #{request}. Rate limit violated."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Please note that there is also a `Ihasa::Bucket#accept?!` method that raise a
|
67
|
+
`Ihasa::Bucket::EmptyBucket` errors if the limit is violated.
|
68
|
+
|
69
|
+
### Advanced
|
70
|
+
|
71
|
+
Here is some details on the available configuration options of the Ihasa::Bucket
|
72
|
+
class.
|
73
|
+
|
74
|
+
#### Configuring rate and burst limit
|
75
|
+
|
76
|
+
Two configuration options exists to configure the rate and burst limits:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
bucket = Ihasa.bucket(rate: 20, burst: 100)
|
80
|
+
```
|
81
|
+
|
82
|
+
#### Namespaces
|
83
|
+
|
84
|
+
You can have as many bucket as you want on the same redis instance, as long as you
|
85
|
+
configure different namespace for each of them.
|
86
|
+
|
87
|
+
Here is an example of using two different buckets for reading and writing to data:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
class Controller < ActionController::Base
|
91
|
+
def self.read_bucket
|
92
|
+
@read_bucket ||= Ihasa.bucket(prefix: 'read')
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.write_bucket
|
96
|
+
@write_bucket ||= Ihasa.bucket(prefix: 'write')
|
97
|
+
end
|
98
|
+
|
99
|
+
def read(request)
|
100
|
+
return head 403 unless self.class.read_bucket.accept?
|
101
|
+
# ... Standard rendering ...
|
102
|
+
end
|
103
|
+
|
104
|
+
def write(request)
|
105
|
+
return head 403 unless self.class.write_bucket.accept?
|
106
|
+
# ... Standard rendering ...
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Redis
|
111
|
+
|
112
|
+
By default, all new buckets use the redis instance hosted at localhost:6379. There is
|
113
|
+
however two way to configure the used redis instance:
|
114
|
+
1. Override the `REDIS_URL` env variable. All new buckets will use that instance
|
115
|
+
2. Override the redis url on a bucket creation basis like follow:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
Ihasa.bucket(redis: Redis.new(url: 'redis://fancy_host:6379'))
|
119
|
+
```
|
120
|
+
|
121
|
+
## Example
|
122
|
+
|
123
|
+
This is an example of a rack middleware accepting 20 requests per seconds, and
|
124
|
+
tolerating burst up to 100 requests per second:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class RateLimiter
|
128
|
+
BUCKET = Ihasa.bucket(rate: 20, burst: 100)
|
129
|
+
|
130
|
+
def initialize(app)
|
131
|
+
@app = app
|
132
|
+
end
|
133
|
+
|
134
|
+
def call(env)
|
135
|
+
return @app.call(env) if BUCKET.accept?
|
136
|
+
[403, {'Content-Type' => 'text/plain'}, ["Request limit violated\n"]]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
data/ihasa.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
4
|
+
require 'ihasa/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'ihasa'
|
8
|
+
s.version = Ihasa::Version::STRING
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.required_ruby_version = '>= 1.9.3'
|
11
|
+
s.authors = ['Alexandre Ignjatovic']
|
12
|
+
s.description = <<-EOF
|
13
|
+
A Redis-backed rate limiter written in ruby and
|
14
|
+
using the token bucket algorithm.
|
15
|
+
Its light and efficient implementation takes
|
16
|
+
advantage of the Lua capabilities of Redis.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
s.email = 'alexandre.ignjatovic@gmail.com'
|
20
|
+
s.files = `git ls-files`.split($RS).reject do |file|
|
21
|
+
file =~ %r{^(?:
|
22
|
+
spec/.*
|
23
|
+
|Gemfile
|
24
|
+
|Rakefile
|
25
|
+
|\.rspec
|
26
|
+
|\.gitignore
|
27
|
+
|\.rubocop.yml
|
28
|
+
|\.rubocop_todo.yml
|
29
|
+
|.*\.eps
|
30
|
+
)$}x
|
31
|
+
end
|
32
|
+
s.test_files = []
|
33
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
34
|
+
s.extra_rdoc_files = ['LICENSE.txt', 'README.md']
|
35
|
+
s.homepage = 'http://github.com/bankair/ihasa'
|
36
|
+
s.licenses = ['MIT']
|
37
|
+
s.require_paths = ['lib']
|
38
|
+
s.rubygems_version = '1.8.23'
|
39
|
+
|
40
|
+
s.summary = 'Redis-backed rate limiter (token bucket) written in Ruby and Lua'
|
41
|
+
|
42
|
+
s.add_runtime_dependency('redis', '~> 3')
|
43
|
+
s.add_development_dependency('rspec', '~> 3.4')
|
44
|
+
end
|
data/lib/ihasa.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'ihasa/version'
|
3
|
+
require 'ihasa/bucket'
|
4
|
+
|
5
|
+
module Ihasa
|
6
|
+
module_function
|
7
|
+
def default_redis
|
8
|
+
@redis ||= if ENV['REDIS_URL']
|
9
|
+
Redis.new url: ENV['REDIS_URL']
|
10
|
+
else
|
11
|
+
Redis.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
DEFAULT_REDIS_PREFIX = 'IHAB'
|
16
|
+
def bucket(rate: 5, burst: 10, prefix: DEFAULT_REDIS_PREFIX, redis: default_redis)
|
17
|
+
@implementation = Bucket.new(rate, burst, prefix, redis)
|
18
|
+
end
|
19
|
+
end
|
data/lib/ihasa/bucket.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
module Ihasa
|
2
|
+
NOK = 0
|
3
|
+
OK = 1
|
4
|
+
# Bucket class. That bucket fills up to burst, by rate per
|
5
|
+
# second. Each accept? or accept?! call decrement it from 1.
|
6
|
+
class Bucket
|
7
|
+
attr_reader :redis
|
8
|
+
def initialize(rate, burst, prefix, redis)
|
9
|
+
@prefix = prefix
|
10
|
+
@keys = {}
|
11
|
+
@keys[:rate] = "#{prefix}:RATE"
|
12
|
+
@keys[:allowance] = "#{prefix}:ALLOWANCE"
|
13
|
+
@keys[:burst] = "#{prefix}:BURST"
|
14
|
+
@keys[:last] = "#{prefix}:LAST"
|
15
|
+
@redis = redis
|
16
|
+
@rate = Float rate
|
17
|
+
@burst = Float burst
|
18
|
+
redis_init
|
19
|
+
end
|
20
|
+
|
21
|
+
def accept?
|
22
|
+
result = redis_eval(statement) == OK
|
23
|
+
return yield if result && block_given?
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
class EmptyBucket < RuntimeError; end
|
28
|
+
|
29
|
+
def accept!
|
30
|
+
result = (block_given? ? accept?(&Proc.new) : accept?)
|
31
|
+
raise EmptyBucket, "Bucket #{@prefix} throttle limit" unless result
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def redis_init
|
38
|
+
redis_eval <<-LUA
|
39
|
+
#{INTRO_STATEMENT}
|
40
|
+
#{redis_set rate, @rate}
|
41
|
+
#{redis_set burst, @burst}
|
42
|
+
#{redis_set allowance, @burst}
|
43
|
+
#{redis_set last, 'now'}
|
44
|
+
LUA
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'forwardable'
|
48
|
+
extend Forwardable
|
49
|
+
|
50
|
+
def_delegator :@keys, :keys
|
51
|
+
def_delegator :@keys, :values, :redis_keys
|
52
|
+
|
53
|
+
def index(key)
|
54
|
+
keys.index(key) + 1
|
55
|
+
end
|
56
|
+
|
57
|
+
# Please note that the replicate_commands is mandatory when using a
|
58
|
+
# non deterministic command before writing shit to the redis instance.
|
59
|
+
INTRO_STATEMENT = <<-LUA.freeze
|
60
|
+
redis.replicate_commands()
|
61
|
+
local now = redis.call('TIME')
|
62
|
+
now = now[1] + now[2] * 10 ^ -6
|
63
|
+
LUA
|
64
|
+
|
65
|
+
def redis_eval(statement)
|
66
|
+
redis.eval(statement, redis_keys)
|
67
|
+
end
|
68
|
+
|
69
|
+
ELAPSED_STATEMENT = 'local elapsed = now - last'.freeze
|
70
|
+
|
71
|
+
def local_statements
|
72
|
+
results = %i(rate burst last allowance).map do |key|
|
73
|
+
"local #{key} = tonumber(#{redis_get(redis_key(key))})"
|
74
|
+
end
|
75
|
+
results << ELAPSED_STATEMENT
|
76
|
+
results.join "\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
ALLOWANCE_UPDATE_STATEMENT = <<-LUA.freeze
|
80
|
+
allowance = allowance + (elapsed * rate)
|
81
|
+
if allowance > burst then
|
82
|
+
allowance = burst
|
83
|
+
end
|
84
|
+
LUA
|
85
|
+
|
86
|
+
def statement
|
87
|
+
@statement ||= <<-LUA
|
88
|
+
#{INTRO_STATEMENT}
|
89
|
+
#{local_statements}
|
90
|
+
#{ALLOWANCE_UPDATE_STATEMENT}
|
91
|
+
local result = #{NOK}
|
92
|
+
if allowance >= 1.0 then
|
93
|
+
#{redis_set(last, 'now')}
|
94
|
+
allowance = allowance - 1.0
|
95
|
+
result = #{OK}
|
96
|
+
end
|
97
|
+
#{redis_set(allowance, 'allowance')}
|
98
|
+
return result
|
99
|
+
LUA
|
100
|
+
end
|
101
|
+
|
102
|
+
def redis_exists(key)
|
103
|
+
"redis.call('EXISTS', #{key})"
|
104
|
+
end
|
105
|
+
|
106
|
+
def redis_key(key)
|
107
|
+
"KEYS[#{index key}]"
|
108
|
+
end
|
109
|
+
|
110
|
+
def redis_get(key)
|
111
|
+
"tonumber(redis.call('GET', #{key}))"
|
112
|
+
end
|
113
|
+
|
114
|
+
def redis_set(key, value)
|
115
|
+
"redis.call('SET', #{key}, tostring(#{value}))"
|
116
|
+
end
|
117
|
+
|
118
|
+
def method_missing(sym, *args, &block)
|
119
|
+
super unless @keys.key?(sym)
|
120
|
+
redis_key sym
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ihasa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexandre Ignjatovic
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.4'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.4'
|
41
|
+
description: |2
|
42
|
+
A Redis-backed rate limiter written in ruby and
|
43
|
+
using the token bucket algorithm.
|
44
|
+
Its light and efficient implementation takes
|
45
|
+
advantage of the Lua capabilities of Redis.
|
46
|
+
email: alexandre.ignjatovic@gmail.com
|
47
|
+
executables: []
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files:
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
files:
|
53
|
+
- Gemfile.lock
|
54
|
+
- LICENSE.txt
|
55
|
+
- README.md
|
56
|
+
- ihasa.gemspec
|
57
|
+
- lib/ihasa.rb
|
58
|
+
- lib/ihasa/bucket.rb
|
59
|
+
- lib/ihasa/version.rb
|
60
|
+
homepage: http://github.com/bankair/ihasa
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 1.9.3
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.5.1
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Redis-backed rate limiter (token bucket) written in Ruby and Lua
|
84
|
+
test_files: []
|