rapidity 0.0.4.64534 → 0.0.6.88566

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: f035f3525eab3c929a835208cd275212935d1052df051234b46a5240517d3c25
4
+ data.tar.gz: 129798e61b0e40d402d18d1d5d80d2e2f1f05e99c53de2c0bec240774d0fda07
5
5
  SHA512:
6
- metadata.gz: ba8b36082e31aadc9e083d6974da24d42ecf91253cf96a9fab6a31ca1d57665662e53bca693754edf90adc6f581314f831173a9a365d90352a1d6e7a6ebf4d1c
7
- data.tar.gz: 7144440be367435bb07ca46820f4e6f2256772c0ffb3c448f49376a868156a88cff4fd9add2d734d6b156944c5fe25e97cf69c20fbce3adb2612b94b72609150
6
+ metadata.gz: eb39b54f19ee6d1bc89592aa9a03012c2e274452a72ff4e6bafdb629b614aa8401e5fbc1626f108b30d98f46e6b9a7bc498f2612cfa9cd0fd593d9c710e8e750
7
+ data.tar.gz: 0043036ac0b0451f1a694f565b2149ecf88b9219a7997beb6a7ec9aa3e4b14be800c1e7e676747a4a3ec93ef21ce69fbba8110ea8f8a6ccedd28a61ce3374c7e
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.6.88566)
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,98 @@
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
+ - free from race condition through LUA scripting
18
+ - fast
19
+
20
+ [Article(russian) about gem.](https://blog.rnds.pro/029-rapidity/?utm_source=github&utm_medium=repo&utm_campaign=rnds)
21
+
22
+ ## Usage
23
+
24
+ Rapidity has two variants:
25
+
26
+ - simple `Rapidity::Limiter` to handle single distibuted counter
27
+ - complex `Rapidity::Composer` to handle multiple counters at once
28
+
29
+ ### Single conter with concurrent access
30
+
31
+ ```ruby
32
+ pool = ConnectionPool.new(size: 10) do
33
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
34
+ end
35
+
36
+ # allow no more 10 requests within 5 seconds
37
+ limiter = Rapidity::Limiter.new(pool, name: 'requests', threshold: 10, interval: 5)
38
+
39
+ loop do
40
+ # try to obtain 3 requests at once
41
+ quota = limiter.obtain(3).times do
42
+ make_request
43
+ end
44
+
45
+ if quota == 0
46
+ # no more requests allowed within interval
47
+ sleep 1
48
+ end
49
+ end
50
+
51
+ ```
52
+
53
+ ### Multiple counters
54
+
55
+ ```ruby
56
+ pool = ConnectionPool.new(size: 10) do
57
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'))
58
+ end
59
+
60
+ LIMITS = [
61
+ { interval: 1, threshold: 2 }, # no more 2 requests per second
62
+ { interval: 60, threshold: 200 }, # no more 200 requests per minute
63
+ { interval: 86400, threshold: 10000 } # no more 10k requests per day
64
+ ]
65
+
66
+ limiter = Rapidity::Composer.new(pool, name: 'requests', limits: LIMITS)
67
+
68
+ loop do
69
+ # try to obtain 3 requests at once
70
+ quota = limiter.obtain(3).times do
71
+ make_request
72
+ end
73
+
74
+ if quota == 0
75
+ # no more requests allowed within interval
76
+ puts limiter.remains # inspect current limits
77
+ sleep 1
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Installation
83
+
84
+ It's a gem:
85
+
86
+ ```bash
87
+ gem install rapidity
88
+ ```
89
+
90
+ There's also the wonders of [the Gemfile](http://bundler.io):
91
+
92
+ ```ruby
93
+ gem 'rapidity'
94
+ ```
95
+
96
+ ## Special Thanks
97
+
98
+ - [WeTransfer/prorate](https://github.com/WeTransfer/prorate) for LUA-examples
@@ -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.6'.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.6.88566
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: