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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c3f17388a485325a47d600dec9898d47b2d89a1beb457bd7d2cd150458009df
4
- data.tar.gz: 1d70a4b143228755af8eb22de03e907fbbbe886c389c793f8ab8b9064f3f4845
3
+ metadata.gz: 1c498f9f097c1eac7384d573ea5e90b04e3850d6b236aae5a0f5b9bb0cdf29ae
4
+ data.tar.gz: 515fae0aad7083290b12cec368148e021ff9312fb026892fb4574fd89a1a0f6c
5
5
  SHA512:
6
- metadata.gz: 48bd7a5b4cdd3b1ad461211879b439d41247764e75e35761eb56de7b010ac83242c303cf6fa3aa2b4b515a1c5d609497a79e305d6e847b79c5d3e9d82dd67bf2
7
- data.tar.gz: f494acc74267e66616e179a74deaab41c189a481f407425210076d59d2f5e0d3e51baf183c4e6625975352577e171f2da303929fc40228b3235debb4c5810a60
6
+ metadata.gz: ec8ac810c8696a93a44bcf305245eef336ce338418b82a5236e73fa919afda7125cee2ac391c0230419f136525ee1104723dc3a81e42517c4c0a48ebd6878d76
7
+ data.tar.gz: 3908d0866286abc0556b79160c8543da2b542ab3cfb55b2c2d636fc69bb0393196848f920714e89e4926d7e4450ae5de33329d0774ae4a0f8f59860bbbaad620
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2025 Abdelkader Boudih
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,27 +1,201 @@
1
- # ThrottleMachines
1
+ # ThrottleMachines 🚀
2
2
 
3
- ThrottleMachines provides advanced throttling and rate limiting mechanisms for distributed systems with intelligent backoff strategies, circuit breakers, and adaptive rate control.
3
+ > **Ultra-thin rate limiting for the cosmos** - Where every request has its own trajectory through spacetime
4
4
 
5
- ## Installation
5
+ A precision-engineered Ruby rate limiting library built for interstellar traffic control.
6
+ Whether you're throttling API calls, AI requests, or quantum communications, ThrottleMachines ensures your systems maintain perfect orbital stability.
6
7
 
7
- Add this line to your application's Gemfile:
8
+ ---
8
9
 
9
- ```ruby
10
+ ## 🌌 Navigation
11
+
12
+ * [🎯 Mission Control](docs/MISSION_CONTROL.md) - Quick start guide for captains
13
+ * [🛸 Spacecraft Manual](docs/SPACECRAFT_MANUAL.md) - Understanding the fleet (algorithms)
14
+ * [⚡ Warp Drive Configuration](docs/WARP_DRIVE.md) - Storage backends & performance
15
+ * [🛡️ Shield Protocols](docs/SHIELD_PROTOCOLS.md) - Circuit breakers & defensive systems
16
+ * [🌍 Planetary Integration](docs/PLANETARY_INTEGRATION.md) - Rails & Rack middleware
17
+ * [🔬 Space Lab](docs/SPACE_LAB.md) - Testing in zero gravity
18
+ * [📡 Telemetry](docs/TELEMETRY.md) - Monitoring & instrumentation
19
+ * [🎮 Command Examples](docs/COMMAND_EXAMPLES.md) - Real mission scenarios
20
+ * [📜 Mission Logs](docs/MISSION_LOGS.md) - Lessons from real incidents
21
+ * [🚀 Advanced Features](docs/ADVANCED_FEATURES.md) - Next-generation capabilities
22
+
23
+ ---
24
+
25
+ ## 🚀 Launch Sequence
26
+
27
+ ```bash
28
+ # Add to your ship's Gemfile
10
29
  gem 'throttle_machines'
30
+
31
+ # For warp drive capabilities (Redis storage)
32
+ gem 'redis'
33
+
34
+ # For planetary Rails integration
35
+ gem 'rails' # or just railties
36
+ ```
37
+
38
+ Then initialize systems:
39
+ ```bash
40
+ bundle install
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 🎯 Quick Mission Brief
46
+
47
+ ### Basic Throttling - The Photon Torpedo Approach
48
+
49
+ ```ruby
50
+ # Simple rate limiting - like controlling photon torpedo launches
51
+ torpedo_limiter = ThrottleMachines.limiter("photon_launcher",
52
+ limit: 10, # 10 torpedoes
53
+ period: 60 # per minute
54
+ )
55
+
56
+ # Check and consume approach
57
+ if torpedo_limiter.allow?
58
+ torpedo_limiter.throttle! # Consume one torpedo charge
59
+ launch_torpedo!
60
+ else
61
+ puts "Torpedo bay recharging... Please wait."
62
+ end
63
+
64
+ # OR use the exception approach
65
+ begin
66
+ torpedo_limiter.throttle!
67
+ launch_torpedo!
68
+ rescue ThrottleMachines::ThrottledError => e
69
+ puts "Torpedo bay recharging... Retry after #{e.limiter.retry_after} seconds"
70
+ end
71
+ ```
72
+
73
+ ### GCRA - The Federation Diplomatic Ship 🛸
74
+
75
+ ```ruby
76
+ # GCRA: Like a diplomatic vessel that smoothly navigates traffic
77
+ # Instead of sudden stops, it gracefully manages flow
78
+ diplomatic_limiter = ThrottleMachines.limiter("federation_embassy",
79
+ limit: 100,
80
+ period: 60,
81
+ algorithm: :gcra # Generic Cell Rate Algorithm
82
+ )
83
+
84
+ # GCRA ensures smooth traffic - no thundering herds at your space dock!
11
85
  ```
12
86
 
