rapidity 0.0.4.64534 → 0.0.5.88564
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +92 -0
- data/lib/rapidity/limiter.lua +37 -0
- data/lib/rapidity/limiter.rb +31 -27
- data/lib/rapidity/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc0bdcbad063ffce0142d3eacdad37e4ee78186ee4bb2092c1677c871fb52fa2
|
4
|
+
data.tar.gz: 15017d1bae8b36afc44d484616bc2fe5f21c85e868606c9dc137ad0f4c04a5f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68967c5c216162141ddf9be4871c47f4db4966b07ad2e2faac5950ec1987244a445df52e41693b3fcb6a324d7d61b640c1293b139d671059047e84778a5df3b0
|
7
|
+
data.tar.gz: a97d793eb5b42c556ee971c1484f25b5910f8576ba280bd60ce845780f79cd63fdd015a5618568b04b4e9bda83000995e3dda096d920d7aca3c33e8ef5a7c129
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rapidity (0.0.
|
4
|
+
rapidity (0.0.5.88564)
|
5
5
|
activesupport
|
6
6
|
connection_pool
|
7
7
|
redis
|
@@ -58,7 +58,7 @@ GEM
|
|
58
58
|
psych (3.3.2)
|
59
59
|
public_suffix (4.0.6)
|
60
60
|
rainbow (3.0.0)
|
61
|
-
redis (4.
|
61
|
+
redis (4.7.0)
|
62
62
|
reek (6.0.4)
|
63
63
|
kwalify (~> 0.7.0)
|
64
64
|
parser (~> 3.0.0)
|
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Rapidity
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/rapidity)
|
4
|
+
[](https://rubygems.org/gems/rapidity/versions)
|
5
|
+
[](http://www.rubydoc.info/gems/rapidity)
|
6
|
+
|
7
|
+
[](https://lysander.rnds.pro/api/v1/badges/rapidity_coverage.html)
|
8
|
+
[](https://lysander.rnds.pro/api/v1/badges/rapidity_quality.html)
|
9
|
+
[](https://lysander.rnds.pro/api/v1/badges/rapidity_outdated.html)
|
10
|
+
[](https://lysander.rnds.pro/api/v1/badges/rapidity_vulnerable.html)
|
11
|
+
|
12
|
+
Simple but fast Redis-backed distributed rate limiter. Allows you to specify time interval and count within to limit distributed operations.
|
13
|
+
|
14
|
+
Features:
|
15
|
+
|
16
|
+
- extremly simple
|
17
|
+
- safe
|
18
|
+
- fast
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Rapidity has two variants:
|
23
|
+
|
24
|
+
- simple `Rapidity::Limiter` to handle single distibuted counter
|
25
|
+
- complex `Rapidity::Composer` to handle multiple counters at once
|
26
|
+
|
27
|
+
### Single conter with concurrent access
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
pool = ConnectionPool.new(size: 10) do
|
31
|
+
Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
|
32
|
+
end
|
33
|
+
|
34
|
+
# allow no more 10 requests within 5 seconds
|
35
|
+
limiter = Rapidity::Limiter.new(pool, name: 'requests', threshold: 10, interval: 5)
|
36
|
+
|
37
|
+
loop do
|
38
|
+
# try to obtain 3 requests at once
|
39
|
+
quota = limiter.obtain(3).times do
|
40
|
+
make_request
|
41
|
+
end
|
42
|
+
|
43
|
+
if quota == 0
|
44
|
+
# no more requests allowed within interval
|
45
|
+
sleep 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
```
|
50
|
+
|
51
|
+
### Multiple counters
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
pool = ConnectionPool.new(size: 10) do
|
55
|
+
Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
|
56
|
+
end
|
57
|
+
|
58
|
+
LIMITS = [
|
59
|
+
{ interval: 1, threshold: 2 }, # no more 2 requests per second
|
60
|
+
{ interval: 60, threshold: 200 }, # no more 200 requests per minute
|
61
|
+
{ interval: 86400, threshold: 10000 } # no more 10k requests per day
|
62
|
+
]
|
63
|
+
|
64
|
+
limiter = Rapidity::Composer.new(pool, name: 'requests', limits: LIMITS)
|
65
|
+
|
66
|
+
loop do
|
67
|
+
# try to obtain 3 requests at once
|
68
|
+
quota = limiter.obtain(3).times do
|
69
|
+
make_request
|
70
|
+
end
|
71
|
+
|
72
|
+
if quota == 0
|
73
|
+
# no more requests allowed within interval
|
74
|
+
puts limiter.remains # inspect current limits
|
75
|
+
sleep 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
```
|
80
|
+
|
81
|
+
## Installation
|
82
|
+
|
83
|
+
It's a gem:
|
84
|
+
```bash
|
85
|
+
gem install rapidity
|
86
|
+
```
|
87
|
+
There's also the wonders of [the Gemfile](http://bundler.io):
|
88
|
+
```ruby
|
89
|
+
gem 'rapidity'
|
90
|
+
```
|
91
|
+
|
92
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
-- args: key, treshold, interval, count
|
2
|
+
-- returns: obtained count.
|
3
|
+
|
4
|
+
-- make some nicer looking variable names:
|
5
|
+
local retval = nil
|
6
|
+
|
7
|
+
-- Redis documentation recommends passing the keys separately so that Redis
|
8
|
+
-- can - in the future - verify that they live on the same shard of a cluster, and
|
9
|
+
-- raise an error if they are not. As far as can be understood this functionality is not
|
10
|
+
-- yet present, but if we can make a little effort to make ourselves more future proof
|
11
|
+
-- we should.
|
12
|
+
local key = KEYS[1]
|
13
|
+
local treshold = tonumber(ARGV[1])
|
14
|
+
local interval = tonumber(ARGV[2])
|
15
|
+
local count = tonumber(ARGV[3])
|
16
|
+
|
17
|
+
local current = 0
|
18
|
+
local to_return = 0
|
19
|
+
|
20
|
+
redis.call("SET", key, treshold, "EX", interval, "NX")
|
21
|
+
current = redis.call("DECRBY", key, count)
|
22
|
+
|
23
|
+
-- If we became below zero we must return some value back
|
24
|
+
if current < 0 then
|
25
|
+
to_return = math.min(count, math.abs(current))
|
26
|
+
|
27
|
+
-- set 0 to current counter value
|
28
|
+
redis.call("SET", key, 0, 'KEEPTTL')
|
29
|
+
|
30
|
+
-- return obtained part of requested count
|
31
|
+
retval = count - to_return
|
32
|
+
else
|
33
|
+
-- return full of requested count
|
34
|
+
retval = count
|
35
|
+
end
|
36
|
+
|
37
|
+
return retval
|
data/lib/rapidity/limiter.rb
CHANGED
@@ -6,6 +6,7 @@ module Rapidity
|
|
6
6
|
|
7
7
|
attr_reader :pool, :name, :interval, :threshold, :namespace
|
8
8
|
|
9
|
+
LUA_SCRIPT_CODE = File.read(File.join(__dir__, 'limiter.lua'))
|
9
10
|
|
10
11
|
# Convert message to given class
|
11
12
|
# @params pool - inititalized Redis pool
|
@@ -30,12 +31,12 @@ module Rapidity
|
|
30
31
|
# @return remaining counter value
|
31
32
|
def remains
|
32
33
|
results = @pool.with do |conn|
|
33
|
-
conn.multi do
|
34
|
-
|
35
|
-
|
34
|
+
conn.multi do |pipeline|
|
35
|
+
pipeline.set(key('remains'), threshold, ex: interval, nx: true)
|
36
|
+
pipeline.get(key('remains'))
|
36
37
|
end
|
37
38
|
end
|
38
|
-
results[1].to_i #=>
|
39
|
+
results[1].to_i #=> pipeline.get(key('remains'))
|
39
40
|
end
|
40
41
|
|
41
42
|
# Obtain values from counter
|
@@ -43,38 +44,41 @@ module Rapidity
|
|
43
44
|
def obtain(count = 5)
|
44
45
|
count = count.abs
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
conn.
|
49
|
-
|
47
|
+
result = begin
|
48
|
+
@pool.with do |conn|
|
49
|
+
conn.evalsha(@script, keys: [key('remains')], argv: [threshold, interval, count])
|
50
|
+
end
|
51
|
+
rescue Redis::CommandError => e
|
52
|
+
if e.message.include?('NOSCRIPT')
|
53
|
+
# The Redis server has never seen this script before. Needs to run only once in the entire lifetime
|
54
|
+
# of the Redis server, until the script changes - in which case it will be loaded under a different SHA
|
55
|
+
ensure_script_loaded
|
56
|
+
retry
|
57
|
+
else
|
58
|
+
raise e
|
50
59
|
end
|
51
60
|
end
|
52
61
|
|
53
|
-
taken =
|
62
|
+
taken = result.to_i
|
54
63
|
|
55
|
-
if taken
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
results = @pool.with do |conn|
|
60
|
-
conn.multi do
|
61
|
-
conn.set(key('remains'), threshold - to_return, ex: interval, nx: true)
|
62
|
-
conn.incrby(key('remains'), to_return)
|
63
|
-
conn.ttl(key('remains'))
|
64
|
-
end
|
64
|
+
if taken == 0
|
65
|
+
ttl = @pool.with do |conn|
|
66
|
+
conn.ttl(key('remains'))
|
65
67
|
end
|
66
68
|
|
67
|
-
ttl
|
68
|
-
|
69
|
-
# reset if no ttl present
|
70
|
-
if ttl == -1
|
69
|
+
# UNKNOWN BUG? reset if no ttl present. Many years ago once upon time we meet our key without TTL
|
70
|
+
if ttl == -1
|
71
71
|
STDERR.puts "ERROR[#{Time.now}]: TTL for key #{key('remains').inspect} disappeared!"
|
72
|
-
@pool.with {|c| c.expire(key('remains'), interval) }
|
72
|
+
@pool.with {|c| c.expire(key('remains'), interval) }
|
73
73
|
end
|
74
|
+
end
|
75
|
+
|
76
|
+
taken
|
77
|
+
end
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
|
79
|
+
def ensure_script_loaded
|
80
|
+
@script = @pool.with do |conn|
|
81
|
+
conn.script(:load, LUA_SCRIPT_CODE)
|
78
82
|
end
|
79
83
|
end
|
80
84
|
|
data/lib/rapidity/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rapidity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5.88564
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yurusov Vlad
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-06-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -232,8 +232,10 @@ files:
|
|
232
232
|
- Gemfile
|
233
233
|
- Gemfile.lock
|
234
234
|
- LICENSE
|
235
|
+
- README.md
|
235
236
|
- lib/rapidity.rb
|
236
237
|
- lib/rapidity/composer.rb
|
238
|
+
- lib/rapidity/limiter.lua
|
237
239
|
- lib/rapidity/limiter.rb
|
238
240
|
- lib/rapidity/version.rb
|
239
241
|
homepage:
|