ihasa 1.0.0 → 1.1.2
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 +5 -5
- data/.editorconfig +27 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/Gemfile.redis4 +6 -0
- data/Gemfile.redis5 +6 -0
- data/README.md +37 -26
- data/ihasa.gemspec +7 -3
- data/lib/ihasa/bucket/implementation.rb +23 -0
- data/lib/ihasa/bucket/legacy_implementation.rb +33 -0
- data/lib/ihasa/bucket.rb +36 -107
- data/lib/ihasa/lua.rb +99 -0
- data/lib/ihasa/version.rb +1 -1
- data/lib/ihasa.rb +7 -2
- metadata +69 -15
- data/Gemfile.lock +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d09f7b450ed81b23e9db24d3b7e7c8ff1405fa6263d391d5a5c2d46424412593
|
4
|
+
data.tar.gz: 04e82d23b0c2c2a4d7eaf5627edfe9d99882850bdfac4f4524abb833ee9e3c3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e444211b1c86ef4f0b406235eb1da09a2804aa17d2d8ce883238db78189acdf1760c405bfcdd58b42250846bc95c0dfb2de7704539ec99afb39d4aea91a03c83
|
7
|
+
data.tar.gz: 965f4f293fda58ca1949e0fb451d3a8836fc70f5b34c12e72095b89a63ee2db3463731f03f4bc067109af7fb942cf09284abc505ec6967caab94141f06ccba3d
|
data/.editorconfig
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# EditorConfig helps developers define and maintain consistent
|
2
|
+
# coding styles between different editors and IDEs
|
3
|
+
# editorconfig.org
|
4
|
+
|
5
|
+
root = true
|
6
|
+
|
7
|
+
[*]
|
8
|
+
end_of_line = lf
|
9
|
+
charset = utf-8
|
10
|
+
trim_trailing_whitespace = true
|
11
|
+
insert_final_newline = true
|
12
|
+
indent_style = space
|
13
|
+
indent_size = 2
|
14
|
+
|
15
|
+
[*.rb]
|
16
|
+
indent_style = space
|
17
|
+
indent_size = 2
|
18
|
+
|
19
|
+
[*.yml]
|
20
|
+
indent_style = space
|
21
|
+
indent_size = 2
|
22
|
+
|
23
|
+
[Makefile]
|
24
|
+
indent_style = tab
|
25
|
+
|
26
|
+
[*.md]
|
27
|
+
trim_trailing_whitespace = false
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.3
|
data/.travis.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
rvm:
|
4
|
+
- 2.4
|
5
|
+
- 2.5
|
6
|
+
- 2.6
|
7
|
+
- 2.7
|
8
|
+
- 3.0
|
9
|
+
- 3.1
|
10
|
+
gemfile:
|
11
|
+
- Gemfile
|
12
|
+
- Gemfile.redis4
|
13
|
+
- Gemfile.redis5
|
14
|
+
jobs:
|
15
|
+
exclude:
|
16
|
+
- rvm: 2.4
|
17
|
+
gemfile: Gemfile.redis5
|
18
|
+
script: bundle exec rspec
|
19
|
+
services:
|
20
|
+
- redis-server
|
21
|
+
sudo: false
|
data/Gemfile.redis4
ADDED
data/Gemfile.redis5
ADDED
data/README.md
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
|
1
|
+
# Ihasa
|
2
|
+
|
3
|
+
[](https://travis-ci.org/bankair/ihasa)
|
4
|
+
|
5
|
+
_No more pounding on your APIs !_
|
2
6
|
|
3
7
|
Ihasa is a ruby implementation of the [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) backed-up by Redis.
|
4
8
|
|
5
9
|
It provides a way to share your rate/burst limit across multiple servers, as well as a simple interface.
|
6
10
|
|
7
|
-
|
11
|
+
**Why use Ihasa?**
|
8
12
|
|
9
|
-
1. It's easy to use ([go check the usage section](#usage)
|
13
|
+
1. It's easy to use ([go check the usage section](#usage))
|
10
14
|
2. It supports rate AND burst
|
11
15
|
3. It does not reset all rate limit consumption each new second/minute/hour
|
12
16
|
4. It has [namespaces](#namespaces)
|
13
17
|
|
18
|
+
This README file contains the following sections:
|
14
19
|
|
15
20
|
- [Installation](#installation)
|
16
21
|
- [Usage](#usage)
|
@@ -25,7 +30,7 @@ Installation is standard:
|
|
25
30
|
$ gem install ihasa
|
26
31
|
```
|
27
32
|
|
28
|
-
You can
|
33
|
+
You can include it in your `Gemfile` as well:
|
29
34
|
|
30
35
|
```
|
31
36
|
gem 'ihasa', require: false
|
@@ -39,7 +44,7 @@ Be sure to require Ihasa:
|
|
39
44
|
require 'ihasa'
|
40
45
|
```
|
41
46
|
|
42
|
-
To create a new bucket
|
47
|
+
To create a new bucket that accepts 5 requests per second with an allowed burst of 10
|
43
48
|
requests per second (the default values), use the `Ihasa.bucket` method:
|
44
49
|
|
45
50
|
```ruby
|
@@ -50,8 +55,8 @@ Please note that the default redis connection is built from the `REDIS_URL`
|
|
50
55
|
environment variable, or use the default constructor of redis-rb
|
51
56
|
(`redis://localhost:6379`).
|
52
57
|
|
53
|
-
Now, you can use your token bucket to check if an incoming request can be handled
|
54
|
-
or must be
|
58
|
+
Now, you can use your token bucket to check if an incoming request can be handled,
|
59
|
+
or must be declined:
|
55
60
|
|
56
61
|
```ruby
|
57
62
|
def process(request)
|
@@ -64,22 +69,24 @@ def process(request)
|
|
64
69
|
end
|
65
70
|
```
|
66
71
|
|
67
|
-
Please note that there is also a `Ihasa::Bucket#accept?!` method that
|
68
|
-
`Ihasa::Bucket::EmptyBucket`
|
72
|
+
Please note that there is also a `Ihasa::Bucket#accept?!` method that raises an
|
73
|
+
`Ihasa::Bucket::EmptyBucket` error if the limit has already been reached.
|
69
74
|
|
70
75
|
### Advanced
|
71
76
|
|
72
77
|
In this section, you will find some details on the available configuration options of the Ihasa::Bucket
|
73
|
-
class, as well
|
78
|
+
class, as well as advice on how to run many Buckets simultaneously.
|
74
79
|
|
75
|
-
#### Using
|
80
|
+
#### Using multiple buckets
|
76
81
|
|
77
|
-
If you want to enforce rate limits
|
78
|
-
Which can be a
|
82
|
+
If you want to enforce per-customer rate limits, you must create as many buckets as you have customers.
|
83
|
+
Which can be quite a few if you are successful ;).
|
79
84
|
|
80
|
-
To have
|
81
|
-
no longer use the `Ihasa.bucket` method. Instead, you should back
|
82
|
-
(for example) and initialize them in
|
85
|
+
To have many buckets in parallel, and to avoid resetting your redis namespaces too often, I suggest you
|
86
|
+
no longer use the `Ihasa.bucket` method. Instead, you should back up your buckets with activerecord models
|
87
|
+
(for example) and initialize them in an after-creation model callback.
|
88
|
+
|
89
|
+
To help you with that, we added the `save` and `delete` instance methods to the `Ihasa::Bucket` class.
|
83
90
|
|
84
91
|
Example:
|
85
92
|
|
@@ -91,10 +98,14 @@ Example:
|
|
91
98
|
@implementation ||= Ihasa::Bucket.new(rate, burst, prefix, $redis)
|
92
99
|
end
|
93
100
|
|
94
|
-
# The Ihasa::Bucket#
|
101
|
+
# The Ihasa::Bucket#save set the relevant
|
95
102
|
# keys in your redis instance to have a working bucket. Do it
|
96
103
|
# only when you create or update your bucket's configuration.
|
97
|
-
after_save { implementation.
|
104
|
+
after_save { implementation.save }
|
105
|
+
|
106
|
+
# The Ihasa::Bucket#delete remove the variables stored into
|
107
|
+
# the redis instance.
|
108
|
+
after_destroy { implementation.delete }
|
98
109
|
|
99
110
|
delegate :accept?, to: :implementation
|
100
111
|
end
|
@@ -124,9 +135,9 @@ Example:
|
|
124
135
|
end
|
125
136
|
```
|
126
137
|
|
127
|
-
#### Configuring rate and burst limit
|
138
|
+
#### Configuring rate limit and burst limit
|
128
139
|
|
129
|
-
|
140
|
+
You can configure both the rate limit and burst limit:
|
130
141
|
|
131
142
|
```ruby
|
132
143
|
bucket = Ihasa.bucket(rate: 20, burst: 100)
|
@@ -134,7 +145,7 @@ bucket = Ihasa.bucket(rate: 20, burst: 100)
|
|
134
145
|
|
135
146
|
#### Namespaces
|
136
147
|
|
137
|
-
You can have as many
|
148
|
+
You can have as many buckets as you want on the same redis instance, as long as you
|
138
149
|
configure different namespace for each of them.
|
139
150
|
|
140
151
|
Here is an example of using two different buckets for reading and writing to data:
|
@@ -162,11 +173,11 @@ class Controller < ActionController::Base
|
|
162
173
|
|
163
174
|
#### Redis
|
164
175
|
|
165
|
-
By default, all new buckets use the redis instance hosted at localhost:6379.
|
166
|
-
|
176
|
+
By default, all new buckets use the redis instance hosted at localhost:6379. You can
|
177
|
+
override this default like so:
|
167
178
|
|
168
179
|
1. Override the `REDIS_URL` env variable. All new buckets will use that instance
|
169
|
-
2. Override the redis url on a bucket creation basis like
|
180
|
+
2. Override the redis url on a bucket creation basis like this:
|
170
181
|
|
171
182
|
```ruby
|
172
183
|
Ihasa.bucket(redis: Redis.new(url: 'redis://fancy_host:6379'))
|
@@ -174,8 +185,8 @@ Ihasa.bucket(redis: Redis.new(url: 'redis://fancy_host:6379'))
|
|
174
185
|
|
175
186
|
## Example
|
176
187
|
|
177
|
-
This is an example of a rack middleware
|
178
|
-
|
188
|
+
This is an example of a rack middleware that accepts 20 requests per seconds, and
|
189
|
+
allows bursts up to 100 requests per second:
|
179
190
|
|
180
191
|
```ruby
|
181
192
|
class RateLimiter
|
data/ihasa.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'ihasa'
|
8
8
|
s.version = Ihasa::Version::STRING
|
9
9
|
s.platform = Gem::Platform::RUBY
|
10
|
-
s.required_ruby_version = '>=
|
10
|
+
s.required_ruby_version = '>= 2.0.0'
|
11
11
|
s.authors = ['Alexandre Ignjatovic']
|
12
12
|
s.description = <<-EOF
|
13
13
|
A Redis-backed rate limiter written in ruby and
|
@@ -39,6 +39,10 @@ Gem::Specification.new do |s|
|
|
39
39
|
|
40
40
|
s.summary = 'Redis-backed rate limiter (token bucket) written in Ruby and Lua'
|
41
41
|
|
42
|
-
s.
|
43
|
-
s.add_development_dependency
|
42
|
+
s.add_development_dependency 'byebug', '~> 11.1.3'
|
43
|
+
s.add_development_dependency 'pry', '~> 0.14.2'
|
44
|
+
s.add_development_dependency 'rspec', '~> 3.11'
|
45
|
+
s.add_development_dependency 'rubocop', '~> 1.1'
|
46
|
+
|
47
|
+
s.add_runtime_dependency 'redis', '>= 3', '< 6'
|
44
48
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'ihasa/lua'
|
2
|
+
require 'singleton'
|
3
|
+
module Ihasa
|
4
|
+
# For redis server whith version >= 3.2
|
5
|
+
class Bucket::Implementation
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def save(bucket)
|
9
|
+
sha = bucket.redis.script(:load, Lua::TOKEN_BUCKET_ALGORITHM)
|
10
|
+
if sha != Lua::TOKEN_BUCKET_HASH
|
11
|
+
raise "SHA1 mismatch: expected #{Lua::TOKEN_BUCKET_HASH}, got #{sha}"
|
12
|
+
end
|
13
|
+
bucket.redis.eval(
|
14
|
+
Lua.configuration(bucket.rate, bucket.burst),
|
15
|
+
bucket.keys
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def accept?(bucket)
|
20
|
+
bucket.redis.evalsha(Lua::TOKEN_BUCKET_HASH, bucket.keys)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'ihasa/lua'
|
2
|
+
require 'singleton'
|
3
|
+
module Ihasa
|
4
|
+
# For redis server whose version is prior to 3.2
|
5
|
+
class Bucket::LegacyImplementation
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def save(bucket)
|
9
|
+
bucket.redis.eval(
|
10
|
+
Lua.configuration(
|
11
|
+
bucket.rate,
|
12
|
+
bucket.burst,
|
13
|
+
Lua.now_declaration(redis_time(bucket.redis))
|
14
|
+
),
|
15
|
+
bucket.keys
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def accept?(bucket)
|
20
|
+
now = redis_time bucket.redis
|
21
|
+
script = Lua.token_bucket_algorithm_legacy(now)
|
22
|
+
bucket.redis.eval(script, bucket.keys)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
MICROSECS_PER_SEC = 10**6
|
28
|
+
def redis_time(redis)
|
29
|
+
seconds, microseconds = redis.time
|
30
|
+
seconds + microseconds.to_f / MICROSECS_PER_SEC
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/ihasa/bucket.rb
CHANGED
@@ -1,31 +1,50 @@
|
|
1
|
-
require '
|
1
|
+
require 'ihasa/lua'
|
2
2
|
module Ihasa
|
3
|
-
NOK = 0
|
4
|
-
OK = 1
|
5
3
|
# Bucket class. That bucket fills up to burst, by rate per
|
6
4
|
# second. Each accept? or accept?! call decrement it from 1.
|
7
5
|
class Bucket
|
8
|
-
|
6
|
+
class << self
|
7
|
+
def create(*args)
|
8
|
+
new(*args).tap(&:save)
|
9
|
+
end
|
10
|
+
|
11
|
+
REDIS_VERSION_WITH_REPLICATE_COMMANDS_SUPPORT = 3.2
|
12
|
+
|
13
|
+
def legacy_mode?(redis)
|
14
|
+
redis_version(redis) < REDIS_VERSION_WITH_REPLICATE_COMMANDS_SUPPORT
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def redis_version(redis)
|
20
|
+
Float(redis.info['redis_version'][/\d+\.\d+/])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :redis, :keys, :rate, :burst, :prefix
|
9
25
|
def initialize(rate, burst, prefix, redis)
|
26
|
+
@implementation =
|
27
|
+
if self.class.legacy_mode?(redis)
|
28
|
+
require 'ihasa/bucket/legacy_implementation'
|
29
|
+
LegacyImplementation.instance
|
30
|
+
else
|
31
|
+
require 'ihasa/bucket/implementation'
|
32
|
+
Implementation.instance
|
33
|
+
end
|
10
34
|
@prefix = prefix
|
11
|
-
@keys = {}
|
12
|
-
@keys[:rate] = "#{prefix}:RATE"
|
13
|
-
@keys[:allowance] = "#{prefix}:ALLOWANCE"
|
14
|
-
@keys[:burst] = "#{prefix}:BURST"
|
15
|
-
@keys[:last] = "#{prefix}:LAST"
|
35
|
+
@keys = Ihasa::OPTIONS.map { |opt| "#{prefix}:#{opt.upcase}" }
|
16
36
|
@redis = redis
|
17
37
|
@rate = Float rate
|
18
38
|
@burst = Float burst
|
19
|
-
self.digest = Digest::SHA1.hexdigest statement
|
20
39
|
end
|
21
40
|
|
22
41
|
SETUP_ADVICE = 'Ensure that the method '\
|
23
|
-
|
42
|
+
'Ihasa::Bucket#save was called.'.freeze
|
24
43
|
SETUP_ERROR = ('Redis raised an error: %{msg}. ' + SETUP_ADVICE).freeze
|
25
44
|
class RedisNamespaceSetupError < RuntimeError; end
|
26
45
|
|
27
46
|
def accept?
|
28
|
-
result =
|
47
|
+
result = @implementation.accept?(self) == OK
|
29
48
|
return yield if result && block_given?
|
30
49
|
result
|
31
50
|
rescue Redis::CommandError => e
|
@@ -36,106 +55,16 @@ module Ihasa
|
|
36
55
|
|
37
56
|
def accept!
|
38
57
|
result = (block_given? ? accept?(&Proc.new) : accept?)
|
39
|
-
raise EmptyBucket, "Bucket #{
|
58
|
+
raise EmptyBucket, "Bucket #{prefix} throttle limit" unless result
|
40
59
|
result
|
41
60
|
end
|
42
61
|
|
43
|
-
def
|
44
|
-
|
45
|
-
redis_eval <<-LUA
|
46
|
-
#{INTRO_STATEMENT}
|
47
|
-
#{redis_set rate, @rate}
|
48
|
-
#{redis_set burst, @burst}
|
49
|
-
#{redis_set allowance, @burst}
|
50
|
-
#{redis_set last, 'now'}
|
51
|
-
LUA
|
52
|
-
end
|
53
|
-
|
54
|
-
protected
|
55
|
-
|
56
|
-
attr_accessor :digest
|
57
|
-
|
58
|
-
def load_statement
|
59
|
-
sha = redis.script(:load, statement)
|
60
|
-
if sha != digest
|
61
|
-
raise 'SHA1 inconsistency: expected #{digest}, got #{sha}'
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
require 'forwardable'
|
66
|
-
extend Forwardable
|
67
|
-
|
68
|
-
def_delegator :@keys, :keys
|
69
|
-
def_delegator :@keys, :values, :redis_keys
|
70
|
-
|
71
|
-
def index(key)
|
72
|
-
keys.index(key) + 1
|
73
|
-
end
|
74
|
-
|
75
|
-
# Please note that the replicate_commands is mandatory when using a
|
76
|
-
# non deterministic command before writing shit to the redis instance.
|
77
|
-
INTRO_STATEMENT = <<-LUA.freeze
|
78
|
-
redis.replicate_commands()
|
79
|
-
local now = redis.call('TIME')
|
80
|
-
now = now[1] + now[2] * 10 ^ -6
|
81
|
-
LUA
|
82
|
-
|
83
|
-
def redis_eval(statement)
|
84
|
-
redis.eval(statement, redis_keys)
|
85
|
-
end
|
86
|
-
|
87
|
-
ELAPSED_STATEMENT = 'local elapsed = now - last'.freeze
|
88
|
-
|
89
|
-
def local_statements
|
90
|
-
results = %i(rate burst last allowance).map do |key|
|
91
|
-
"local #{key} = tonumber(#{redis_get(redis_key(key))})"
|
92
|
-
end
|
93
|
-
results << ELAPSED_STATEMENT
|
94
|
-
results.join "\n"
|
95
|
-
end
|
96
|
-
|
97
|
-
ALLOWANCE_UPDATE_STATEMENT = <<-LUA.freeze
|
98
|
-
allowance = allowance + (elapsed * rate)
|
99
|
-
if allowance > burst then
|
100
|
-
allowance = burst
|
101
|
-
end
|
102
|
-
LUA
|
103
|
-
|
104
|
-
def statement
|
105
|
-
@statement ||= <<-LUA
|
106
|
-
#{INTRO_STATEMENT}
|
107
|
-
#{local_statements}
|
108
|
-
#{ALLOWANCE_UPDATE_STATEMENT}
|
109
|
-
local result = #{NOK}
|
110
|
-
if allowance >= 1.0 then
|
111
|
-
allowance = allowance - 1.0
|
112
|
-
result = #{OK}
|
113
|
-
end
|
114
|
-
#{redis_set(last, 'now')}
|
115
|
-
#{redis_set(allowance, 'allowance')}
|
116
|
-
return result
|
117
|
-
LUA
|
118
|
-
end
|
119
|
-
|
120
|
-
def redis_exists(key)
|
121
|
-
"redis.call('EXISTS', #{key})"
|
122
|
-
end
|
123
|
-
|
124
|
-
def redis_key(key)
|
125
|
-
"KEYS[#{index key}]"
|
126
|
-
end
|
127
|
-
|
128
|
-
def redis_get(key)
|
129
|
-
"tonumber(redis.call('GET', #{key}))"
|
130
|
-
end
|
131
|
-
|
132
|
-
def redis_set(key, value)
|
133
|
-
"redis.call('SET', #{key}, tostring(#{value}))"
|
62
|
+
def save
|
63
|
+
@implementation.save(self)
|
134
64
|
end
|
135
65
|
|
136
|
-
def
|
137
|
-
|
138
|
-
redis_key sym
|
66
|
+
def delete
|
67
|
+
redis.del(keys)
|
139
68
|
end
|
140
69
|
end
|
141
70
|
end
|
data/lib/ihasa/lua.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
module Ihasa
|
3
|
+
# Contains lua related logic
|
4
|
+
module Lua
|
5
|
+
NOW_DECLARATION = <<-LUA.freeze
|
6
|
+
redis.replicate_commands()
|
7
|
+
local now = redis.call('TIME')
|
8
|
+
now = now[1] + now[2] * 10 ^ -6
|
9
|
+
LUA
|
10
|
+
|
11
|
+
ALLOWANCE_UPDATE_STATEMENT = <<-LUA.freeze
|
12
|
+
allowance = allowance + (elapsed * rate)
|
13
|
+
if allowance > burst then
|
14
|
+
allowance = burst
|
15
|
+
end
|
16
|
+
LUA
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def now_declaration(value)
|
20
|
+
"local now = #{value}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Please note that the replicate_commands is mandatory when using a
|
24
|
+
# non deterministic command before writing shit to the redis instance
|
25
|
+
# for versions >= 3.2
|
26
|
+
def configuration(rate_value, burst_value, now_declaration = NOW_DECLARATION)
|
27
|
+
<<-LUA
|
28
|
+
#{now_declaration}
|
29
|
+
#{set rate, rate_value}
|
30
|
+
#{set burst, burst_value}
|
31
|
+
#{set allowance, burst_value}
|
32
|
+
#{set last, 'now'}
|
33
|
+
LUA
|
34
|
+
end
|
35
|
+
|
36
|
+
def index(key)
|
37
|
+
Integer(Ihasa::OPTIONS.index(key)) + 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch(key)
|
41
|
+
"KEYS[#{index key}]"
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(key)
|
45
|
+
"tonumber(redis.call('GET', #{key}))"
|
46
|
+
end
|
47
|
+
|
48
|
+
def set(key, value)
|
49
|
+
"redis.call('SET', #{key}, tostring(#{value}))"
|
50
|
+
end
|
51
|
+
|
52
|
+
def exists?(key)
|
53
|
+
"redis.call('EXISTS', #{key})"
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_missing(sym, *args, &block)
|
57
|
+
super unless Ihasa::OPTIONS.include? sym
|
58
|
+
fetch sym
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_local(key)
|
62
|
+
"local #{key} = tonumber(#{get(fetch(key))})"
|
63
|
+
end
|
64
|
+
|
65
|
+
def token_bucket_algorithm_legacy(now_value)
|
66
|
+
<<-LUA.freeze
|
67
|
+
#{now_declaration(now_value)}
|
68
|
+
#{TOKEN_BUCKET_ALGORITHM_BODY}
|
69
|
+
LUA
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
ELAPSED_STATEMENT = 'local elapsed = now - last'.freeze
|
74
|
+
SEP = "\n".freeze
|
75
|
+
LOCAL_VARIABLES = Ihasa::OPTIONS
|
76
|
+
.map { |key| to_local(key) }
|
77
|
+
.tap { |vars| vars << ELAPSED_STATEMENT }.join(SEP).freeze
|
78
|
+
TOKEN_BUCKET_ALGORITHM_BODY = <<-LUA.freeze
|
79
|
+
#{LOCAL_VARIABLES}
|
80
|
+
#{ALLOWANCE_UPDATE_STATEMENT}
|
81
|
+
local result = #{Ihasa::NOK}
|
82
|
+
if allowance >= 1.0 then
|
83
|
+
allowance = allowance - 1.0
|
84
|
+
result = #{Ihasa::OK}
|
85
|
+
end
|
86
|
+
#{set(last, 'now')}
|
87
|
+
#{set(allowance, 'allowance')}
|
88
|
+
return result
|
89
|
+
LUA
|
90
|
+
TOKEN_BUCKET_ALGORITHM = <<-LUA.freeze
|
91
|
+
#{NOW_DECLARATION}
|
92
|
+
#{TOKEN_BUCKET_ALGORITHM_BODY}
|
93
|
+
LUA
|
94
|
+
TOKEN_BUCKET_HASH = Digest::SHA1.hexdigest(
|
95
|
+
Lua::TOKEN_BUCKET_ALGORITHM
|
96
|
+
).freeze
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/ihasa/version.rb
CHANGED
data/lib/ihasa.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
require 'redis'
|
2
2
|
require 'ihasa/version'
|
3
|
-
require 'ihasa/bucket'
|
4
3
|
|
5
4
|
# Ihasa module. Root of the Ihasa::Bucket class
|
6
5
|
module Ihasa
|
6
|
+
NOK = 0
|
7
|
+
OK = 1
|
8
|
+
OPTIONS = %i(rate burst last allowance).freeze
|
9
|
+
|
10
|
+
require 'ihasa/bucket'
|
11
|
+
|
7
12
|
module_function
|
8
13
|
|
9
14
|
def default_redis
|
@@ -16,6 +21,6 @@ module Ihasa
|
|
16
21
|
|
17
22
|
DEFAULT_PREFIX = 'IHAB'.freeze
|
18
23
|
def bucket(rate: 5, burst: 10, prefix: DEFAULT_PREFIX, redis: default_redis)
|
19
|
-
Bucket.
|
24
|
+
Bucket.create(rate, burst, prefix, redis)
|
20
25
|
end
|
21
26
|
end
|
metadata
CHANGED
@@ -1,43 +1,91 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ihasa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Ignjatovic
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: byebug
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
19
|
+
version: 11.1.3
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 11.1.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.14.2
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.14.2
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rspec
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - "~>"
|
32
46
|
- !ruby/object:Gem::Version
|
33
|
-
version: '3.
|
47
|
+
version: '3.11'
|
34
48
|
type: :development
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version: '3.
|
54
|
+
version: '3.11'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3'
|
76
|
+
- - "<"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '6'
|
79
|
+
type: :runtime
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '3'
|
86
|
+
- - "<"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '6'
|
41
89
|
description: |2
|
42
90
|
A Redis-backed rate limiter written in ruby and
|
43
91
|
using the token bucket algorithm.
|
@@ -50,18 +98,25 @@ extra_rdoc_files:
|
|
50
98
|
- LICENSE.txt
|
51
99
|
- README.md
|
52
100
|
files:
|
53
|
-
-
|
101
|
+
- ".editorconfig"
|
102
|
+
- ".ruby-version"
|
103
|
+
- ".travis.yml"
|
104
|
+
- Gemfile.redis4
|
105
|
+
- Gemfile.redis5
|
54
106
|
- LICENSE.txt
|
55
107
|
- README.md
|
56
108
|
- ihasa.gemspec
|
57
109
|
- lib/ihasa.rb
|
58
110
|
- lib/ihasa/bucket.rb
|
111
|
+
- lib/ihasa/bucket/implementation.rb
|
112
|
+
- lib/ihasa/bucket/legacy_implementation.rb
|
113
|
+
- lib/ihasa/lua.rb
|
59
114
|
- lib/ihasa/version.rb
|
60
115
|
homepage: http://github.com/bankair/ihasa
|
61
116
|
licenses:
|
62
117
|
- MIT
|
63
118
|
metadata: {}
|
64
|
-
post_install_message:
|
119
|
+
post_install_message:
|
65
120
|
rdoc_options: []
|
66
121
|
require_paths:
|
67
122
|
- lib
|
@@ -69,16 +124,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
124
|
requirements:
|
70
125
|
- - ">="
|
71
126
|
- !ruby/object:Gem::Version
|
72
|
-
version:
|
127
|
+
version: 2.0.0
|
73
128
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
129
|
requirements:
|
75
130
|
- - ">="
|
76
131
|
- !ruby/object:Gem::Version
|
77
132
|
version: '0'
|
78
133
|
requirements: []
|
79
|
-
|
80
|
-
|
81
|
-
signing_key:
|
134
|
+
rubygems_version: 3.3.7
|
135
|
+
signing_key:
|
82
136
|
specification_version: 4
|
83
137
|
summary: Redis-backed rate limiter (token bucket) written in Ruby and Lua
|
84
138
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,44 +0,0 @@
|
|
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
|