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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/README.md +187 -13
  4. data/Rakefile +12 -0
  5. data/lib/throttle_machines/async_limiter.rb +134 -0
  6. data/lib/throttle_machines/control.rb +95 -0
  7. data/lib/throttle_machines/controller_helpers.rb +79 -0
  8. data/lib/throttle_machines/dependency_error.rb +6 -0
  9. data/lib/throttle_machines/engine.rb +23 -0
  10. data/lib/throttle_machines/hedged_breaker.rb +23 -0
  11. data/lib/throttle_machines/hedged_request.rb +117 -0
  12. data/lib/throttle_machines/instrumentation.rb +158 -0
  13. data/lib/throttle_machines/limiter.rb +167 -0
  14. data/lib/throttle_machines/middleware.rb +90 -0
  15. data/lib/throttle_machines/rack_middleware/allow2_ban.rb +62 -0
  16. data/lib/throttle_machines/rack_middleware/blocklist.rb +27 -0
  17. data/lib/throttle_machines/rack_middleware/configuration.rb +103 -0
  18. data/lib/throttle_machines/rack_middleware/fail2_ban.rb +87 -0
  19. data/lib/throttle_machines/rack_middleware/request.rb +12 -0
  20. data/lib/throttle_machines/rack_middleware/safelist.rb +27 -0
  21. data/lib/throttle_machines/rack_middleware/throttle.rb +95 -0
  22. data/lib/throttle_machines/rack_middleware/track.rb +51 -0
  23. data/lib/throttle_machines/rack_middleware.rb +89 -0
  24. data/lib/throttle_machines/storage/base.rb +93 -0
  25. data/lib/throttle_machines/storage/memory.rb +373 -0
  26. data/lib/throttle_machines/storage/null.rb +88 -0
  27. data/lib/throttle_machines/storage/redis/gcra.lua +22 -0
  28. data/lib/throttle_machines/storage/redis/get_breaker_state.lua +23 -0
  29. data/lib/throttle_machines/storage/redis/increment_counter.lua +9 -0
  30. data/lib/throttle_machines/storage/redis/peek_gcra.lua +16 -0
  31. data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +18 -0
  32. data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +24 -0
  33. data/lib/throttle_machines/storage/redis/record_breaker_success.lua +16 -0
  34. data/lib/throttle_machines/storage/redis/token_bucket.lua +23 -0
  35. data/lib/throttle_machines/storage/redis.rb +294 -0
  36. data/lib/throttle_machines/throttled_error.rb +14 -0
  37. data/lib/throttle_machines/version.rb +5 -0
  38. data/lib/throttle_machines.rb +130 -5
  39. metadata +113 -9
  40. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ VERSION = '0.1.1'
5
+ end
@@ -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
- VERSION = "0.0.0"
5
-
6
- def self.version
7
- VERSION
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
- end
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.0.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
- - seuros
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: rails
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
- description: ThrottleMachines provides intelligent throttling and rate limiting mechanisms
27
- for distributed systems. This is a placeholder gem - full implementation coming
28
- soon.
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.txt
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.0.0
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 throttling and rate limiting for distributed systems
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.