ratelimit 1.0.4 → 1.1.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
2
  SHA256:
3
- metadata.gz: 0b05cd16072c91b5a26037a5455e7ae1d5952eea22e0b17187a3e25be4623728
4
- data.tar.gz: b57fdeba66adbe85f1303ff5c4bfedead5bc6ce5d7b1d4d3608da1ebc847bb5c
3
+ metadata.gz: cb39d9fac25f51e2a7a95957ce738e281898774a296974b05cfa57b0ea858462
4
+ data.tar.gz: 6c2026639b39ebfec84029f00a4a56a78299378a8fbece227806b80a96f5058e
5
5
  SHA512:
6
- metadata.gz: ef451adc48b2e6248231e43a633544bcd823013059a0736d4fb284e1b4a3d8ab002cec7ebae9b9c26e1aad5ffa192a35c3f7a623b1fd438a20ed4526a8d0d989
7
- data.tar.gz: bae41807e4b35c2050353d4bdebeb0ba35b0408464901682b86e6870f6c1744162773287fcca96fc35ca2b80daa0db8a844db9f4e7ee16b5cbe46d8eb28ee36f
6
+ metadata.gz: 542f0e876ea9f96646b7e2241988af030f1a8c54a02710559a31433bf8f8f3f99d8efd3e589cc35a5780f7ba4f426d5b6227c65b04ed5b5f48d6541e598deeec
7
+ data.tar.gz: 7718a195d5afdbdf94339ca65ff7341d992f2f7d56d55ed70c8b8b450509db85db029c14e7cadfd4a41e0bcf6adf835590f424fd79750f66e6d8ec18bf033d52
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ ruby-version:
13
+ - head
14
+ - '3.2'
15
+ - '3.1'
16
+ - '3.0'
17
+ - '2.7'
18
+ - '2.6'
19
+ - '2.5'
20
+ - '2.4'
21
+ - '2.3'
22
+ steps:
23
+ - uses: actions/checkout@v3
24
+ - name: Set up Ruby ${{ matrix.ruby-version }}
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby-version }}
28
+ bundler-cache: true # 'bundle install' and cache
29
+ - name: Run tests
30
+ run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 1.1.0
2
+
3
+ * #45 fix inaccurate rate limit count
4
+
1
5
  # 1.0.4
2
6
 
