rapidity 0.0.4.64534 → 0.0.5.88564

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
  SHA256:
3
- metadata.gz: af052ec73bab58f49e7004d63d9dea9ab27247df2bb012d874c30a1c387a9d3f
4
- data.tar.gz: a5ff8b93f6ad5f12656a67ea9e938aea0ab4f06494ecab38bd5da234829eb2b4
3
+ metadata.gz: dc0bdcbad063ffce0142d3eacdad37e4ee78186ee4bb2092c1677c871fb52fa2
4
+ data.tar.gz: 15017d1bae8b36afc44d484616bc2fe5f21c85e868606c9dc137ad0f4c04a5f1
5
5
  SHA512:
6
- metadata.gz: ba8b36082e31aadc9e083d6974da24d42ecf91253cf96a9fab6a31ca1d57665662e53bca693754edf90adc6f581314f831173a9a365d90352a1d6e7a6ebf4d1c
7
- data.tar.gz: 7144440be367435bb07ca46820f4e6f2256772c0ffb3c448f49376a868156a88cff4fd9add2d734d6b156944c5fe25e97cf69c20fbce3adb2612b94b72609150
6
+ metadata.gz: 68967c5c216162141ddf9be4871c47f4db4966b07ad2e2faac5950ec1987244a445df52e41693b3fcb6a324d7d61b640c1293b139d671059047e84778a5df3b0
7
+ data.tar.gz: a97d793eb5b42c556ee971c1484f25b5910f8576ba280bd60ce845780f79cd63fdd015a5618568b04b4e9bda83000995e3dda096d920d7aca3c33e8ef5a7c129
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rapidity (0.0.4.64534)
4
+ rapidity (0.0.5.88564)
5
5
  activesupport
6
6
  connection_pool
7
7
  redis
@@ -58,7 +58,7 @@ GEM
58
58
  psych (3.3.2)
59
59
  public_suffix (4.0.6)
60
60
  rainbow (3.0.0)
61
- redis (4.5.0)
61
+ redis (4.7.0)
62
62
  reek (6.0.4)
63
63
  kwalify (~> 0.7.0)
