co-limit 0.1.2 → 0.1.4
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 +4 -4
- data/README.md +101 -17
- data/lib/limit/version.rb +1 -1
- data/lib/limit.rb +81 -56
- data/lib/scripts/FixedWindow.lua +21 -0
- data/lib/scripts/RollingWindow.lua +28 -0
- data/lib/scripts/TokenBucket.lua +23 -0
- data/sig/limit/limit.rbs +11 -3
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6485d06837c5fb0b4b0fad6a0d8d4cdaede3b73dbd056b89af116e39f0be121a
|
|
4
|
+
data.tar.gz: 3d237294612352598260a83054ee370d7dacd297543f257189c1e0d18ea84771
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c30142a96da7574db4946c78a37d3716d5f69e4fd5c73a86e6bfffddb30f8c1bacf67206a7b22159c94c5322c5f3be821044a4c1c43ce94051fe9c3f25174611
|
|
7
|
+
data.tar.gz: 02c49b15b93c06208221ff6d4013c31dfe0053f9326c0e22f3d04c187906d17627b1c72e20ee7bea78f7ebedc169ac4074759aa9342c5eff8fef824dcef62d5e
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
# Limit Gem
|
|
3
3
|
|
|
4
|
-
Gem that provides flexible, Redis-backed rate limiting utilities. It supports
|
|
4
|
+
Gem that provides flexible, Redis-backed rate limiting utilities. It supports Fixed Window, Rolling Window (Sliding Log), and Token Bucket strategies to control the number of allowed requests for a given identifier within a time window or with a steady refill rate.
|
|
5
5
|
|
|
6
|
-
You can define rate-limiting rules dynamically using a lambda
|
|
6
|
+
You can define rate-limiting rules dynamically using a lambda and configure Redis via environment variables (REDIS_HOST, REDIS_PORT, REDIS_PASSWORD) or bypassing connection details directly.
|
|
7
7
|
|
|
8
8
|
This gem is ideal for APIs, background jobs, or any system that needs simple, efficient throttling logic.
|
|
9
9
|
|
|
@@ -21,11 +21,23 @@ If you are not using Bundler, you can install the gem directly by running:
|
|
|
21
21
|
$ gem install co-limit
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
### Supported Rate Limiters:
|
|
25
|
+
|
|
26
|
+
- **Fixed Window Rate Limiter**:
|
|
27
|
+
Allows a specified number of requests within a fixed time window. This method can cause burst traffic as it doesn't account for requests made outside of the window until it resets.
|
|
28
|
+
|
|
29
|
+
- **Rolling Window Rate Limiter**:
|
|
30
|
+
Uses a sliding window mechanism, where only requests made within the last `n` seconds are counted. It provides more consistent traffic flow but can be more resource-intensive.
|
|
31
|
+
|
|
32
|
+
- **Token Bucket Rate Limiter**:
|
|
33
|
+
Implements the classic token bucket algorithm. A bucket with fixed capacity refills tokens at a steady rate, allowing short bursts up to the bucket capacity while enforcing long‑term average throughput.
|
|
34
|
+
|
|
24
35
|
## Usage
|
|
25
36
|
|
|
26
|
-
###
|
|
37
|
+
### Examples
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
#### Rolling Window Example
|
|
40
|
+
Here's an example of how to use the rolling window rate limiter in your application:
|
|
29
41
|
|
|
30
42
|
```ruby
|
|
31
43
|
sync_limit_calculator = lambda do |key|
|
|
@@ -56,6 +68,91 @@ success_count += 1 if allowed
|
|
|
56
68
|
puts "Success count: #{success_count}" # Expected to be 11
|
|
57
69
|
```
|
|
58
70
|
|
|
71
|
+
#### Fixed Window Example
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Map keys to a fixed-window limit of N requests per M seconds
|
|
75
|
+
SITE_LIMITS = {
|
|
76
|
+
x: { max_requests: 10, window_seconds: 5 }, # 10 requests per 5 seconds
|
|
77
|
+
default: { max_requests: 10, window_seconds: 60 }
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
calc = lambda do |key|
|
|
81
|
+
plan = key.split(":").last.to_sym rescue :default
|
|
82
|
+
SITE_LIMITS.fetch(plan, SITE_LIMITS[:default])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
limiter = Limit::FixedWindowRateLimiter.new(
|
|
86
|
+
identifier_prefix: "access",
|
|
87
|
+
limit_calculator: calc
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
key = "user123:x" # => will map to { max_requests: 10, window_seconds: 5 }
|
|
91
|
+
|
|
92
|
+
# Consume up to the window limit
|
|
93
|
+
allowed = 0
|
|
94
|
+
12.times { allowed += 1 if limiter.allowed?(key) }
|
|
95
|
+
puts allowed # => 10
|
|
96
|
+
|
|
97
|
+
# Wait until the next aligned fixed window begins to reset the counter
|
|
98
|
+
window = 5
|
|
99
|
+
now = Time.now
|
|
100
|
+
sleep(window - (now.to_i % window) + 0.5)
|
|
101
|
+
|
|
102
|
+
puts limiter.allowed?(key) # => true (window reset)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Token Bucket Example
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Choose a plan via the key suffix, then map it to a token bucket config
|
|
109
|
+
PLANS = {
|
|
110
|
+
basic: { bucket_capacity: 3, refill_rate: 1, refill_interval: 1 }, # 3 burst, ~1 req/s
|
|
111
|
+
premium: { bucket_capacity: 10, refill_rate: 5, refill_interval: 1 } # 10 burst, ~5 req/s
|
|
112
|
+
}.freeze
|
|
113
|
+
|
|
114
|
+
calc = lambda do |key|
|
|
115
|
+
plan = key.split(":").last.to_sym rescue :basic
|
|
116
|
+
PLANS.fetch(plan, PLANS[:basic])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
limiter = Limit::TokenBucketRateLimiter.new(
|
|
120
|
+
identifier_prefix: "access", # identifier_prefix is used to namespace keys in Redis (consistent across all limiters)
|
|
121
|
+
limit_calculator: calc
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
key = "user123:basic" # the final Redis key will be "access:user123:basic" via identifier_prefix
|
|
125
|
+
|
|
126
|
+
# Use up to capacity immediately
|
|
127
|
+
3.times { puts limiter.allowed?(key) } # => true, true, true
|
|
128
|
+
puts limiter.allowed?(key) # => false (bucket empty)
|
|
129
|
+
|
|
130
|
+
sleep 1.2 # wait ~1s to refill ~1 token
|
|
131
|
+
puts limiter.allowed?(key) # => true
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Key Points:
|
|
135
|
+
|
|
136
|
+
- **identifier_prefix**: A namespace prefix for Redis keys (e.g., `"access"`).
|
|
137
|
+
- **limit_calculator**: A `Lambda` that takes a key (e.g., `"user_id:site_name"`) and returns a hash with `max_requests` and `window_seconds`.
|
|
138
|
+
|
|
139
|
+
### Limit Hash Structure by Limiter
|
|
140
|
+
|
|
141
|
+
Your `limit_calculator` lambda must return a hash whose expected keys depend on the limiter you use:
|
|
142
|
+
|
|
143
|
+
- FixedWindowRateLimiter and RollingWindowRateLimiter expect:
|
|
144
|
+
`{ max_requests: Integer, window_seconds: Integer }`
|
|
145
|
+
|
|
146
|
+
- TokenBucketRateLimiter expects:
|
|
147
|
+
`{ bucket_capacity: Integer, refill_rate: Integer, refill_interval: Integer }`
|
|
148
|
+
|
|
149
|
+
Meaning: `refill_rate` tokens are added every `refill_interval` seconds (e.g., `refill_rate: 1, refill_interval: 1` = 1 token/second; `refill_rate: 3, refill_interval: 2` = 1.5 tokens/second effective rate). All values must be positive integers.
|
|
150
|
+
|
|
151
|
+
Notes:
|
|
152
|
+
- Redis keys are stored as "#{identifier_prefix}:#{key}". Choose concise, stable keys; e.g., identifier_prefix: "access" and key: "user_id:plan" → "access:user_id:plan".
|
|
153
|
+
- Ensure the keys you pass are unique per entity you want to limit (e.g., "access:user_id:plan").
|
|
154
|
+
- For Token Bucket, the Redis TTL is kept around the refill interval so buckets expire when inactive.
|
|
155
|
+
|
|
59
156
|
### Redis Configuration
|
|
60
157
|
|
|
61
158
|
You can configure the Redis connection either by passing the connection details as arguments or by setting environment variables.
|
|
@@ -82,19 +179,6 @@ You can configure the Redis connection either by passing the connection details
|
|
|
82
179
|
|
|
83
180
|
In this case, the gem will use these environment variables to establish the Redis connection.
|
|
84
181
|
|
|
85
|
-
### Key Points:
|
|
86
|
-
|
|
87
|
-
- **identifier_prefix**: A namespace prefix for Redis keys (e.g., `"access"`).
|
|
88
|
-
- **limit_calculator**: A `Lambda` that takes a key (e.g., `"user_id:site_name"`) and returns a hash with `max_requests` and `window_seconds`.
|
|
89
|
-
|
|
90
|
-
### Supported Rate Limiters:
|
|
91
|
-
|
|
92
|
-
- **Fixed Window Rate Limiter**:
|
|
93
|
-
Allows a specified number of requests within a fixed time window. This method can cause burst traffic as it doesn't account for requests made outside of the window until it resets.
|
|
94
|
-
|
|
95
|
-
- **Rolling Window Rate Limiter**:
|
|
96
|
-
Uses a sliding window mechanism, where only requests made within the last `n` seconds are counted. It provides more consistent traffic flow but can be more resource-intensive.
|
|
97
|
-
|
|
98
182
|
## Development
|
|
99
183
|
|
|
100
184
|
After checking out the repo, install the dependencies by running:
|
data/lib/limit/version.rb
CHANGED
data/lib/limit.rb
CHANGED
|
@@ -9,12 +9,14 @@ module Limit
|
|
|
9
9
|
# Base Class, implementing:
|
|
10
10
|
# - Method to connect with redis
|
|
11
11
|
# - Signature for limit_calculator
|
|
12
|
-
class
|
|
12
|
+
class BaseRateLimiter
|
|
13
13
|
attr_reader :limit_calculator, :identifier_prefix
|
|
14
14
|
|
|
15
15
|
@redis = nil
|
|
16
16
|
@logger = Logger.new($stdout)
|
|
17
17
|
|
|
18
|
+
REQUIRED_KEYS = %i[max_requests window_seconds].freeze
|
|
19
|
+
|
|
18
20
|
class << self
|
|
19
21
|
|
|
20
22
|
def connection
|
|
@@ -28,6 +30,14 @@ module Limit
|
|
|
28
30
|
@logger ||= Logger.new($stdout)
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
def load_script
|
|
34
|
+
script_name = name.split('::')[1].chomp('RateLimiter')
|
|
35
|
+
script_path = File.expand_path("scripts/#{script_name}.lua", __dir__)
|
|
36
|
+
@script_sha = @redis.script(:load, File.read(script_path))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :script_sha
|
|
40
|
+
|
|
31
41
|
private
|
|
32
42
|
|
|
33
43
|
def create_connection(host:, port:, password:)
|
|
@@ -44,7 +54,8 @@ module Limit
|
|
|
44
54
|
def initialize(identifier_prefix:, limit_calculator:, host: nil, port: nil, password: nil)
|
|
45
55
|
|
|
46
56
|
# @param identifier_prefix: [String] A namespace prefix for redis keys for this limiter instance
|
|
47
|
-
# @param limit_calculator:
|
|
57
|
+
# @param limit_calculator: Lambda that takes a key(String) and returns hash: {max_requests: Integer, window_seconds: Integer}
|
|
58
|
+
# But diff implementation can update the hash struct
|
|
48
59
|
|
|
49
60
|
unless identifier_prefix.is_a?(String) && !identifier_prefix.empty?
|
|
50
61
|
raise ArgumentError, 'identifier_prefix must be a non-empty String'
|
|
@@ -52,7 +63,7 @@ module Limit
|
|
|
52
63
|
|
|
53
64
|
raise ArgumentError, 'limit_calculator must be a Lambda' unless limit_calculator.lambda?
|
|
54
65
|
|
|
55
|
-
# Will be using the same connection across all
|
|
66
|
+
# Will be using the same connection across all instances unless wanted to connect to diff instance of redis
|
|
56
67
|
|
|
57
68
|
@redis = if host && port && password
|
|
58
69
|
self.class.send(:create_connection, host: host, port: port, password: password)
|
|
@@ -72,6 +83,7 @@ module Limit
|
|
|
72
83
|
raise e
|
|
73
84
|
end
|
|
74
85
|
|
|
86
|
+
self.class.load_script
|
|
75
87
|
end
|
|
76
88
|
|
|
77
89
|
def allowed?(key)
|
|
@@ -79,37 +91,54 @@ module Limit
|
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def get_key(key)
|
|
82
|
-
|
|
94
|
+
"#{@identifier_prefix}:#{key}"
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
|
|
86
98
|
protected
|
|
87
99
|
|
|
100
|
+
def required_keys
|
|
101
|
+
self.class::REQUIRED_KEYS
|
|
102
|
+
end
|
|
103
|
+
|
|
88
104
|
def get_current_limit(key)
|
|
89
105
|
limit_data = @limit_calculator.call(key)
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
required_keys.all? do |key|
|
|
108
|
+
unless limit_data[key].is_a?(Integer) && limit_data[key].positive?
|
|
109
|
+
raise ArgumentError, "Limit calculator for key '#{key}' returned invalid data: #{limit_data.inspect}.\n
|
|
110
|
+
Expected #{required_keys}"
|
|
111
|
+
end
|
|
95
112
|
end
|
|
96
113
|
|
|
97
114
|
limit_data
|
|
98
115
|
end
|
|
99
116
|
|
|
100
|
-
def
|
|
117
|
+
def eval_sha(key, argv)
|
|
118
|
+
attempts ||= 0
|
|
101
119
|
begin
|
|
102
|
-
@redis.
|
|
120
|
+
@redis.evalsha(self.class.script_sha, key, argv)
|
|
121
|
+
rescue Redis::CannotConnectError, Redis::TimeoutError, Redis::ConnectionError => e
|
|
122
|
+
if attempts < 3
|
|
123
|
+
@logger.warn(e.message)
|
|
124
|
+
@redis = self.class.connection
|
|
125
|
+
attempts += 1
|
|
126
|
+
retry
|
|
127
|
+
else
|
|
128
|
+
@logger.error("Error connecting to Redis: #{e.message}")
|
|
129
|
+
end
|
|
103
130
|
rescue Redis::CommandError => e
|
|
131
|
+
if e.message.start_with?("NOSCRIPT")
|
|
132
|
+
self.class.load_script
|
|
133
|
+
retry
|
|
134
|
+
else
|
|
135
|
+
@logger.error(e.message)
|
|
136
|
+
end
|
|
137
|
+
rescue Redis::BaseError => e
|
|
104
138
|
@logger.error(e.message)
|
|
105
139
|
end
|
|
106
140
|
end
|
|
107
141
|
|
|
108
|
-
def current_micros
|
|
109
|
-
(Time.now.to_f * 1_000_000).to_i
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
|
|
113
142
|
end
|
|
114
143
|
|
|
115
144
|
|
|
@@ -117,60 +146,56 @@ module Limit
|
|
|
117
146
|
|
|
118
147
|
# Fixed Window Rate Limiter, allows n of request in a fixed window
|
|
119
148
|
# ALERT: There is a chance of bursts/spike in this method, so use it with caution
|
|
120
|
-
class FixedWindowRateLimiter <
|
|
149
|
+
class FixedWindowRateLimiter < BaseRateLimiter
|
|
121
150
|
def allowed?(key)
|
|
122
151
|
limit_data = get_current_limit(key)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# https://redis.io/docs/latest/develop/use/pipelining/
|
|
132
|
-
pipe.incr(window_key)
|
|
133
|
-
pipe.expire(window_key, window_seconds)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
results[0] <= max_requests
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def get_key(key, window_seconds)
|
|
140
|
-
time_window = (Time.now.to_i / window_seconds) * window_seconds
|
|
141
|
-
"#{@identifier_prefix}:#{key}:#{time_window.to_s}"
|
|
152
|
+
result = eval_sha(
|
|
153
|
+
[get_key(key)],
|
|
154
|
+
[
|
|
155
|
+
limit_data[:window_seconds],
|
|
156
|
+
limit_data[:max_requests]
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
result == 1
|
|
142
160
|
end
|
|
143
161
|
end
|
|
144
162
|
|
|
145
|
-
# RollingWindow Rate limiter implemented using Sliding Log, allows n no of requests in rolling window
|
|
146
|
-
class RollingWindowRateLimiter <
|
|
163
|
+
# RollingWindow Rate limiter, implemented using Sliding Log, allows n no of requests in a rolling window
|
|
164
|
+
class RollingWindowRateLimiter < BaseRateLimiter
|
|
147
165
|
def allowed?(key)
|
|
148
166
|
limit_data = get_current_limit(key)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
result = eval_sha(
|
|
168
|
+
[get_key(key)],
|
|
169
|
+
[
|
|
170
|
+
limit_data[:window_seconds],
|
|
171
|
+
limit_data[:max_requests]
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
result == 1
|
|
175
|
+
end
|
|
176
|
+
end
|
|
153
177
|
|
|
154
|
-
|
|
178
|
+
# TokenBucketRateLimiter implements the token bucket algorithm that'll allow the steady refill rate
|
|
179
|
+
class TokenBucketRateLimiter < BaseRateLimiter
|
|
155
180
|
|
|
156
|
-
|
|
181
|
+
# limit_calculator: Lambda that takes a key(String) and returns hash:
|
|
182
|
+
# {bucket_capacity: Integer, refill_rate: Integer, refill_interval: Integer}
|
|
157
183
|
|
|
158
|
-
|
|
159
|
-
# uses sorted set
|
|
160
|
-
# https://redis.io/glossary/redis-sorted-sets/
|
|
161
|
-
pipe.zremrangebyscore(set_key, 0, window_start_micros)
|
|
162
|
-
pipe.zadd(set_key, curr_micros, curr_micros.to_s)
|
|
163
|
-
pipe.zcard(set_key)
|
|
164
|
-
pipe.expire(set_key, window_seconds)
|
|
165
|
-
end
|
|
184
|
+
REQUIRED_KEYS = %i[bucket_capacity refill_rate refill_interval].freeze
|
|
166
185
|
|
|
167
|
-
|
|
186
|
+
def allowed?(key)
|
|
187
|
+
limit_data = get_current_limit(key)
|
|
188
|
+
result = eval_sha(
|
|
189
|
+
[get_key(key)],
|
|
190
|
+
[
|
|
191
|
+
limit_data[:bucket_capacity],
|
|
192
|
+
limit_data[:refill_rate],
|
|
193
|
+
limit_data[:refill_interval]
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
result == 1
|
|
168
197
|
end
|
|
169
198
|
|
|
170
|
-
def get_key(key)
|
|
171
|
-
"#{@identifier_prefix}:#{key}"
|
|
172
|
-
end
|
|
173
199
|
end
|
|
174
200
|
|
|
175
|
-
|
|
176
201
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
local key = KEYS[1]
|
|
2
|
+
local window_seconds = tonumber(ARGV[1])
|
|
3
|
+
local max_requests = tonumber(ARGV[2])
|
|
4
|
+
|
|
5
|
+
local curr_time = redis.call('TIME')
|
|
6
|
+
local curr_sec = tonumber(curr_time[1])
|
|
7
|
+
|
|
8
|
+
local time_window = math.floor(curr_sec / window_seconds) * window_seconds
|
|
9
|
+
|
|
10
|
+
local window_key = key .. ":" .. tostring(time_window)
|
|
11
|
+
|
|
12
|
+
-- This is for simplicity as incr handles both creation and incrementing, rather than waiting on some read
|
|
13
|
+
local count = redis.call('incr', window_key)
|
|
14
|
+
|
|
15
|
+
redis.call('expire', window_key, window_seconds)
|
|
16
|
+
|
|
17
|
+
if count <= max_requests then
|
|
18
|
+
return 1
|
|
19
|
+
else
|
|
20
|
+
return 0
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
local key = KEYS[1]
|
|
2
|
+
local window_seconds = tonumber(ARGV[1])
|
|
3
|
+
local max_requests = tonumber(ARGV[2])
|
|
4
|
+
|
|
5
|
+
local time_data = redis.call('TIME')
|
|
6
|
+
local curr_micros = tonumber(time_data[1]) * 1000000 + tonumber(time_data[2])
|
|
7
|
+
local window_start_micros = curr_micros - (window_seconds * 1000000)
|
|
8
|
+
|
|
9
|
+
-- uses sorted set
|
|
10
|
+
-- https://redis.io/glossary/redis-sorted-sets/
|
|
11
|
+
-- Remove old entries
|
|
12
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, window_start_micros)
|
|
13
|
+
|
|
14
|
+
-- Add current request
|
|
15
|
+
redis.call('ZADD', key, curr_micros, tostring(curr_micros))
|
|
16
|
+
|
|
17
|
+
-- Count requests
|
|
18
|
+
local count = redis.call('ZCARD', key)
|
|
19
|
+
|
|
20
|
+
-- Set expiry
|
|
21
|
+
redis.call('EXPIRE', key, window_seconds)
|
|
22
|
+
|
|
23
|
+
if count <= max_requests then
|
|
24
|
+
return 1
|
|
25
|
+
else
|
|
26
|
+
return 0
|
|
27
|
+
end
|
|
28
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
local key = KEYS[1]
|
|
2
|
+
local bucket_capacity = tonumber(ARGV[1])
|
|
3
|
+
local refill_rate = tonumber(ARGV[2])
|
|
4
|
+
local refill_interval = tonumber(ARGV[3])
|
|
5
|
+
|
|
6
|
+
local time_data = redis.call('TIME')
|
|
7
|
+
local current_time = tonumber(time_data[1]) * 1000000 + tonumber(time_data[2])
|
|
8
|
+
|
|
9
|
+
local bucket_data = redis.call('HMGET', key, 'tokens', 'last_updated')
|
|
10
|
+
local tokens = tonumber(bucket_data[1]) or bucket_capacity
|
|
11
|
+
local last_updated = tonumber(bucket_data[2]) or current_time
|
|
12
|
+
|
|
13
|
+
local elapsed_micros = current_time - last_updated
|
|
14
|
+
local elapsed_seconds = elapsed_micros / 1000000
|
|
15
|
+
local tokens_to_add = math.floor(elapsed_seconds * refill_rate / refill_interval)
|
|
16
|
+
local new_tokens = math.min(bucket_capacity, tokens + tokens_to_add)
|
|
17
|
+
|
|
18
|
+
if new_tokens > 0 then
|
|
19
|
+
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_updated', current_time)
|
|
20
|
+
redis.call('EXPIRE', key, refill_interval * 2)
|
|
21
|
+
return 1
|
|
22
|
+
end
|
|
23
|
+
return 0
|
data/sig/limit/limit.rbs
CHANGED
|
@@ -2,9 +2,11 @@ type redis = untyped
|
|
|
2
2
|
|
|
3
3
|
module Limit
|
|
4
4
|
|
|
5
|
-
class
|
|
5
|
+
class BaseRateLimiter
|
|
6
|
+
REQUIRED_KEYS: Array[Symbol]
|
|
6
7
|
self.@logger: Logger
|
|
7
8
|
self.@redis: redis
|
|
9
|
+
self.@script_sha: untyped
|
|
8
10
|
@logger: Logger
|
|
9
11
|
|
|
10
12
|
@redis: redis | nil
|
|
@@ -17,6 +19,10 @@ module Limit
|
|
|
17
19
|
|
|
18
20
|
def self.logger: -> Logger
|
|
19
21
|
|
|
22
|
+
def self.load_script: -> String
|
|
23
|
+
|
|
24
|
+
def self.script_sha: -> String
|
|
25
|
+
|
|
20
26
|
attr_reader identifier_prefix: String
|
|
21
27
|
attr_reader limit_calculator: Proc
|
|
22
28
|
|
|
@@ -32,6 +38,8 @@ module Limit
|
|
|
32
38
|
|
|
33
39
|
def log: (String, String) -> nil
|
|
34
40
|
|
|
35
|
-
def
|
|
36
|
-
|
|
41
|
+
def eval_sha: (Array[String], Array[Integer]) -> bool
|
|
42
|
+
|
|
43
|
+
def required_keys: -> Array[Symbol]
|
|
44
|
+
end
|
|
37
45
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: co-limit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CosmicOppai
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -39,6 +39,9 @@ files:
|
|
|
39
39
|
- Steepfile
|
|
40
40
|
- lib/limit.rb
|
|
41
41
|
- lib/limit/version.rb
|
|
42
|
+
- lib/scripts/FixedWindow.lua
|
|
43
|
+
- lib/scripts/RollingWindow.lua
|
|
44
|
+
- lib/scripts/TokenBucket.lua
|
|
42
45
|
- sig/limit/limit.rbs
|
|
43
46
|
homepage: https://github.com/cosmicoppai/limit
|
|
44
47
|
licenses:
|