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 +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
|
-
[![
|
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).
|
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:
|