redis-adequate_rate_limiter 1.0.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 +7 -0
- data/lib/redis/adequate_rate_limiter.rb +175 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc796edc36084592000ece83d07d06e1849b42e1f46965607ecc044be9285017
|
4
|
+
data.tar.gz: dae9dee9c2cf19a2f0a200f416ed1aed06f93ea63b172c0a6f06c89c43e5effb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7f06e5c038a2bcaa79974ccb2cc9c56fe1a8badff43ba204738de926ab5f25b3a504d82f378a61014617a39bb8386ba3f92f73b3e97f38dff13dc0572cb9d865
|
7
|
+
data.tar.gz: 4c03eaa4ee1e0d0fe3929f3d2a4062d3cd4cd5352a71bb8bfc05b37ba1e91aaf768b0f334ab7c42bd4b8f3d3f185108f64c4625bda9361858eabb3efdcca4162
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @see https://github.com/redis/redis-rb
|
4
|
+
class Redis
|
5
|
+
# Wrapper for a Lua script that provides smooth, configurable, space-efficient & blazing fast
|
6
|
+
# rate limiting.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# require 'redis'
|
11
|
+
# require 'redis/adequate_rate_limiter'
|
12
|
+
#
|
13
|
+
# r = Redis.new
|
14
|
+
# rate_limiter = Redis::AdequateRateLimiter.new(r)
|
15
|
+
# rate_limiter.configure(r, event_type, max_allowed, over_interval, lockout_interval)
|
16
|
+
#
|
17
|
+
# ...
|
18
|
+
# if rate_limiter.allow?(r, event_type, actor)
|
19
|
+
# # Count this action and check if it is allowed.
|
20
|
+
# ...
|
21
|
+
# end
|
22
|
+
# ...
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# If `allow?` is invoked on an event type that has not been configured, a
|
26
|
+
# ConfigNotDefinedError exception will be raised.
|
27
|
+
#
|
28
|
+
class AdequateRateLimiter
|
29
|
+
# Lua script SHA1 digest
|
30
|
+
# @return [String]
|
31
|
+
attr_reader :sha1_digest
|
32
|
+
|
33
|
+
# Create a new AdequateRateLimter instance
|
34
|
+
# @param redis [Redis]
|
35
|
+
def initialize(redis)
|
36
|
+
load_script(redis)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Configure rate limiting for an event type
|
40
|
+
# @param redis [Redis]
|
41
|
+
# @param event_type [String]
|
42
|
+
# @param max_allowed [Integer] Maximum allowed events for an actor
|
43
|
+
# @param over_interval [Integer] Over a rolling window of seconds
|
44
|
+
# @param lockout_interval [Integer] Seconds to lock out an actor from an event.
|
45
|
+
# @return [void]
|
46
|
+
# See README for more details.
|
47
|
+
def configure(redis, event_type, max_allowed, over_interval, lockout_interval)
|
48
|
+
key = namespaced_key(event_type)
|
49
|
+
redis.del(key)
|
50
|
+
redis.rpush(key, max_allowed)
|
51
|
+
redis.rpush(key, over_interval)
|
52
|
+
redis.rpush(key, lockout_interval)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if an actor is allowed to perform an action of event_type.
|
56
|
+
# @param redis [Redis]
|
57
|
+
# @param event_type [String]
|
58
|
+
# @param actor [String]
|
59
|
+
# @return [Boolean]
|
60
|
+
def allow?(redis, event_type, actor)
|
61
|
+
q = available_quota(redis, event_type, actor)
|
62
|
+
q.positive?
|
63
|
+
end
|
64
|
+
|
65
|
+
def peek(redis, event_type, actor)
|
66
|
+
redis.lrange(namespaced_key("#{event_type}:#{actor}"), 0, -1)
|
67
|
+
end
|
68
|
+
|
69
|
+
def peek_config(redis, event_type)
|
70
|
+
redis.lrange(namespaced_key(event_type), 0, -1)
|
71
|
+
end
|
72
|
+
|
73
|
+
def namespaced_key(key)
|
74
|
+
"arl:#{key}"
|
75
|
+
end
|
76
|
+
|
77
|
+
class ConfigNotDefinedError < StandardError
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Fetch remaining quota, as a fraction of max_allowed for an event_type, actor pair.
|
83
|
+
# @param redis [Redis]
|
84
|
+
# @param event_type [String]
|
85
|
+
# @param actor [String]
|
86
|
+
# @return [Float]
|
87
|
+
def available_quota(redis, event_type, actor)
|
88
|
+
keys = [namespaced_key(event_type), actor]
|
89
|
+
argv = [Time.now.to_i, 1]
|
90
|
+
|
91
|
+
available_quota = 1.0
|
92
|
+
|
93
|
+
available_quota = redis.evalsha(sha1_digest, keys, argv).to_f
|
94
|
+
rescue Redis::CommandError => e
|
95
|
+
if e.to_s.include?('NOSCRIPT')
|
96
|
+
load_script(redis)
|
97
|
+
available_quota = redis.evalsha(sha1_digest, keys, argv).to_f
|
98
|
+
elsif e.to_s.include?('No config found')
|
99
|
+
raise ConfigNotDefinedError, e.to_s
|
100
|
+
end
|
101
|
+
ensure
|
102
|
+
available_quota
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_script(redis)
|
106
|
+
lua_code = <<-LUA
|
107
|
+
local config_identifier = KEYS[1]
|
108
|
+
local actor_identifier = KEYS[2]
|
109
|
+
|
110
|
+
local config = redis.call('lrange', config_identifier, 0, -1)
|
111
|
+
if not next(config) then
|
112
|
+
return redis.error_reply("No config found for event type - "..config_identifier)
|
113
|
+
end
|
114
|
+
|
115
|
+
local max_allowed = tonumber(config[1])
|
116
|
+
local over_interval = tonumber(config[2])
|
117
|
+
local lockout_interval = tonumber(config[3])
|
118
|
+
local expire_in = over_interval + lockout_interval
|
119
|
+
|
120
|
+
local t1 = tonumber(ARGV[1])
|
121
|
+
local bump_counter = 0
|
122
|
+
if nil ~= ARGV[2] then
|
123
|
+
bump_counter = tonumber(ARGV[2])
|
124
|
+
end
|
125
|
+
|
126
|
+
local y = nil
|
127
|
+
|
128
|
+
local key = config_identifier..":"..actor_identifier
|
129
|
+
local tuple = redis.call('lrange', key, 0, -1)
|
130
|
+
-- Tuple format = {last_updated_score, last_updated_timestamp, last_blocked_timestamp}
|
131
|
+
|
132
|
+
if not next(tuple) then
|
133
|
+
y = bump_counter
|
134
|
+
if bump_counter > 0 then
|
135
|
+
-- Update tuple only if an event has occurred.
|
136
|
+
redis.call('rpush', key, y)
|
137
|
+
redis.call('rpush', key, t1)
|
138
|
+
redis.call('rpush', key, 0)
|
139
|
+
redis.call('expire', key, expire_in)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
y = tonumber(tuple[1])
|
143
|
+
local t0 = tonumber(tuple[2])
|
144
|
+
local b = tonumber(tuple[3])
|
145
|
+
|
146
|
+
if t1 - b > lockout_interval then
|
147
|
+
-- If not in the lockout interval since the last block
|
148
|
+
-- Decay the old score (at t0) using the configured slope to compute the current value.
|
149
|
+
y = y - (max_allowed / over_interval) * (t1 - t0)
|
150
|
+
y = math.max(y, 0) -- Score cannot drop below 0.
|
151
|
+
|
152
|
+
if bump_counter > 0 then
|
153
|
+
-- Update tuple only if an event has occurred.
|
154
|
+
y = y + bump_counter
|
155
|
+
|
156
|
+
if y >= max_allowed then
|
157
|
+
y = max_allowed -- Score cannot go above max_allowed.
|
158
|
+
-- Set t1 as the last_blocked_timestamp
|
159
|
+
redis.call('lset', key, 2, t1)
|
160
|
+
end
|
161
|
+
|
162
|
+
redis.call('lset', key, 0, string.format("%.4f", y))
|
163
|
+
redis.call('lset', key, 1, t1)
|
164
|
+
redis.call('expire', key, expire_in)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
return tostring(1.0 - y / max_allowed)
|
170
|
+
LUA
|
171
|
+
|
172
|
+
@sha1_digest = redis.script(:load, lua_code.freeze).freeze
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis-adequate_rate_limiter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bhal Agashe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-08-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
description: |
|
28
|
+
Uses a Lua script for Redis to provide smooth, configurable, space-efficient &
|
29
|
+
blazing fast rate limiting. The script is very lightweight and performs the entire operation
|
30
|
+
atomically. So it can be used to rate limit access to any resource at scale. Linked homepage
|
31
|
+
has more details.
|
32
|
+
email: ''
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- lib/redis/adequate_rate_limiter.rb
|
38
|
+
homepage: https://github.com/bagashe/redis-adequate-rate-limiter-rb
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '2.5'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.2.5
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Provides rate limiting using Redis & a Lua script.
|
61
|
+
test_files: []
|