64
64
  parser (~> 3.0.0)
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Rapidity
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rapidity.svg)](https://rubygems.org/gems/rapidity)
4
+ [![Gem](https://img.shields.io/gem/dt/rapidity.svg)](https://rubygems.org/gems/rapidity/versions)
5
+ [![YARD](https://badgen.net/badge/YARD/doc/blue)](http://www.rubydoc.info/gems/rapidity)
6
+
7
+ [![Coverage](https://lysander.rnds.pro/api/v1/badges/rapidity_coverage.svg)](https://lysander.rnds.pro/api/v1/badges/rapidity_coverage.html)
8
+ [![Quality](https://lysander.rnds.pro/api/v1/badges/rapidity_quality.svg)](https://lysander.rnds.pro/api/v1/badges/rapidity_quality.html)
9
+ [![Outdated](https://lysander.rnds.pro/api/v1/badges/rapidity_outdated.svg)](https://lysander.rnds.pro/api/v1/badges/rapidity_outdated.html)
10
+ [![Vulnerabilities](https://lysander.rnds.pro/api/v1/badges/rapidity_vulnerable.svg)](https://lysander.rnds.pro/api/v1/badges/rapidity_vulnerable.html)
11
+
12
+ Simple but fast Redis-backed distributed rate limiter. Allows you to specify time interval and count within to limit distributed operations.
13
+
14
+ Features:
15
+
16
+ - extremly simple
17
+ - safe
18
+ - fast
19
+
20
+ ## Usage
21
+
22
+ Rapidity has two variants:
23
+
24
+ - simple `Rapidity::Limiter` to handle single distibuted counter
25
+ - complex `Rapidity::Composer` to handle multiple counters at once
26
+
27
+ ### Single conter with concurrent access
28
+
29
+ ```ruby
30
+ pool = ConnectionPool.new(size: 10) do
31
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
32
+ end
33
+
34
+ # allow no more 10 requests within 5 seconds
35
+ limiter = Rapidity::Limiter.new(pool, name: 'requests', threshold: 10, interval: 5)
36
+
37
+ loop do
38
+ # try to obtain 3 requests at once
39
+ quota = limiter.obtain(3).times do
40
+ make_request
41
+ end
42
+
43
+ if quota == 0
44
+ # no more requests allowed within interval
45
+ sleep 1
46
+ end
47
+ end
48
+
49
+ ```
50
+
51
+ ### Multiple counters
52
+
53
+ ```ruby
54
+ pool = ConnectionPool.new(size: 10) do
55
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
56
+ end
57
+
58
+ LIMITS = [
59
+ { interval: 1, threshold: 2 }, # no more 2 requests per second
60
+ { interval: 60, threshold: 200 }, # no more 200 requests per minute
61
+ { interval: 86400, threshold: 10000 } # no more 10k requests per day
62
+ ]
63
+
64
+ limiter = Rapidity::Composer.new(pool, name: 'requests', limits: LIMITS)
65
+
66
+ loop do
67
+ # try to obtain 3 requests at once
68
+ quota = limiter.obtain(3).times do
69
+ make_request
70
+ end
71
+
72
+ if quota == 0
73
+ # no more requests allowed within interval
74
+ puts limiter.remains # inspect current limits
75
+ sleep 1
76
+ end
77
+ end
78
+
79
+ ```
80
+
81
+ ## Installation
82
+
83
+ It's a gem:
84
+ ```bash
85
+ gem install rapidity
86
+ ```
87
+ There's also the wonders of [the Gemfile](http://bundler.io):
88
+ ```ruby
89
+ gem 'rapidity'
90
+ ```
91
+
92
+
@@ -0,0 +1,37 @@
1
+ -- args: key, treshold, interval, count
2
+ -- returns: obtained count.
3
+
4
+ -- make some nicer looking variable names:
5
+ local retval = nil
6
+
7
+ -- Redis documentation recommends passing the keys separately so that Redis
8
+ -- can - in the future - verify that they live on the same shard of a cluster, and
9
+ -- raise an error if they are not. As far as can be understood this functionality is not
10
+ -- yet present, but if we can make a little effort to make ourselves more future proof
11
+ -- we should.
12
+ local key = KEYS[1]
13
+ local treshold = tonumber(ARGV[1])
14
+ local interval = tonumber(ARGV[2])
15
+ local count = tonumber(ARGV[3])
16
+
17
+ local current = 0
18
+ local to_return = 0
19
+
20
+ redis.call("SET", key, treshold, "EX", interval, "NX")
21
+ current = redis.call("DECRBY", key, count)
22
+
23
+ -- If we became below zero we must return some value back
24
+ if current < 0 then
25
+ to_return = math.min(count, math.abs(current))
26
+
27
+ -- set 0 to current counter value
28
+ redis.call("SET", key, 0, 'KEEPTTL')
29
+
30
+ -- return obtained part of requested count
31
+ retval = count - to_return
32
+ else
33
+ -- return full of requested count
34
+ retval = count
35
+ end
36
+
37
+ return retval
@@ -6,6 +6,7 @@ module Rapidity
6
6
 
7
7
  attr_reader :pool, :name, :interval, :threshold, :namespace
8
8
 
9
+ LUA_SCRIPT_CODE = File.read(File.join(__dir__, 'limiter.lua'))
9
10
 
10
11
  # Convert message to given class
11
12
  # @params pool - inititalized Redis pool
@@ -30,12 +31,12 @@ module Rapidity
30
31
  # @return remaining counter value
31
32
  def remains
32
33
  results = @pool.with do |conn|
33
- conn.multi do
34
- conn.set(key('remains'), threshold, ex: interval, nx: true)
35
- conn.get(key('remains'))
34
+ conn.multi do |pipeline|
35
+ pipeline.set(key('remains'), threshold, ex: interval, nx: true)
36
+ pipeline.get(key('remains'))
36
37
  end
37
38
  end
38
- results[1].to_i #=> conn.get(key('remains'))
39
+ results[1].to_i #=> pipeline.get(key('remains'))
39
40
  end
40
41
 
41
42
  # Obtain values from counter
@@ -43,38 +44,41 @@ module Rapidity
43
44
  def obtain(count = 5)
44
45
  count = count.abs
45
46
 
46
- results = @pool.with do |conn|
47
- conn.multi do
48
- conn.set(key('remains'), threshold, ex: interval, nx: true)
49
- conn.decrby(key('remains'), count)
47
+ result = begin
48
+ @pool.with do |conn|
49
+ conn.evalsha(@script, keys: [key('remains')], argv: [threshold, interval, count])
50
+ end
51
+ rescue Redis::CommandError => e
52
+ if e.message.include?('NOSCRIPT')
53
+ # The Redis server has never seen this script before. Needs to run only once in the entire lifetime
54
+ # of the Redis server, until the script changes - in which case it will be loaded under a different SHA
55
+ ensure_script_loaded
56
+ retry
57
+ else
58
+ raise e
50
59
  end
51
60
  end
52
61
 
53
- taken = results[1].to_i #=> conn.decrby(key('remains'), count)
62
+ taken = result.to_i
54
63
 
55
- if taken < 0
56
- overflow = taken.abs
57
- to_return = [count, overflow].min
58
-
59
- results = @pool.with do |conn|
60
- conn.multi do
61
- conn.set(key('remains'), threshold - to_return, ex: interval, nx: true)
62
- conn.incrby(key('remains'), to_return)
63
- conn.ttl(key('remains'))
64
- end
64
+ if taken == 0
65
+ ttl = @pool.with do |conn|
66
+ conn.ttl(key('remains'))
65
67
  end
66
68
 
67
- ttl = results[2].to_i #=> conn.ttl(key('remains'))
68
-
69
- # reset if no ttl present
70
- if ttl == -1
69
+ # UNKNOWN BUG? reset if no ttl present. Many years ago once upon time we meet our key without TTL
70
+ if ttl == -1
71
71
  STDERR.puts "ERROR[#{Time.now}]: TTL for key #{key('remains').inspect} disappeared!"
72
- @pool.with {|c| c.expire(key('remains'), interval) }
72
+ @pool.with {|c| c.expire(key('remains'), interval) }
73
73
  end
74
+ end
75
+
76
+ taken
77
+ end
74
78
 
75
- count - to_return
76
- else
77
- count
79
+ def ensure_script_loaded
80
+ @script = @pool.with do |conn|
81
+ conn.script(:load, LUA_SCRIPT_CODE)
78
82
  end
79
83
  end
80
84
 
@@ -1,6 +1,6 @@
1
1
  module Rapidity
2
2
 
3
- VERSION = '0.0.4'.freeze
3
+ VERSION = '0.0.5'.freeze
4
4
 
5
5
  end
6
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rapidity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4.64534
4
+ version: 0.0.5.88564
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yurusov Vlad
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-10-14 00:00:00.000000000 Z
12
+ date: 2022-06-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -232,8 +232,10 @@ files:
232
232
  - Gemfile
233
233
  - Gemfile.lock
234
234
  - LICENSE
235
+ - README.md
235
236
  - lib/rapidity.rb
236
237
  - lib/rapidity/composer.rb
238
+ - lib/rapidity/limiter.lua
237
239
  - lib/rapidity/limiter.rb
238
240
  - lib/rapidity/version.rb
239
241
  homepage: