ratelimit 1.0.3 → 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
- SHA1:
3
- metadata.gz: 3ca5c11e810378b9f764fc4df1a9eb95c5be682f
4
- data.tar.gz: 29be916842600285b975409daa377a6ef0a47d7b
2
+ SHA256:
3
+ metadata.gz: cb39d9fac25f51e2a7a95957ce738e281898774a296974b05cfa57b0ea858462
4
+ data.tar.gz: 6c2026639b39ebfec84029f00a4a56a78299378a8fbece227806b80a96f5058e
5
5
  SHA512:
6
- metadata.gz: a8bf4fdb9ccb0e298870c1cb6e19d44e90d9fb92d5f5f311dd7d68eff139150c38b1a7bfcd3175c0cff317b3d96fbe02c72f20a4acdf26243cd3c28f4a74d0b3
7
- data.tar.gz: fa1910970d4f93ec8e5c947012344a002bb0e8f7ed56b130fb8b454ae9ff373a140dd2e9e4865bd13772e51ba5485ca2448f0c2165f92d7549c41756d8935bbf
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,9 +1,25 @@
1
+ # 1.1.0
2
+
3
+ * #45 fix inaccurate rate limit count
4
+
5
+ # 1.0.4
6
+
7
+ * #41 Remove redis deprecation warning
8
+
9
+ # 1.0.3
10
+
11
+ * #28 Fix counting the same buckets multiple times when the interval > bucket_span
12
+
13
+ # 1.0.2 09/09/2014
14
+
15
+ * #15 Raise error if bucket_count too small
16
+
1
17
  # 1.0.1 08/05/2014
18
+
2
19
  * #14 fix issue with wrong count on bucket_span == count(..interval) - @olgen
3
20
 
4
21
  # 1.0.0 06/15/2014
5
22
 
6
-
7
23
  * #7 Allow adding more than one to a counter - @Nielsomat
8
24
  * #8 Add Ruby 2 support and remove Ruby 1.8 support
9
25
  * #6 Clean up initialization method
@@ -16,7 +32,6 @@
16
32
  * Allow non-string subject and key -- Thanks Alexey Noskov
17
33
  Fix for bucket_expiry allowed to be larger than bucket_span and causing bad results -- Thanks Alexey Noskov
18
34
 
19
-
20
35
  # 0.0.2 10/29/2011
21
36
 
22
37
  Initial Relase
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.3"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/ratelimit.rb CHANGED
@@ -2,8 +2,37 @@ 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
5
10
 
6
- # Create a RateLimit object.
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
34
+
35
+ # Create a Ratelimit object.
7
36
  #
8
37
  # @param [String] key A name to uniquely identify this rate limit. For example, 'emails'
9
38
  # @param [Hash] options Options hash
@@ -12,7 +41,7 @@ class Ratelimit
12
41
  # @option options [Integer] :bucket_expiry (@bucket_span) How long we keep data in each bucket before it is auto expired. Cannot be larger than the bucket_span.
13
42
  # @option options [Redis] :redis (nil) Redis client if you need to customize connection options
14
43
  #
15
- # @return [RateLimit] RateLimit instance
44
+ # @return [Ratelimit] Ratelimit instance
16
45
  #
17
46
  def initialize(key, options = {})
18
47
  @key = key
@@ -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}"
44
- redis.multi do
45
- redis.hincrby(subject, bucket, count)
46
- redis.hdel(subject, (bucket + 1) % @bucket_count)
47
- redis.hdel(subject, (bucket + 2) % @bucket_count)
48
- redis.expire(subject, @bucket_expiry)
74
+
75
+ # Cleanup expired keys every 100th request
76
+ cleanup_expired_keys(subject) if rand < 0.01
77
+
78
+ redis.multi do |transaction|
79
+ transaction.hincrby(subject, bucket, count)
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
@@ -18,11 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "redis", ">= 2.0.0"
21
+ spec.add_dependency "redis", ">= 3.0.0"
22
22
  spec.add_dependency "redis-namespace", ">= 1.0.0"
23
- spec.add_development_dependency "bundler", "~> 1.6"
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.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - E.J. Finneran
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-24 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
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.0.0
19
+ version: 3.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.0.0
26
+ version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: redis-namespace
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -42,14 +42,14 @@ dependencies:
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '1.6'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.6'
55
55
  - !ruby/object:Gem::Dependency
@@ -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
@@ -178,7 +164,7 @@ homepage: https://github.com/ejfinneran/ratelimit
178
164
  licenses:
179
165
  - MIT
180
166
  metadata: {}
181
- post_install_message:
167
+ post_install_message:
182
168
  rdoc_options: []
183
169
  require_paths:
184
170
  - lib
@@ -193,9 +179,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
179
  - !ruby/object:Gem::Version
194
180
  version: '0'
195
181
  requirements: []
196
- rubyforge_project:
197
- rubygems_version: 2.2.2
198
- signing_key:
182
+ rubygems_version: 3.1.4
183
+ signing_key:
199
184
  specification_version: 4
200
185
  summary: Rate limiting backed by redis
201
186
  test_files:
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