redis-gcra 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +45 -9
- data/lib/redis-gcra.rb +25 -19
- data/lib/redis-gcra/version.rb +1 -1
- data/vendor/inspect_gcra_ratelimit.lua +45 -0
- data/vendor/perform_gcra_ratelimit.lua +4 -6
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '085c8c8eb0a8f54f613cec4551f777bf0ad3e49d'
|
4
|
+
data.tar.gz: fad812c225a86dd55234fd51e68911f6099f3c79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8458a2915d214ea6f8c9daa247aa92ed25e46e61a42c4d274969970efd1fda3b7fa109f86afb4fcbe2c4bde1d6f6f1839ef0568324a1a56ca76010badcd79fb9
|
7
|
+
data.tar.gz: 75058e691d72f31eb6a147e157b112fd4968dff4d9897fae3a5a8bda6655c5adf44f51bfebedc5456e6fe8f6327ac8925f2a07fe6012eef623c5c8d120c13a41
|
data/README.md
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
[](https://travis-ci.org/rwz/redis-gcra)
|
3
3
|
|
4
4
|
This gem is an implementation of GCRA for rate limiting based on Redis. The
|
5
|
-
code requires Redis version 3.2
|
6
|
-
[`
|
5
|
+
code requires Redis version 3.2 or newer since it relies on
|
6
|
+
[`replicate_commands`][redis-replicate-commands] feature.
|
7
7
|
|
8
8
|
[redis-replicate-commands]: https://redis.io/commands/eval#replicating-commands-instead-of-scripts
|
9
|
+
|
9
10
|
## Installation
|
10
11
|
|
11
12
|
```ruby
|
@@ -22,32 +23,67 @@ Or install it yourself as:
|
|
22
23
|
|
23
24
|
## Usage
|
24
25
|
|
26
|
+
In order to perform rate limiting, you need to call the `limit` method.
|
27
|
+
|
28
|
+
In this example the rate limit bucket has 1000 tokens in it and recovers at
|
29
|
+
speed of 100 tokens per minute.
|
30
|
+
|
25
31
|
```ruby
|
26
32
|
redis = Redis.new
|
27
33
|
|
28
34
|
result = RedisGCRA.limit(
|
29
35
|
redis: redis,
|
30
|
-
key: "
|
36
|
+
key: "overall-account/bob@example.com",
|
31
37
|
burst: 1000,
|
32
38
|
rate: 100,
|
33
|
-
period: 60,
|
39
|
+
period: 60, # seconds
|
34
40
|
cost: 2
|
35
41
|
)
|
36
42
|
|
37
43
|
result.limited? # => false - request should not be limited
|
38
44
|
result.remaning # => 998 - remaining number of requests until limited
|
39
45
|
result.retry_after # => nil - can retry without delay
|
40
|
-
result.reset_after # => ~0.6 - in 0.
|
46
|
+
result.reset_after # => ~0.6 - in 0.6 seconds rate limiter will completely reset
|
41
47
|
|
42
|
-
#
|
48
|
+
# call limit 499 more times in rapid succession and you get:
|
43
49
|
|
44
50
|
result.limited? # => true - request should be limited
|
45
51
|
result.remaining # => 0 - no requests can be made at this point
|
46
|
-
result.retry_after # => ~1.1 - can retry in 1.
|
47
|
-
result.reset_after # => ~600 - in
|
52
|
+
result.retry_after # => ~1.1 - can retry in 1.1 seconds
|
53
|
+
result.reset_after # => ~600 - in 600 seconds rate limiter will completely reset
|
54
|
+
```
|
55
|
+
|
56
|
+
The implementation utilizes single key in Redis that matches the key you pass
|
57
|
+
to the `limit` method. If you need to reset rate limiter for particular key,
|
58
|
+
just delete the key from Redis:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Let's imagine `overall-account/bob@example.com` is limited.
|
62
|
+
# This will effectively reset limit for the key:
|
63
|
+
redis.del "overall-account/bob@example.com"
|
64
|
+
```
|
65
|
+
|
66
|
+
You call also retrieve the current state of rate limiter for particular key
|
67
|
+
without actually modifying the state. Of order to do that, use the `peek`
|
68
|
+
method:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
RedisGCRA.peek(
|
72
|
+
redis: redis,
|
73
|
+
key: "overall-account/bob@example.com",
|
74
|
+
burst: 1000,
|
75
|
+
rate: 100,
|
76
|
+
period: 60 # seconds
|
77
|
+
)
|
78
|
+
|
79
|
+
result.limited? # => true - current state is limited
|
80
|
+
result.remaining # => 0 - no requests can be made
|
81
|
+
result.retry_after # => nil - peek always return nil here
|
82
|
+
result.reset_after # => ~600 - in 600 seconds rate limiter will completely reset
|
48
83
|
```
|
49
84
|
|
50
85
|
## License
|
51
86
|
|
52
|
-
The gem is available as open source under the terms of the [MIT
|
87
|
+
The gem is available as open source under the terms of the [MIT
|
88
|
+
License](http://opensource.org/licenses/MIT).
|
53
89
|
|
data/lib/redis-gcra.rb
CHANGED
@@ -6,22 +6,29 @@ module RedisGCRA
|
|
6
6
|
autoload :Result, "redis-gcra/result"
|
7
7
|
|
8
8
|
def limit(redis:, key:, burst:, rate:, period:, cost: 1)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
call redis, :perform_gcra_ratelimit, key, burst, rate, period, cost
|
10
|
+
end
|
11
|
+
|
12
|
+
def peek(redis:, key:, burst:, rate:, period:)
|
13
|
+
call redis, :inspect_gcra_ratelimit, key, burst, rate, period
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def call(redis, script_name, key, *argv)
|
19
|
+
res = call_script(redis, script_name, keys: [key], argv: argv)
|
15
20
|
|
16
21
|
Result.new(
|
17
|
-
limited:
|
18
|
-
remaining:
|
19
|
-
retry_after:
|
20
|
-
reset_after:
|
22
|
+
limited: res[0] == 1,
|
23
|
+
remaining: res[1],
|
24
|
+
retry_after: parse_float_string(res[2]),
|
25
|
+
reset_after: parse_float_string(res[3])
|
21
26
|
)
|
22
27
|
end
|
23
28
|
|
24
|
-
|
29
|
+
def parse_float_string(value)
|
30
|
+
value == "-1" ? nil : value.to_f
|
31
|
+
end
|
25
32
|
|
26
33
|
def call_script(redis, script_name, *args)
|
27
34
|
script_sha = mutex.synchronize { get_cached_sha(redis, script_name) }
|
@@ -37,13 +44,12 @@ module RedisGCRA
|
|
37
44
|
end
|
38
45
|
|
39
46
|
def get_cached_sha(redis, script_name)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
sha
|
47
|
+
cache_key = "#{redis.id}/#{script_name}"
|
48
|
+
redis_cache[cache_key] ||= load_script(redis, script_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_script(redis, script_name)
|
52
|
+
script_path = File.expand_path("../../vendor/#{script_name}.lua", __FILE__)
|
53
|
+
redis.script(:load, File.read(script_path))
|
48
54
|
end
|
49
55
|
end
|
data/lib/redis-gcra/version.rb
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
local rate_limit_key = KEYS[1]
|
2
|
+
local burst = ARGV[1]
|
3
|
+
local rate = ARGV[2]
|
4
|
+
local period = ARGV[3]
|
5
|
+
|
6
|
+
local emission_interval = period / rate
|
7
|
+
local burst_offset = emission_interval * burst
|
8
|
+
local now = redis.call("TIME")
|
9
|
+
|
10
|
+
-- redis returns time as an array containing two integers: seconds of the epoch
|
11
|
+
-- time and microseconds. for convenience we need to convert them to float
|
12
|
+
-- point number
|
13
|
+
now = now[1] + now[2] / 1000000
|
14
|
+
|
15
|
+
local tat = redis.call("GET", rate_limit_key)
|
16
|
+
|
17
|
+
if not tat then
|
18
|
+
tat = now
|
19
|
+
else
|
20
|
+
tat = tonumber(tat)
|
21
|
+
end
|
22
|
+
|
23
|
+
local allow_at = math.max(tat, now) - burst_offset
|
24
|
+
local diff = now - allow_at
|
25
|
+
|
26
|
+
local remaining = math.floor(diff / emission_interval)
|
27
|
+
|
28
|
+
local reset_after = tat - now
|
29
|
+
if reset_after == 0 then
|
30
|
+
reset_after = -1
|
31
|
+
end
|
32
|
+
|
33
|
+
local limited
|
34
|
+
|
35
|
+
if remaining == 0 then
|
36
|
+
limited = 1
|
37
|
+
else
|
38
|
+
limited = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
-- retry_after is always nil because it doesn't make much sense in inspect
|
42
|
+
-- context
|
43
|
+
local retry_after = -1
|
44
|
+
|
45
|
+
return {limited, remaining, tostring(retry_after), tostring(reset_after)}
|
@@ -41,13 +41,11 @@ if diff < 0 then
|
|
41
41
|
reset_after = tat - now
|
42
42
|
retry_after = diff * -1
|
43
43
|
else
|
44
|
-
local ttl = new_tat - now
|
45
|
-
redis.call("SET", rate_limit_key, new_tat, "EX", math.ceil(ttl))
|
46
|
-
local next_in = burst_offset - ttl
|
47
|
-
remaining = math.floor(next_in / emission_interval)
|
48
|
-
reset_after = ttl
|
49
|
-
retry_after = -1
|
50
44
|
limited = 0
|
45
|
+
reset_after = new_tat - now
|
46
|
+
redis.call("SET", rate_limit_key, new_tat, "EX", math.ceil(reset_after))
|
47
|
+
remaining = math.floor(diff / emission_interval)
|
48
|
+
retry_after = -1
|
51
49
|
end
|
52
50
|
|
53
51
|
return {limited, remaining, tostring(retry_after), tostring(reset_after)}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-gcra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pavel Pravosud
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- lib/redis-gcra.rb
|
37
37
|
- lib/redis-gcra/result.rb
|
38
38
|
- lib/redis-gcra/version.rb
|
39
|
+
- vendor/inspect_gcra_ratelimit.lua
|
39
40
|
- vendor/perform_gcra_ratelimit.lua
|
40
41
|
homepage: https://github.com/rwz/redis-gcra
|
41
42
|
licenses:
|