gcra 1.0.1 → 1.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 +5 -5
- data/lib/gcra/rate_limiter.rb +26 -0
- data/lib/gcra/redis_store.rb +49 -18
- data/lib/gcra/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b02bda1b59d67535c5f7e098f0553e7d79dab0baed8266668e4533eaf083fdd8
|
4
|
+
data.tar.gz: 12f92e851d067cc367180e9c4528f7564ffc487ddd62273fecd6450fbb8de894
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 993dfc9876b0b5f92e7456a20f1e9da2875505f41457540d7e8a41ea59f2e0080880ad96bc6e7b1751dbea67c60b4b416a88099350a5435c60b467f90f42e1fa
|
7
|
+
data.tar.gz: cdc1e40167c88d8a3749a8176c2683520973e844063ed3035e4b9be8533c1c07af7d8119ed8eec6a24e3c918850f914a55394784e3db06d8d9c0e91e5bb3eed0
|
data/lib/gcra/rate_limiter.rb
CHANGED
@@ -23,6 +23,7 @@ module GCRA
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def limit(key, quantity)
|
26
|
+
key = key.to_s unless key.is_a?(String)
|
26
27
|
i = 0
|
27
28
|
|
28
29
|
while i < MAX_ATTEMPTS
|
@@ -96,5 +97,30 @@ module GCRA
|
|
96
97
|
"Failed to store updated rate limit data for key '#{key}' after #{MAX_ATTEMPTS} attempts"
|
97
98
|
)
|
98
99
|
end
|
100
|
+
|
101
|
+
# Overwrite the stored value for key to that of a bucket that has
|
102
|
+
# just overflowed, ignoring any existing stored data.
|
103
|
+
def mark_overflowed(key)
|
104
|
+
key = key.to_s unless key.is_a?(String)
|
105
|
+
i = 0
|
106
|
+
while i < MAX_ATTEMPTS
|
107
|
+
tat_from_store, now = @store.get_with_time(key)
|
108
|
+
new_value = now + @delay_variation_tolerance
|
109
|
+
ttl = @delay_variation_tolerance
|
110
|
+
updated = if tat_from_store.nil?
|
111
|
+
@store.set_if_not_exists_with_ttl(key, new_value, ttl)
|
112
|
+
else
|
113
|
+
@store.compare_and_set_with_ttl(key, tat_from_store, new_value, ttl)
|
114
|
+
end
|
115
|
+
if updated
|
116
|
+
return true
|
117
|
+
end
|
118
|
+
i += 1
|
119
|
+
end
|
120
|
+
|
121
|
+
raise StoreUpdateFailed.new(
|
122
|
+
"Failed to store updated rate limit data for key '#{key}' after #{MAX_ATTEMPTS} attempts"
|
123
|
+
)
|
124
|
+
end
|
99
125
|
end
|
100
126
|
end
|
data/lib/gcra/redis_store.rb
CHANGED
@@ -9,27 +9,33 @@ module GCRA
|
|
9
9
|
if v ~= ARGV[1] then
|
10
10
|
return 0
|
11
11
|
end
|
12
|
-
|
13
|
-
redis.call('psetex', KEYS[1], ARGV[3], ARGV[2])
|
14
|
-
else
|
15
|
-
redis.call('set', KEYS[1], ARGV[2])
|
16
|
-
end
|
12
|
+
redis.call('psetex', KEYS[1], ARGV[3], ARGV[2])
|
17
13
|
return 1
|
18
14
|
EOF
|
15
|
+
|
16
|
+
# Digest::SHA1.hexdigest(CAS_SCRIPT)
|
17
|
+
CAS_SHA = "89118e702230c0d65969c5fc557a6e942a2f4d31".freeze
|
19
18
|
CAS_SCRIPT_MISSING_KEY_RESPONSE = 'key does not exist'.freeze
|
19
|
+
SCRIPT_NOT_IN_CACHE_RESPONSE = 'NOSCRIPT No matching script. Please use EVAL.'.freeze
|
20
20
|
|
21
|
-
|
21
|
+
CONNECTED_TO_READONLY = "READONLY You can't write against a read only slave.".freeze
|
22
|
+
|
23
|
+
def initialize(redis, key_prefix, options = {})
|
22
24
|
@redis = redis
|
23
25
|
@key_prefix = key_prefix
|
26
|
+
|
27
|
+
@reconnect_on_readonly = options[:reconnect_on_readonly] || false
|
24
28
|
end
|
25
29
|
|
26
30
|
# Returns the value of the key or nil, if it isn't in the store.
|
27
31
|
# Also returns the time from the Redis server, with microsecond precision.
|
28
32
|
def get_with_time(key)
|
29
|
-
time_response = @redis.
|
33
|
+
time_response, value = @redis.pipelined do
|
34
|
+
@redis.time # returns tuple (seconds since epoch, microseconds)
|
35
|
+
@redis.get(@key_prefix + key)
|
36
|
+
end
|
30
37
|
# Convert tuple to nanoseconds
|
31
38
|
time = (time_response[0] * 1_000_000 + time_response[1]) * 1_000
|
32
|
-
value = @redis.get(@key_prefix + key)
|
33
39
|
if value != nil
|
34
40
|
value = value.to_i
|
35
41
|
end
|
@@ -38,17 +44,21 @@ module GCRA
|
|
38
44
|
end
|
39
45
|
|
40
46
|
# Set the value of key only if it is not already set. Return whether the value was set.
|
41
|
-
# Also set the key's expiration (ttl, in seconds).
|
47
|
+
# Also set the key's expiration (ttl, in seconds).
|
42
48
|
def set_if_not_exists_with_ttl(key, value, ttl_nano)
|
43
49
|
full_key = @key_prefix + key
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
retried = false
|
51
|
+
begin
|
52
|
+
ttl_milli = calculate_ttl_milli(ttl_nano)
|
53
|
+
@redis.set(full_key, value, nx: true, px: ttl_milli)
|
54
|
+
rescue Redis::CommandError => e
|
55
|
+
if e.message == CONNECTED_TO_READONLY && @reconnect_on_readonly && !retried
|
56
|
+
@redis.client.reconnect
|
57
|
+
retried = true
|
58
|
+
retry
|
59
|
+
end
|
60
|
+
raise
|
49
61
|
end
|
50
|
-
|
51
|
-
return did_set
|
52
62
|
end
|
53
63
|
|
54
64
|
# Atomically compare the value at key to the old value. If it matches, set it to the new value
|
@@ -56,17 +66,38 @@ module GCRA
|
|
56
66
|
# return false with no error. If the swap succeeds, update the ttl for the key atomically.
|
57
67
|
def compare_and_set_with_ttl(key, old_value, new_value, ttl_nano)
|
58
68
|
full_key = @key_prefix + key
|
69
|
+
retried = false
|
59
70
|
begin
|
60
|
-
ttl_milli = ttl_nano
|
61
|
-
swapped = @redis.
|
71
|
+
ttl_milli = calculate_ttl_milli(ttl_nano)
|
72
|
+
swapped = @redis.evalsha(CAS_SHA, keys: [full_key], argv: [old_value, new_value, ttl_milli])
|
62
73
|
rescue Redis::CommandError => e
|
63
74
|
if e.message == CAS_SCRIPT_MISSING_KEY_RESPONSE
|
64
75
|
return false
|
76
|
+
elsif e.message == SCRIPT_NOT_IN_CACHE_RESPONSE && !retried
|
77
|
+
@redis.script('load', CAS_SCRIPT)
|
78
|
+
retried = true
|
79
|
+
retry
|
80
|
+
elsif e.message == CONNECTED_TO_READONLY && @reconnect_on_readonly && !retried
|
81
|
+
@redis.client.reconnect
|
82
|
+
retried = true
|
83
|
+
retry
|
65
84
|
end
|
66
85
|
raise
|
67
86
|
end
|
68
87
|
|
69
88
|
return swapped == 1
|
70
89
|
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def calculate_ttl_milli(ttl_nano)
|
94
|
+
ttl_milli = ttl_nano / 1_000_000
|
95
|
+
# Setting 0 as expiration/ttl would result in an error.
|
96
|
+
# Therefore overwrite it and use 1
|
97
|
+
if ttl_milli == 0
|
98
|
+
return 1
|
99
|
+
end
|
100
|
+
return ttl_milli
|
101
|
+
end
|
71
102
|
end
|
72
103
|
end
|
data/lib/gcra/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gcra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Frister
|
8
|
-
|
8
|
+
- Tobias Schoknecht
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2020-10-18 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rspec
|
@@ -40,7 +41,7 @@ dependencies:
|
|
40
41
|
version: '3.3'
|
41
42
|
description: GCRA implementation for rate limiting
|
42
43
|
email:
|
43
|
-
-
|
44
|
+
- tobias.schoknecht@barzahlen.de
|
44
45
|
executables: []
|
45
46
|
extensions: []
|
46
47
|
extra_rdoc_files: []
|
@@ -48,11 +49,11 @@ files:
|
|
48
49
|
- lib/gcra/rate_limiter.rb
|
49
50
|
- lib/gcra/redis_store.rb
|
50
51
|
- lib/gcra/version.rb
|
51
|
-
homepage: https://github.com/Barzahlen/gcra
|
52
|
+
homepage: https://github.com/Barzahlen/gcra-ruby
|
52
53
|
licenses:
|
53
54
|
- MIT
|
54
55
|
metadata: {}
|
55
|
-
post_install_message:
|
56
|
+
post_install_message:
|
56
57
|
rdoc_options: []
|
57
58
|
require_paths:
|
58
59
|
- lib
|
@@ -67,9 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '0'
|
69
70
|
requirements: []
|
70
|
-
|
71
|
-
|
72
|
-
signing_key:
|
71
|
+
rubygems_version: 3.0.6
|
72
|
+
signing_key:
|
73
73
|
specification_version: 4
|
74
74
|
summary: Ruby implementation of a generic cell rate algorithm (GCRA), ported from
|
75
75
|
the Go implementation throttled.
|