3
7
  * #41 Remove redis deprecation warning
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Ratelimit: Slow your roll
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/ejfinneran/ratelimit.svg?branch=master)](http://travis-ci.org/ejfinneran/ratelimit)
4
- [![Code Climate](https://img.shields.io/codeclimate/github/ejfinneran/ratelimit.svg)](https://codeclimate.com/github/ejfinneran/ratelimit)
3
+ [![Code Climate](https://codeclimate.com/github/ejfinneran/ratelimit.png)](https://codeclimate.com/github/ejfinneran/ratelimit)
5
4
  [![Coverage Status](https://img.shields.io/coveralls/ejfinneran/ratelimit.svg)](https://coveralls.io/r/ejfinneran/ratelimit)
6
5
 
7
6
  Ratelimit provides a way to rate limit actions across multiple servers using Redis. This is a port of RateLimit.js found [here](https://github.com/chriso/redback/blob/master/lib/advanced_structures/RateLimit.js) and inspired by [this post](https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da).
@@ -1,3 +1,3 @@
1
1
  class Ratelimit
2
- VERSION = "1.0.4"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/ratelimit.rb CHANGED
@@ -2,6 +2,35 @@ require 'redis'
2
2
  require 'redis-namespace'
3
3
 
4
4
  class Ratelimit
5
+ COUNT_LUA_SCRIPT = <<-LUA.freeze
6
+ local subject = KEYS[1]
7
+ local oldest_bucket = tonumber(ARGV[1])
8
+ local current_bucket = tonumber(ARGV[2])
9
+ local count = 0
10
+
11
+ for bucket = oldest_bucket + 1, current_bucket do
12
+ local value = redis.call('HGET', subject, tostring(bucket))
13
+ if value then
14
+ count = count + tonumber(value)
15
+ end
16
+ end
17
+
18
+ return count
19
+ LUA
20
+
21
+ MAINTENANCE_LUA_SCRIPT = <<-LUA.freeze
22
+ local subject = KEYS[1]
23
+ local oldest_bucket = tonumber(ARGV[1])
24
+
25
+ -- Delete expired keys
26
+ local all_keys = redis.call('HKEYS', subject)
27
+ for _, key in ipairs(all_keys) do
28
+ local bucket_key = tonumber(key)
29
+ if bucket_key < oldest_bucket then
30
+ redis.call('HDEL', subject, tostring(bucket_key))
31
+ end
32
+ end
33
+ LUA
5
34
 
6
35
  # Create a Ratelimit object.
7
36
  #
@@ -30,6 +59,7 @@ class Ratelimit
30
59
  raise ArgumentError.new("Cannot have less than 3 buckets")
31
60
  end
32
61
  @raw_redis = options[:redis]
62
+ load_scripts
33
63
  end
34
64
 
35
65
  # Add to the counter for a given subject.
@@ -41,11 +71,13 @@ class Ratelimit
41
71
  def add(subject, count = 1)
42
72
  bucket = get_bucket
43
73
  subject = "#{@key}:#{subject}"
74
+
75
+ # Cleanup expired keys every 100th request
76
+ cleanup_expired_keys(subject) if rand < 0.01
77
+
44
78
  redis.multi do |transaction|
45
79
  transaction.hincrby(subject, bucket, count)
46
- transaction.hdel(subject, (bucket + 1) % @bucket_count)
47
- transaction.hdel(subject, (bucket + 2) % @bucket_count)
48
- transaction.expire(subject, @bucket_expiry)
80
+ transaction.expire(subject, @bucket_expiry + @bucket_interval)
49
81
  end.first
50
82
  end
51
83
 
@@ -54,15 +86,12 @@ class Ratelimit
54
86
  # @param [String] subject Subject for the count
55
87
  # @param [Integer] interval How far back (in seconds) to retrieve activity.
56
88
  def count(subject, interval)
57
- bucket = get_bucket
58
89
  interval = [[interval, @bucket_interval].max, @bucket_span].min
59
- count = (interval / @bucket_interval).floor
90
+ oldest_bucket = get_bucket(Time.now.to_i - interval)
91
+ current_bucket = get_bucket
60
92
  subject = "#{@key}:#{subject}"
61
93
 
62
- keys = (0..count - 1).map do |i|
63
- (bucket - i) % @bucket_count
64
- end
65
- return redis.hmget(subject, *keys).inject(0) {|a, i| a + i.to_i}
94
+ execute_script(@count_script_sha, [subject], [oldest_bucket, current_bucket])
66
95
  end
67
96
 
68
97
  # Check if the rate limit has been exceeded.
@@ -111,7 +140,30 @@ class Ratelimit
111
140
  private
112
141
 
113
142
  def get_bucket(time = Time.now.to_i)
114
- ((time % @bucket_span) / @bucket_interval).floor
143
+ (time / @bucket_interval).floor
144
+ end
145
+
146
+ # Cleanup expired keys for a given subject
147
+ def cleanup_expired_keys(subject)
148
+ oldest_bucket = get_bucket(Time.now.to_i - @bucket_expiry)
149
+ execute_script(@maintenance_script_sha, [subject], [oldest_bucket])
150
+ end
151
+
152
+ # Execute the script or reload the scripts on error
153
+ def execute_script(*args)
154
+ redis.evalsha(*args)
155
+ rescue Redis::CommandError => e
156
+ raise unless e.message =~ /NOSCRIPT/
157
+
158
+ load_scripts
159
+ retry
160
+ end
161
+
162
+ # Load the lua scripts into redis
163
+ # This must be on the redis.redis object, not the namespace
164
+ def load_scripts
165
+ @count_script_sha = redis.redis.script(:load, COUNT_LUA_SCRIPT)
166
+ @maintenance_script_sha = redis.redis.script(:load, MAINTENANCE_LUA_SCRIPT)
115
167
  end
116
168
 
117
169
  def redis
data/ratelimit.gemspec CHANGED
@@ -22,7 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "redis-namespace", ">= 1.0.0"
23
23
  spec.add_development_dependency "bundler", ">= 1.6"
24
24
  spec.add_development_dependency "rake"
25
- spec.add_development_dependency "fakeredis"
26
25
  spec.add_development_dependency "timecop"
27
26
  spec.add_development_dependency "rspec"
28
27
  spec.add_development_dependency "yard"
@@ -14,6 +14,7 @@ describe Ratelimit do
14
14
  let(:options) { super().merge(redis: redis) }
15
15
 
16
16
  it 'wraps redis in redis-namespace' do
17
+ expect(redis).to receive(:script).with(:load, anything).twice
17
18
  expect(subject.send(:redis)).to be_instance_of(Redis::Namespace)
18
19
  end
19
20
  end
@@ -117,7 +118,6 @@ describe Ratelimit do
117
118
  expect(@value).to be 1
118
119
  end
119
120
 
120
-
121
121
  it "counts correctly if bucket_span equals count-interval " do
122
122
  @r = Ratelimit.new("key", {:bucket_span => 10, bucket_interval: 1})
123
123
  @r.add('value1')
data/spec/spec_helper.rb CHANGED
@@ -16,7 +16,6 @@
16
16
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
17
  require 'coveralls'
18
18
  Coveralls.wear!
19
- require 'fakeredis'
20
19
  require 'timecop'
21
20
  require 'ratelimit'
22
21
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratelimit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - E.J. Finneran
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-09 00:00:00.000000000 Z
11
+ date: 2024-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: fakeredis
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: timecop
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -159,11 +145,11 @@ extensions: []
159
145
  extra_rdoc_files: []
160
146
  files:
161
147
  - ".document"
148
+ - ".github/workflows/ci.yml"
162
149
  - ".gitignore"
163
150
  - ".rspec"
164
151
  - ".ruby-gemset"
165
152
  - ".ruby-version"
166
- - ".travis.yml"
167
153
  - CHANGELOG.md
168
154
  - Gemfile
169
155
  - LICENSE.txt
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- script: "bundle exec rspec"
2
- rvm:
3
- - 2.1.10
4
- - 2.2
5
- - 2.3
6
- - 2.4.0
7
- - jruby-9.0.5.0