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 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: