redis-gcra 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1b80231f7f7cb343c629c565c27cf7b6d83cdef4
4
- data.tar.gz: cdcce949d903b7da612eb466baf683fbb4479bf7
3
+ metadata.gz: '085c8c8eb0a8f54f613cec4551f777bf0ad3e49d'
4
+ data.tar.gz: fad812c225a86dd55234fd51e68911f6099f3c79
5
5
  SHA512:
6
- metadata.gz: 275086f4ec7c050d699f61c35451cb01725adfc084de2bf813de97ae1df930a2dc39fb05ba137a4c34aca2197696e728636d38dee77e800f539f565a5779eda9
7
- data.tar.gz: 33ed550d170ccb96ba70ff1d05a3add5655ec06fa2b3ba946444c2832f1470148d11b0208d4beb89a84dbaa57ca237fa0e63c77a3202694c6f23d8e04b860d83
6
+ metadata.gz: 8458a2915d214ea6f8c9daa247aa92ed25e46e61a42c4d274969970efd1fda3b7fa109f86afb4fcbe2c4bde1d6f6f1839ef0568324a1a56ca76010badcd79fb9
7
+ data.tar.gz: 75058e691d72f31eb6a147e157b112fd4968dff4d9897fae3a5a8bda6655c5adf44f51bfebedc5456e6fe8f6327ac8925f2a07fe6012eef623c5c8d120c13a41
data/README.md CHANGED
@@ -2,10 +2,11 @@
2
2
  [![Build Status](https://travis-ci.org/rwz/redis-gcra.svg?branch=master)](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+ or newer since it relies on
6
- [`replicate_comands`][redis-replicate-commands] feature.
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: "rate-limit-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.6s rate limiter will completely reset
46
+ result.reset_after # => ~0.6 - in 0.6 seconds rate limiter will completely reset
41
47
 
42
- # do this 500 more times and then
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.1seconds
47
- result.reset_after # => ~600 - in 600s rate limiter will completely reset
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 License](http://opensource.org/licenses/MIT).
87
+ The gem is available as open source under the terms of the [MIT
88
+ License](http://opensource.org/licenses/MIT).
53
89
 
@@ -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
- resp = call_script(
10
- redis,
11
- :perform_gcra_ratelimit,
12
- keys: [key],
13
- argv: [burst, rate, period, cost]
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: resp[0] == 1,
18
- remaining: resp[1],
19
- retry_after: resp[2] == "-1" ? nil : resp[2].to_f,
20
- reset_after: resp[3].to_f
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
- private
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
- sha = redis_cache.dig(redis.id, script_name)
41
- return sha if sha
42
-
43
- script = File.read(File.expand_path("../../vendor/#{script_name}.lua", __FILE__))
44
- sha = redis.script(:load, script)
45
- redis_cache[redis.id] ||= {}
46
- redis_cache[redis.id][script_name] = sha
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
@@ -1,3 +1,3 @@
1
1
  module RedisGCRA
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
@@ -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.1.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: