ratelimit 1.0.3 → 1.1.0

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
- 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