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 +5 -5
- data/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -2
- data/lib/ratelimit/version.rb +1 -1
- data/lib/ratelimit.rb +66 -14
- data/ratelimit.gemspec +2 -3
- data/spec/ratelimit_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -1
- metadata +11 -26
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cb39d9fac25f51e2a7a95957ce738e281898774a296974b05cfa57b0ea858462
|
4
|
+
data.tar.gz: 6c2026639b39ebfec84029f00a4a56a78299378a8fbece227806b80a96f5058e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
[](https://codeclimate.com/github/ejfinneran/ratelimit)
|
3
|
+
[](https://codeclimate.com/github/ejfinneran/ratelimit)
|
5
4
|
[](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).
|
data/lib/ratelimit/version.rb
CHANGED
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
|
-
|
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 [
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
90
|
+
oldest_bucket = get_bucket(Time.now.to_i - interval)
|
91
|
+
current_bucket = get_bucket
|
60
92
|
subject = "#{@key}:#{subject}"
|
61
93
|
|
62
|
-
|
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
|
-
(
|
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", ">=
|
21
|
+
spec.add_dependency "redis", ">= 3.0.0"
|
22
22
|
spec.add_dependency "redis-namespace", ">= 1.0.0"
|
23
|
-
spec.add_development_dependency "bundler", "
|
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"
|
data/spec/ratelimit_spec.rb
CHANGED
@@ -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
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
|
+
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:
|
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:
|
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:
|
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
|
-
|
197
|
-
|
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:
|