gcra 1.0.1 → 1.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
- SHA1:
3
- metadata.gz: d9436b4bda7bf469a2a3cf08df00716039ba14ef
4
- data.tar.gz: d3b31ffe07a84f62310acb0724ee7401be104ab1
2
+ SHA256:
3
+ metadata.gz: b02bda1b59d67535c5f7e098f0553e7d79dab0baed8266668e4533eaf083fdd8
4
+ data.tar.gz: 12f92e851d067cc367180e9c4528f7564ffc487ddd62273fecd6450fbb8de894
5
5
  SHA512:
6
- metadata.gz: f50866ecc292bb86a613e421b4c095a8e1a7afe452c292c63242e3e912b99744266e2f8621ca087cdb85619d6fc20772a66fbf3065082e440c4dd452540c210e
7
- data.tar.gz: d820ff9d540b75d471392e3a56dcab43b700c21a05ca60ec317926b945a67ce45fa905159385431432d153692127063393169aed5d693f32fef4e9c3ef630d58
6
+ metadata.gz: 993dfc9876b0b5f92e7456a20f1e9da2875505f41457540d7e8a41ea59f2e0080880ad96bc6e7b1751dbea67c60b4b416a88099350a5435c60b467f90f42e1fa
7
+ data.tar.gz: cdc1e40167c88d8a3749a8176c2683520973e844063ed3035e4b9be8533c1c07af7d8119ed8eec6a24e3c918850f914a55394784e3db06d8d9c0e91e5bb3eed0
@@ -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
@@ -9,27 +9,33 @@ module GCRA
9
9
  if v ~= ARGV[1] then
10
10
  return 0
11
11
  end
12
- if ARGV[3] ~= "0" then
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
- def initialize(redis, key_prefix)
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.time # returns tuple (seconds since epoch, microseconds)
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). The operations are not performed atomically.
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
- did_set = @redis.setnx(full_key, value)
45
-
46
- if did_set && ttl_nano > 0
47
- ttl_milli = ttl_nano / 1_000_000
48
- @redis.pexpire(full_key, ttl_milli)
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 / 1_000_000
61
- swapped = @redis.eval(CAS_SCRIPT, keys: [full_key], argv: [old_value, new_value, ttl_milli])
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
@@ -1,3 +1,3 @@
1
1
  module GCRA
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
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.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Frister
8
- autorequire:
8
+ - Tobias Schoknecht
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2017-02-08 00:00:00.000000000 Z
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
- - michael.frister@barzahlen.de
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
- rubyforge_project:
71
- rubygems_version: 2.2.2
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.