throttle_machines 0.0.0 → 0.1.1
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/LICENSE +20 -0
- data/README.md +187 -13
- data/Rakefile +12 -0
- data/lib/throttle_machines/async_limiter.rb +134 -0
- data/lib/throttle_machines/control.rb +95 -0
- data/lib/throttle_machines/controller_helpers.rb +79 -0
- data/lib/throttle_machines/dependency_error.rb +6 -0
- data/lib/throttle_machines/engine.rb +23 -0
- data/lib/throttle_machines/hedged_breaker.rb +23 -0
- data/lib/throttle_machines/hedged_request.rb +117 -0
- data/lib/throttle_machines/instrumentation.rb +158 -0
- data/lib/throttle_machines/limiter.rb +167 -0
- data/lib/throttle_machines/middleware.rb +90 -0
- data/lib/throttle_machines/rack_middleware/allow2_ban.rb +62 -0
- data/lib/throttle_machines/rack_middleware/blocklist.rb +27 -0
- data/lib/throttle_machines/rack_middleware/configuration.rb +103 -0
- data/lib/throttle_machines/rack_middleware/fail2_ban.rb +87 -0
- data/lib/throttle_machines/rack_middleware/request.rb +12 -0
- data/lib/throttle_machines/rack_middleware/safelist.rb +27 -0
- data/lib/throttle_machines/rack_middleware/throttle.rb +95 -0
- data/lib/throttle_machines/rack_middleware/track.rb +51 -0
- data/lib/throttle_machines/rack_middleware.rb +89 -0
- data/lib/throttle_machines/storage/base.rb +93 -0
- data/lib/throttle_machines/storage/memory.rb +373 -0
- data/lib/throttle_machines/storage/null.rb +88 -0
- data/lib/throttle_machines/storage/redis/gcra.lua +22 -0
- data/lib/throttle_machines/storage/redis/get_breaker_state.lua +23 -0
- data/lib/throttle_machines/storage/redis/increment_counter.lua +9 -0
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +16 -0
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +18 -0
- data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +24 -0
- data/lib/throttle_machines/storage/redis/record_breaker_success.lua +16 -0
- data/lib/throttle_machines/storage/redis/token_bucket.lua +23 -0
- data/lib/throttle_machines/storage/redis.rb +294 -0
- data/lib/throttle_machines/throttled_error.rb +14 -0
- data/lib/throttle_machines/version.rb +5 -0
- data/lib/throttle_machines.rb +130 -5
- metadata +113 -9
- data/LICENSE.txt +0 -21
@@ -0,0 +1,294 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThrottleMachines
|
4
|
+
module Storage
|
5
|
+
class Redis < Base
|
6
|
+
# Load Lua scripts from files
|
7
|
+
LUA_SCRIPTS_DIR = File.expand_path('redis', __dir__)
|
8
|
+
|
9
|
+
GCRA_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'gcra.lua'))
|
10
|
+
TOKEN_BUCKET_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'token_bucket.lua'))
|
11
|
+
PEEK_GCRA_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_gcra.lua'))
|
12
|
+
PEEK_TOKEN_BUCKET_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_token_bucket.lua'))
|
13
|
+
INCREMENT_COUNTER_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'increment_counter.lua'))
|
14
|
+
GET_BREAKER_STATE_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'get_breaker_state.lua'))
|
15
|
+
RECORD_BREAKER_SUCCESS_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'record_breaker_success.lua'))
|
16
|
+
RECORD_BREAKER_FAILURE_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'record_breaker_failure.lua'))
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
super
|
20
|
+
@redis = options[:redis] || options[:client] || options[:pool]
|
21
|
+
@prefix = options[:prefix] || 'throttle:'
|
22
|
+
|
23
|
+
# Cache scripts to avoid repeated script loads
|
24
|
+
@gcra_sha = nil
|
25
|
+
@token_bucket_sha = nil
|
26
|
+
@peek_gcra_sha = nil
|
27
|
+
@peek_token_bucket_sha = nil
|
28
|
+
|
29
|
+
# Validate Redis connection
|
30
|
+
validate_redis_connection!
|
31
|
+
end
|
32
|
+
|
33
|
+
# Rate limiting operations
|
34
|
+
def increment_counter(key, window, amount = 1)
|
35
|
+
window_key = prefixed("#{key}:#{window}")
|
36
|
+
|
37
|
+
# Use Lua script for atomic increment with TTL
|
38
|
+
with_redis do |redis|
|
39
|
+
redis.eval(INCREMENT_COUNTER_SCRIPT, keys: [window_key], argv: [amount, window.to_i])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_counter(key, window)
|
44
|
+
window_key = prefixed("#{key}:#{window}")
|
45
|
+
with_redis { |r| (r.get(window_key) || 0).to_i }
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_counter_ttl(key, window)
|
49
|
+
window_key = prefixed("#{key}:#{window}")
|
50
|
+
ttl = with_redis { |r| r.ttl(window_key) }
|
51
|
+
[ttl, 0].max
|
52
|
+
end
|
53
|
+
|
54
|
+
def reset_counter(key, window)
|
55
|
+
window_key = prefixed("#{key}:#{window}")
|
56
|
+
with_redis { |r| r.del(window_key) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# GCRA operations (atomic via Lua)
|
60
|
+
def check_gcra_limit(key, emission_interval, delay_tolerance, ttl)
|
61
|
+
ensure_gcra_script_loaded!
|
62
|
+
|
63
|
+
result = with_redis do |redis|
|
64
|
+
redis.evalsha(
|
65
|
+
@gcra_sha,
|
66
|
+
keys: [prefixed(key)],
|
67
|
+
argv: [emission_interval, delay_tolerance, ttl, current_time]
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
allowed = result[0] == 1
|
72
|
+
tat = result[1]
|
73
|
+
now = current_time
|
74
|
+
|
75
|
+
{
|
76
|
+
allowed: allowed,
|
77
|
+
retry_after: allowed ? 0 : (tat - now - delay_tolerance),
|
78
|
+
tat: tat
|
79
|
+
}
|
80
|
+
rescue ::Redis::CommandError => e
|
81
|
+
raise unless e.message.include?('NOSCRIPT')
|
82
|
+
|
83
|
+
@gcra_sha = nil
|
84
|
+
retry
|
85
|
+
end
|
86
|
+
|
87
|
+
# Token bucket operations (atomic via Lua)
|
88
|
+
def check_token_bucket(key, capacity, refill_rate, ttl)
|
89
|
+
ensure_token_bucket_script_loaded!
|
90
|
+
|
91
|
+
result = with_redis do |redis|
|
92
|
+
redis.evalsha(
|
93
|
+
@token_bucket_sha,
|
94
|
+
keys: [prefixed(key)],
|
95
|
+
argv: [capacity, refill_rate, ttl, current_time]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
allowed = result[0] == 1
|
100
|
+
tokens = result[1]
|
101
|
+
|
102
|
+
{
|
103
|
+
allowed: allowed,
|
104
|
+
retry_after: allowed ? 0 : (1 - tokens) / refill_rate,
|
105
|
+
tokens_remaining: tokens.floor
|
106
|
+
}
|
107
|
+
rescue ::Redis::CommandError => e
|
108
|
+
raise unless e.message.include?('NOSCRIPT')
|
109
|
+
|
110
|
+
@token_bucket_sha = nil
|
111
|
+
retry
|
112
|
+
end
|
113
|
+
|
114
|
+
# Peek methods for non-consuming checks
|
115
|
+
def peek_gcra_limit(key, emission_interval, delay_tolerance)
|
116
|
+
ensure_peek_gcra_script_loaded!
|
117
|
+
|
118
|
+
result = with_redis do |redis|
|
119
|
+
redis.evalsha(
|
120
|
+
@peek_gcra_sha,
|
121
|
+
keys: [prefixed(key)],
|
122
|
+
argv: [emission_interval, delay_tolerance, current_time]
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
allowed = result[0] == 1
|
127
|
+
tat = result[1]
|
128
|
+
now = current_time
|
129
|
+
|
130
|
+
{
|
131
|
+
allowed: allowed,
|
132
|
+
retry_after: allowed ? 0 : (tat - now - delay_tolerance),
|
133
|
+
tat: tat
|
134
|
+
}
|
135
|
+
rescue ::Redis::CommandError => e
|
136
|
+
raise unless e.message.include?('NOSCRIPT')
|
137
|
+
|
138
|
+
@peek_gcra_sha = nil
|
139
|
+
retry
|
140
|
+
end
|
141
|
+
|
142
|
+
def peek_token_bucket(key, capacity, refill_rate)
|
143
|
+
ensure_peek_token_bucket_script_loaded!
|
144
|
+
|
145
|
+
result = with_redis do |redis|
|
146
|
+
redis.evalsha(
|
147
|
+
@peek_token_bucket_sha,
|
148
|
+
keys: [prefixed(key)],
|
149
|
+
argv: [capacity, refill_rate, current_time]
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
allowed = result[0] == 1
|
154
|
+
tokens_remaining = result[1]
|
155
|
+
|
156
|
+
{
|
157
|
+
allowed: allowed,
|
158
|
+
retry_after: allowed ? 0 : (1 - tokens_remaining) / refill_rate,
|
159
|
+
tokens_remaining: tokens_remaining.floor
|
160
|
+
}
|
161
|
+
rescue ::Redis::CommandError => e
|
162
|
+
raise unless e.message.include?('NOSCRIPT')
|
163
|
+
|
164
|
+
@peek_token_bucket_sha = nil
|
165
|
+
retry
|
166
|
+
end
|
167
|
+
|
168
|
+
# Circuit breaker operations
|
169
|
+
def get_breaker_state(key)
|
170
|
+
breaker_key = prefixed("breaker:#{key}")
|
171
|
+
|
172
|
+
# Use Lua script for atomic read and potential state transition
|
173
|
+
result = with_redis do |redis|
|
174
|
+
redis.eval(GET_BREAKER_STATE_SCRIPT, keys: [breaker_key], argv: [current_time])
|
175
|
+
end
|
176
|
+
|
177
|
+
return { state: :closed, failures: 0, last_failure: nil } if result.empty?
|
178
|
+
|
179
|
+
# Convert hash from Lua to Ruby format
|
180
|
+
state = {}
|
181
|
+
result.each_slice(2) { |k, v| state[k] = v }
|
182
|
+
|
183
|
+
{
|
184
|
+
state: state['state'].to_sym,
|
185
|
+
failures: state['failures'].to_i,
|
186
|
+
last_failure: state['last_failure']&.to_f,
|
187
|
+
opens_at: state['opens_at']&.to_f,
|
188
|
+
half_open_attempts: state['half_open_attempts']&.to_i
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
def record_breaker_success(key, _timeout, half_open_requests = 1)
|
193
|
+
breaker_key = prefixed("breaker:#{key}")
|
194
|
+
|
195
|
+
# Use Lua script for atomic success recording
|
196
|
+
with_redis do |redis|
|
197
|
+
redis.eval(RECORD_BREAKER_SUCCESS_SCRIPT, keys: [breaker_key], argv: [half_open_requests])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def record_breaker_failure(key, threshold, timeout)
|
202
|
+
breaker_key = prefixed("breaker:#{key}")
|
203
|
+
now = current_time
|
204
|
+
|
205
|
+
# Use Lua script for atomic failure recording
|
206
|
+
with_redis do |redis|
|
207
|
+
redis.eval(RECORD_BREAKER_FAILURE_SCRIPT, keys: [breaker_key], argv: [threshold, timeout, now])
|
208
|
+
end
|
209
|
+
|
210
|
+
get_breaker_state(key)
|
211
|
+
end
|
212
|
+
|
213
|
+
def trip_breaker(key, timeout)
|
214
|
+
breaker_key = prefixed("breaker:#{key}")
|
215
|
+
now = current_time
|
216
|
+
|
217
|
+
with_redis do |redis|
|
218
|
+
redis.hmset(breaker_key,
|
219
|
+
'state', 'open',
|
220
|
+
'failures', 0,
|
221
|
+
'last_failure', now,
|
222
|
+
'opens_at', now + timeout)
|
223
|
+
redis.expire(breaker_key, (timeout * 2).to_i)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def reset_breaker(key)
|
228
|
+
with_redis { |r| r.del(prefixed("breaker:#{key}")) }
|
229
|
+
end
|
230
|
+
|
231
|
+
# Utility operations
|
232
|
+
def clear(pattern = nil)
|
233
|
+
# Use SCAN instead of KEYS to avoid blocking in production
|
234
|
+
cursor = '0'
|
235
|
+
scan_pattern = pattern ? prefixed(pattern) : "#{@prefix}*"
|
236
|
+
|
237
|
+
with_redis do |redis|
|
238
|
+
loop do
|
239
|
+
cursor, keys = redis.scan(cursor, match: scan_pattern, count: 100)
|
240
|
+
redis.del(*keys) unless keys.empty?
|
241
|
+
break if cursor == '0'
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def healthy?
|
247
|
+
with_redis { |r| r.ping == 'PONG' }
|
248
|
+
rescue StandardError
|
249
|
+
false
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
def prefixed(key)
|
255
|
+
"#{@prefix}#{key}"
|
256
|
+
end
|
257
|
+
|
258
|
+
def ensure_gcra_script_loaded!
|
259
|
+
@ensure_gcra_script_loaded ||= with_redis { |r| r.script(:load, GCRA_SCRIPT) }
|
260
|
+
end
|
261
|
+
|
262
|
+
def ensure_token_bucket_script_loaded!
|
263
|
+
@ensure_token_bucket_script_loaded ||= with_redis { |r| r.script(:load, TOKEN_BUCKET_SCRIPT) }
|
264
|
+
end
|
265
|
+
|
266
|
+
def ensure_peek_gcra_script_loaded!
|
267
|
+
@ensure_peek_gcra_script_loaded ||= with_redis { |r| r.script(:load, PEEK_GCRA_SCRIPT) }
|
268
|
+
end
|
269
|
+
|
270
|
+
def ensure_peek_token_bucket_script_loaded!
|
271
|
+
@ensure_peek_token_bucket_script_loaded ||= with_redis { |r| r.script(:load, PEEK_TOKEN_BUCKET_SCRIPT) }
|
272
|
+
end
|
273
|
+
|
274
|
+
def validate_redis_connection!
|
275
|
+
raise ArgumentError, 'Redis client not provided' unless @redis
|
276
|
+
|
277
|
+
# Test connection
|
278
|
+
with_redis(&:ping)
|
279
|
+
rescue StandardError => e
|
280
|
+
raise ArgumentError, "Invalid Redis connection: #{e.message}"
|
281
|
+
end
|
282
|
+
|
283
|
+
def with_redis(&)
|
284
|
+
if @redis.respond_to?(:with)
|
285
|
+
# Connection pool
|
286
|
+
@redis.with(&)
|
287
|
+
else
|
288
|
+
# Regular Redis client
|
289
|
+
yield @redis
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThrottleMachines
|
4
|
+
class ThrottledError < StandardError
|
5
|
+
attr_reader :limiter
|
6
|
+
|
7
|
+
def initialize(limiter)
|
8
|
+
@limiter = limiter
|
9
|
+
super("Rate limit exceeded for #{limiter.key}")
|
10
|
+
end
|
11
|
+
|
12
|
+
delegate :retry_after, to: :@limiter
|
13
|
+
end
|
14
|
+
end
|
data/lib/throttle_machines.rb
CHANGED
@@ -1,9 +1,134 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'json'
|
4
|
+
require 'timeout'
|
5
|
+
require 'zeitwerk'
|
6
|
+
require 'active_support/configurable'
|
7
|
+
|
8
|
+
# Ecosystem dependencies
|
9
|
+
require 'chrono_machines'
|
10
|
+
require 'breaker_machines'
|
11
|
+
|
12
|
+
# Set up Zeitwerk loader
|
13
|
+
loader = Zeitwerk::Loader.for_gem
|
14
|
+
loader.ignore("#{__dir__}/throttle_machines/engine.rb") unless defined?(Rails::Engine)
|
15
|
+
loader.setup
|
16
|
+
|
3
17
|
module ThrottleMachines
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
18
|
+
include ActiveSupport::Configurable
|
19
|
+
|
20
|
+
# Define configuration options with defaults
|
21
|
+
config_accessor :default_limit, default: 100
|
22
|
+
config_accessor :default_period, default: 60 # 1 minute
|
23
|
+
config_accessor :default_storage, default: :memory
|
24
|
+
config_accessor :clock, default: nil
|
25
|
+
config_accessor :instrumentation_enabled, default: true
|
26
|
+
config_accessor :instrumentation_backend
|
27
|
+
config_accessor :_storage_instance
|
28
|
+
|
29
|
+
class << self
|
30
|
+
# Delegate monotonic time to BreakerMachines for consistency
|
31
|
+
delegate :monotonic_time, to: :BreakerMachines
|
32
|
+
|
33
|
+
def configure
|
34
|
+
yield(config) if block_given?
|
35
|
+
|
36
|
+
# Apply instrumentation settings
|
37
|
+
Instrumentation.enabled = config.instrumentation_enabled
|
38
|
+
Instrumentation.backend = config.instrumentation_backend if config.instrumentation_backend
|
39
|
+
end
|
40
|
+
|
41
|
+
def storage
|
42
|
+
config._storage_instance ||= create_storage(config.default_storage)
|
43
|
+
end
|
44
|
+
|
45
|
+
def storage=(value)
|
46
|
+
config._storage_instance = create_storage(value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def reset!(key = nil)
|
50
|
+
if key
|
51
|
+
storage.clear("#{key}*")
|
52
|
+
else
|
53
|
+
storage.clear
|
54
|
+
# Reset storage instance to force recreation with defaults
|
55
|
+
config._storage_instance = nil
|
56
|
+
# Re-apply instrumentation settings from the configuration
|
57
|
+
Instrumentation.enabled = config.instrumentation_enabled
|
58
|
+
Instrumentation.backend = config.instrumentation_backend
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def control(key, &block)
|
63
|
+
control = Control.new(key)
|
64
|
+
control.instance_eval(&block) if block
|
65
|
+
control
|
66
|
+
end
|
67
|
+
|
68
|
+
def limit(key, limit:, period:, algorithm: :fixed_window, &)
|
69
|
+
limiter = limiter(key, limit: limit, period: period, algorithm: algorithm)
|
70
|
+
limiter.throttle!(&)
|
71
|
+
end
|
72
|
+
|
73
|
+
def break_circuit(key, failures:, timeout:, &)
|
74
|
+
# Delegate to breaker_machines
|
75
|
+
breaker = BreakerMachines::Circuit.new(
|
76
|
+
key: key,
|
77
|
+
failure_threshold: failures,
|
78
|
+
timeout: timeout
|
79
|
+
)
|
80
|
+
breaker.call(&)
|
81
|
+
end
|
82
|
+
|
83
|
+
def retry_with(max_attempts: 3, backoff: :exponential, &)
|
84
|
+
# Delegate to chrono_machines
|
85
|
+
policy_options = {
|
86
|
+
max_attempts: max_attempts,
|
87
|
+
jitter_factor: backoff == :exponential ? 1.0 : 0.0
|
88
|
+
}
|
89
|
+
ChronoMachines.retry(policy_options, &)
|
90
|
+
end
|
91
|
+
|
92
|
+
def limiter(key, limit:, period:, algorithm: :fixed_window)
|
93
|
+
Limiter.new(key, limit: limit, period: period, algorithm: algorithm, storage: storage)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def create_storage(storage)
|
99
|
+
case storage
|
100
|
+
when Symbol
|
101
|
+
create_storage_from_symbol(storage)
|
102
|
+
when Class
|
103
|
+
storage.new
|
104
|
+
when Storage::Base
|
105
|
+
storage
|
106
|
+
else
|
107
|
+
raise ArgumentError, "Invalid storage: #{storage.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_storage_from_symbol(symbol)
|
112
|
+
case symbol
|
113
|
+
when :memory
|
114
|
+
Storage::Memory.new
|
115
|
+
when :redis
|
116
|
+
raise ArgumentError, 'Redis storage requires redis gem' unless defined?(Redis)
|
117
|
+
|
118
|
+
raise ArgumentError, 'Redis storage requires a Redis client instance. ' \
|
119
|
+
'Configure with: config.storage = Storage::Redis.new(redis: Redis.new)'
|
120
|
+
|
121
|
+
when :null
|
122
|
+
Storage::Null.new
|
123
|
+
else
|
124
|
+
raise ArgumentError, "Unknown storage type: #{symbol}"
|
125
|
+
end
|
126
|
+
end
|
8
127
|
end
|
9
|
-
|
128
|
+
|
129
|
+
# Auto-configure with defaults
|
130
|
+
configure
|
131
|
+
|
132
|
+
CircuitOpenError = BreakerMachines::CircuitOpenError
|
133
|
+
RetryExhaustedError = ChronoMachines::MaxRetriesExceededError
|
134
|
+
end
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttle_machines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Abdelkader Boudih
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
|
-
name:
|
13
|
+
name: activesupport
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
16
|
- - ">="
|
@@ -23,18 +23,121 @@ dependencies:
|
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '7.0'
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: concurrent-ruby
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.3'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rack
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: zeitwerk
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.7'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.7'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: breaker_machines
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.4'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.4'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: chrono_machines
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.2'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.2'
|
96
|
+
description: ThrottleMachines provides ultra-thin, elegant rate limiting with temporal
|
97
|
+
precision for distributed systems.
|
29
98
|
email:
|
30
99
|
- terminale@gmail.com
|
31
100
|
executables: []
|
32
101
|
extensions: []
|
33
102
|
extra_rdoc_files: []
|
34
103
|
files:
|
35
|
-
- LICENSE
|
104
|
+
- LICENSE
|
36
105
|
- README.md
|
106
|
+
- Rakefile
|
37
107
|
- lib/throttle_machines.rb
|
108
|
+
- lib/throttle_machines/async_limiter.rb
|
109
|
+
- lib/throttle_machines/control.rb
|
110
|
+
- lib/throttle_machines/controller_helpers.rb
|
111
|
+
- lib/throttle_machines/dependency_error.rb
|
112
|
+
- lib/throttle_machines/engine.rb
|
113
|
+
- lib/throttle_machines/hedged_breaker.rb
|
114
|
+
- lib/throttle_machines/hedged_request.rb
|
115
|
+
- lib/throttle_machines/instrumentation.rb
|
116
|
+
- lib/throttle_machines/limiter.rb
|
117
|
+
- lib/throttle_machines/middleware.rb
|
118
|
+
- lib/throttle_machines/rack_middleware.rb
|
119
|
+
- lib/throttle_machines/rack_middleware/allow2_ban.rb
|
120
|
+
- lib/throttle_machines/rack_middleware/blocklist.rb
|
121
|
+
- lib/throttle_machines/rack_middleware/configuration.rb
|
122
|
+
- lib/throttle_machines/rack_middleware/fail2_ban.rb
|
123
|
+
- lib/throttle_machines/rack_middleware/request.rb
|
124
|
+
- lib/throttle_machines/rack_middleware/safelist.rb
|
125
|
+
- lib/throttle_machines/rack_middleware/throttle.rb
|
126
|
+
- lib/throttle_machines/rack_middleware/track.rb
|
127
|
+
- lib/throttle_machines/storage/base.rb
|
128
|
+
- lib/throttle_machines/storage/memory.rb
|
129
|
+
- lib/throttle_machines/storage/null.rb
|
130
|
+
- lib/throttle_machines/storage/redis.rb
|
131
|
+
- lib/throttle_machines/storage/redis/gcra.lua
|
132
|
+
- lib/throttle_machines/storage/redis/get_breaker_state.lua
|
133
|
+
- lib/throttle_machines/storage/redis/increment_counter.lua
|
134
|
+
- lib/throttle_machines/storage/redis/peek_gcra.lua
|
135
|
+
- lib/throttle_machines/storage/redis/peek_token_bucket.lua
|
136
|
+
- lib/throttle_machines/storage/redis/record_breaker_failure.lua
|
137
|
+
- lib/throttle_machines/storage/redis/record_breaker_success.lua
|
138
|
+
- lib/throttle_machines/storage/redis/token_bucket.lua
|
139
|
+
- lib/throttle_machines/throttled_error.rb
|
140
|
+
- lib/throttle_machines/version.rb
|
38
141
|
homepage: https://github.com/seuros/throttle_machines
|
39
142
|
licenses:
|
40
143
|
- MIT
|
@@ -42,6 +145,7 @@ metadata:
|
|
42
145
|
homepage_uri: https://github.com/seuros/throttle_machines
|
43
146
|
source_code_uri: https://github.com/seuros/throttle_machines
|
44
147
|
changelog_uri: https://github.com/seuros/throttle_machines/blob/main/CHANGELOG.md
|
148
|
+
rubygems_mfa_required: 'true'
|
45
149
|
rdoc_options: []
|
46
150
|
require_paths:
|
47
151
|
- lib
|
@@ -49,7 +153,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
153
|
requirements:
|
50
154
|
- - ">="
|
51
155
|
- !ruby/object:Gem::Version
|
52
|
-
version: 3.
|
156
|
+
version: 3.3.0
|
53
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
158
|
requirements:
|
55
159
|
- - ">="
|
@@ -58,5 +162,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
162
|
requirements: []
|
59
163
|
rubygems_version: 3.6.9
|
60
164
|
specification_version: 4
|
61
|
-
summary: Advanced
|
165
|
+
summary: Advanced Rate limiting for Ruby applications
|
62
166
|
test_files: []
|
data/LICENSE.txt
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
The MIT License (MIT)
|
2
|
-
|
3
|
-
Copyright (c) 2025 seuros
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
13
|
-
all copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
-
THE SOFTWARE.
|