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 +4 -4
- data/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +4 -0
- data/README.md +1 -2
- data/lib/ratelimit/version.rb +1 -1
- data/lib/ratelimit.rb +62 -10
- data/ratelimit.gemspec +0 -1
- data/spec/ratelimit_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -1
- metadata +3 -17
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
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
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,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.
|
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
|
-
|
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
@@ -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"
|
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
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
|
@@ -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
|