13
- And then execute:
87
+ ### Block Form - Fire and Forget 🎯
88
+
89
+ ```ruby
90
+ # Use the block form for automatic throttling
91
+ # This handles the throttle! and error handling for you
92
+ ThrottleMachines.limit("warp_drive", limit: 5, period: 60) do
93
+ engage_warp_drive! # This will be throttled automatically
94
+ end
95
+ # Raises ThrottledError if limit exceeded
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 🌠 The Ultra-Thin Philosophy
101
+
102
+ Like the best spacecraft, ThrottleMachines follows the principle of **ultra-thin design**:
103
+
104
+ - **No bloat** - Every component serves a critical function
105
+ - **No dependencies** - Operates in deep space without supply lines
106
+ - **Pure Ruby propulsion** - No alien technology required
107
+ - **Modular systems** - Swap components like ship modules
108
+
109
+ ---
110
+
111
+ ## ⚡ Warp Factor Features
112
+
113
+ - **🚀 Multiple Algorithms** - GCRA, Token Bucket, Fixed Window, Sliding Window
114
+ - **💫 Distributed Ready** - Redis backend for fleet coordination
115
+ - **🔄 Async Support** - Fiber-safe operations for quantum communications
116
+ - **🏃 Hedged Requests** - Multi-path navigation for reduced latency
117
+ - **🎯 Microsecond Precision** - Navigate the cosmos with temporal accuracy
118
+ - **🔌 Pluggable Storage** - Memory crystals or Redis quantum storage
119
+ - **🌍 Rails Integration** - Seamless planetary docking procedures
120
+ - **📡 Rack Middleware** - Universal translator for all spacecraft
121
+ - **🔍 Full Instrumentation** - Real-time telemetry via ActiveSupport::Notifications
122
+ - **⚡ Thread-Safe** - Safe for multi-threaded spacecraft operations
123
+ - **🎭 Multiple Usage Patterns** - Check-first, exception-based, or block form
124
+
125
+ ---
126
+
127
+ ## 🌌 Why ThrottleMachines?
128
+
129
+ In the vast expanse of cyberspace, your systems face:
130
+ - **Asteroid fields** of concurrent requests
131
+ - **Black holes** of resource exhaustion
132
+ - **Alien attacks** from malicious actors
133
+ - **Temporal anomalies** in distributed systems
134
+
135
+ ThrottleMachines is your navigation system through these dangers, ensuring safe passage for every request in your fleet.
136
+
137
+ ---
138
+
139
+ ## 📜 Captain's Log
140
+
141
+ See our [Mission Archives](CHANGELOG.md) for the full history of our voyages.
142
+
143
+ ---
144
+
145
+ ## 🤝 Join the Crew
146
+
147
+ 1. Signal your intent (`fork` the repository)
148
+ 2. Create your feature branch (`git checkout -b feature/quantum-throttling`)
149
+ 3. Document your modifications (`git commit -am 'Add quantum entanglement support'`)
150
+ 4. Transmit to mothership (`git push origin feature/quantum-throttling`)
151
+ 5. Request docking clearance (`Pull Request`)
152
+
153
+ ---
154
+
155
+ ## 📡 Distress Signals
156
+
157
+ Found a breach in the hull? Encountered an unknown anomaly?
158
+ - **Emergency beacon**: [GitHub Issues](https://github.com/seuros/throttle_machines/issues)
159
+ - **Mission reports**: [Discussions](https://github.com/seuros/throttle_machines/discussions)
160
+
161
+ ---
162
+
163
+ ## 🎖️ Mission Credentials
164
+
165
+ MIT License - See [LICENSE](LICENSE) for full transmission.
166
+
167
+ ---
168
+
169
+ ## A Message from the Temporal Defense Corps
170
+
171
+ *The Quantum Navigation Computer flickers to life:*
172
+
173
+ "Space is vast. Time is relative. And your API is getting hammered by a bot farm in Eastern Europe.
174
+
175
+ Welcome to the temporal wars, where milliseconds matter and rate limits are the thin line between order and chaos. You think you're just limiting requests? No, pilot. You're manipulating the very fabric of spacetime to ensure fair resource distribution across the quantum multiverse of your distributed system.
176
+
177
+ Every request has a trajectory. Every limit has a purpose. Every algorithm is a different spacecraft designed for a specific mission through the hostile void.
178
+
179
+ The universe doesn't care about your startup's runway or your clever blog post about 'Building a Rate Limiter in 5 Minutes with Redis.' It cares about one immutable law:
180
+
181
+ **Can your system maintain temporal stability when the thundering herd arrives?**
182
+
183
+ If not, welcome aboard. We have algorithms."
184
+
185
+ *— Quantum Navigation Computer, Log Entry ∞*
186
+
187
+ ---
14
188
 
15
- $ bundle install
189
+ ## The Fleet Admiral's Warning
16
190
 
17
- Or install it yourself as:
191
+ So you built a microservice architecture because a YouTube video told you it was 'web scale'?
18
192
 
19
- $ gem install throttle_machines
193
+ Now you're drowning in a sea of uncontrolled requests, cascade failures, and that one service that keeps calling your API 10,000 times per second because someone forgot to implement exponential backoff.
20
194
 
21
- ## Note
195
+ This is your life raft. Don't let go.
22
196
 
23
- This is a placeholder gem. The full implementation is coming soon.
197
+ ---
24
198
 
25
- ## License
199
+ **"In space, nobody can hear your servers scream. But with ThrottleMachines, they won't need to."**
26
200
 
27
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
201
+ *— Fleet Admiral J'Rao, Survivor of the Great DDoS Wars of 2019*
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.test_files = FileList['test/**/*_test.rb'].exclude('test/dummy/**/*')
9
+ t.verbose = false
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+
5
+ module ThrottleMachines
6
+ # Async-aware rate limiter for fiber-safe operations
7
+ #
8
+ # Like quantum entanglement communications - allows multiple spacecraft
9
+ # to communicate simultaneously without interference, each in their own
10
+ # quantum state (fiber).
11
+ #
12
+ # Example:
13
+ # limiter = ThrottleMachines::AsyncLimiter.new("quantum_comms",
14
+ # limit: 100,
15
+ # period: 60,
16
+ # algorithm: :gcra
17
+ # )
18
+ #
19
+ # Async do
20
+ # if limiter.allowed_async?
21
+ # # Non-blocking operation
22
+ # end
23
+ # end
24
+ class AsyncLimiter < Limiter
25
+ def initialize(key, limit:, period:, algorithm: :fixed_window, storage: nil)
26
+ super
27
+ @fiber_storage = Concurrent::Map.new # Thread-safe fiber storage
28
+ end
29
+
30
+ # Async version of allowed? that's fiber-safe
31
+ def allowed_async?
32
+ allowed = if defined?(Async::Task) && Async::Task.current?
33
+ # In async context, use fiber-local checking
34
+ fiber_allowed?
35
+ else
36
+ # Fall back to regular synchronous check
37
+ allowed?
38
+ end
39
+
40
+ # Don't double-instrument when calling parent allowed?
41
+ return allowed unless defined?(Async::Task) && Async::Task.current?
42
+
43
+ # Instrument the async check
44
+ Instrumentation.rate_limit_checked(self, allowed: allowed, remaining: nil)
45
+ allowed
46
+ end
47
+
48
+ # Non-blocking check with async support
49
+ def check_async
50
+ if allowed_async?
51
+ yield if block_given?
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ # Async throttle with automatic retry
59
+ def throttle_async(max_wait: nil)
60
+ start_time = current_time
61
+
62
+ loop do
63
+ if allowed_async?
64
+ return yield if block_given?
65
+
66
+ return true
67
+ end
68
+
69
+ wait_time = retry_after
70
+
71
+ # Check if we've exceeded max wait time
72
+ if max_wait && (current_time - start_time + wait_time) > max_wait
73
+ raise ThrottleError, 'Maximum wait time exceeded'
74
+ end
75
+
76
+ # Non-blocking sleep in async context
77
+ if defined?(Async::Task) && Async::Task.current?
78
+ Async::Task.current.sleep(wait_time)
79
+ else
80
+ sleep(wait_time)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Get current fiber's state
86
+ def fiber_state
87
+ fiber_id = Fiber.current.object_id
88
+ @fiber_storage.compute_if_absent(fiber_id) do
89
+ {
90
+ last_check: 0,
91
+ tokens: @limit.to_f
92
+ }
93
+ end
94
+ end
95
+
96
+ # Clean up fiber storage periodically
97
+ def cleanup_fiber_storage
98
+ current_fibers = ObjectSpace.each_object(Fiber).map(&:object_id)
99
+ @fiber_storage.each_key do |fiber_id|
100
+ @fiber_storage.delete(fiber_id) unless current_fibers.include?(fiber_id)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def fiber_allowed?
107
+ state = fiber_state
108
+ now = current_time
109
+
110
+ case @algorithm
111
+ when :token_bucket
112
+ # Refill tokens based on time passed
113
+ time_passed = now - state[:last_check]
114
+ tokens_to_add = time_passed * (@limit.to_f / @period)
115
+ state[:tokens] = [state[:tokens] + tokens_to_add, @limit.to_f].min
116
+ state[:last_check] = now
117
+
118
+ if state[:tokens] >= 1
119
+ state[:tokens] -= 1
120
+ true
121
+ else
122
+ false
123
+ end
124
+ else
125
+ # Delegate to parent for other algorithms
126
+ allowed?
127
+ end
128
+ end
129
+
130
+ def current_time
131
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ class Control
5
+ attr_reader :key, :limiter, :breaker, :retrier
6
+
7
+ def initialize(key)
8
+ @key = key
9
+ @rules = {}
10
+ end
11
+
12
+ def limit(rate:, per:, algorithm: :gcra)
13
+ @rules[:limit] = { rate: rate, period: per, algorithm: algorithm }
14
+ self
15
+ end
16
+
17
+ def break_on(failures:, within:, timeout: nil)
18
+ timeout ||= within
19
+ @rules[:breaker] = {
20
+ failure_threshold: failures,
21
+ timeout: timeout,
22
+ window: within
23
+ }
24
+ self
25
+ end
26
+
27
+ def retry_on_failure(times:, backoff: :exponential, base_delay: 1, max_delay: 60)
28
+ @rules[:retry] = {
29
+ max_attempts: times,
30
+ jitter: backoff,
31
+ base_delay: base_delay,
32
+ max_delay: max_delay
33
+ }
34
+ self
35
+ end
36
+
37
+ def call(&block)
38
+ setup_components
39
+
40
+ # Build execution chain: Retry -> Breaker -> Limiter -> User Code
41
+ execution_chain = block
42
+
43
+ # Wrap with rate limiter (innermost, checked first)
44
+ if @limiter
45
+ limiter_wrapped = execution_chain
46
+ execution_chain = proc { @limiter.throttle!(&limiter_wrapped) }
47
+ end
48
+
49
+ # Wrap with circuit breaker
50
+ if @breaker
51
+ breaker_wrapped = execution_chain
52
+ execution_chain = proc { @breaker.call(&breaker_wrapped) }
53
+ end
54
+
55
+ # Wrap with retry logic (outermost, handles all failures)
56
+ if @retrier
57
+ retry_wrapped = execution_chain
58
+ execution_chain = proc { @retrier.call(&retry_wrapped) }
59
+ end
60
+
61
+ execution_chain.call
62
+ end
63
+
64
+ private
65
+
66
+ def setup_components
67
+ if @rules[:limit] && !@limiter
68
+ @limiter = ThrottleMachines.limiter(@key,
69
+ limit: @rules[:limit][:rate],
70
+ period: @rules[:limit][:period],
71
+ algorithm: @rules[:limit][:algorithm])
72
+ end
73
+
74
+ if @rules[:breaker] && !@breaker
75
+ @breaker = BreakerMachines::Circuit.new(
76
+ @key,
77
+ failure_threshold: @rules[:breaker][:failure_threshold],
78
+ failure_window: @rules[:breaker][:window],
79
+ reset_timeout: @rules[:breaker][:timeout]
80
+ )
81
+ end
82
+
83
+ return unless @rules[:retry] && !@retrier
84
+
85
+ # Use ChronoMachines for retry functionality
86
+ policy_options = {
87
+ max_attempts: @rules[:retry][:max_attempts],
88
+ base_delay: @rules[:retry][:base_delay],
89
+ max_delay: @rules[:retry][:max_delay],
90
+ jitter_factor: @rules[:retry][:jitter] == :exponential ? 1.0 : 0.0
91
+ }
92
+ @retrier = ChronoMachines::Executor.new(policy_options)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ module ControllerHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ if respond_to?(:helper_method) # No available in API Mode
9
+ helper_method :rate_limited?
10
+ helper_method :rate_limit_remaining
11
+ end
12
+ end
13
+
14
+ def throttle!(key = nil, limit:, period:, algorithm: :gcra)
15
+ key ||= default_throttle_key
16
+
17
+ limiter = ThrottleMachines.limiter(key, limit: limit, period: period, algorithm: algorithm)
18
+
19
+ render_rate_limited(limiter) unless limiter.allow?
20
+
21
+ set_rate_limit_headers(limiter)
22
+ end
23
+
24
+ def with_throttle(key = nil, limit:, period:, &)
25
+ key ||= default_throttle_key
26
+
27
+ ThrottleMachines.limit(key, limit: limit, period: period, &)
28
+ rescue ThrottledError => e
29
+ render_rate_limited(e.limiter)
30
+ end
31
+
32
+ def rate_limited?(key = nil, limit:, period:)
33
+ key ||= default_throttle_key
34
+ limiter = ThrottleMachines.limiter(key, limit: limit, period: period)
35
+ !limiter.allow?
36
+ end
37
+
38
+ def rate_limit_remaining(key = nil, limit:, period:)
39
+ key ||= default_throttle_key
40
+ limiter = ThrottleMachines.limiter(key, limit: limit, period: period)
41
+ limiter.remaining
42
+ end
43
+
44
+ private
45
+
46
+ def default_throttle_key
47
+ if current_user.respond_to?(:id)
48
+ "user:#{current_user.id}"
49
+ else
50
+ "ip:#{request.remote_ip}"
51
+ end
52
+ end
53
+
54
+ def render_rate_limited(limiter)
55
+ set_rate_limit_headers(limiter)
56
+
57
+ respond_to do |format|
58
+ format.json do
59
+ render json: {
60
+ error: 'Rate limit exceeded',
61
+ retry_after: limiter.retry_after
62
+ }, status: :too_many_requests
63
+ end
64
+
65
+ format.html do
66
+ render plain: "Rate limit exceeded. Please try again in #{limiter.retry_after} seconds.",
67
+ status: :too_many_requests
68
+ end
69
+ end
70
+ end
71
+
72
+ def set_rate_limit_headers(limiter)
73
+ response.headers['X-RateLimit-Limit'] = limiter.limit.to_s
74
+ response.headers['X-RateLimit-Remaining'] = limiter.remaining.to_s
75
+ response.headers['X-RateLimit-Reset'] = (Time.now.to_i + limiter.retry_after).to_s
76
+ response.headers['Retry-After'] = limiter.retry_after.ceil.to_s
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ # Error raised when dependencies aren't satisfied
5
+ class DependencyError < StandardError; end
6
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'throttle_machines/controller_helpers'
4
+ require 'throttle_machines/middleware'
5
+
6
+ module ThrottleMachines
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace ThrottleMachines
9
+
10
+ initializer 'throttle_machines.controller_helpers' do
11
+ ActiveSupport.on_load(:action_controller) do
12
+ include ThrottleMachines::ControllerHelpers
13
+ end
14
+ end
15
+
16
+ initializer 'throttle_machines.configure_defaults' do |_app|
17
+ ThrottleMachines.configure do |config|
18
+ # Use Rails.cache , user can override
19
+ config.store = Rails.cache
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ # Hedged request with circuit breaker integration
5
+ class HedgedBreaker
6
+ def initialize(breakers, delay: 0.05)
7
+ @breakers = Array(breakers)
8
+ @hedged = HedgedRequest.new(
9
+ delay: delay,
10
+ max_attempts: @breakers.size
11
+ )
12
+ end
13
+
14
+ def run(&)
15
+ @hedged.run do |attempt|
16
+ breaker = @breakers[attempt]
17
+ next if breaker.nil?
18
+
19
+ breaker.call(&)
20
+ end
21
+ end
22
+ end
23
+ end