throttle_machines 0.2.4-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +204 -0
- data/lib/throttle_machines/async_breaker.rb +12 -0
- data/lib/throttle_machines/async_limiter.rb +24 -0
- data/lib/throttle_machines/async_support.rb +75 -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 +18 -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 +89 -0
- data/lib/throttle_machines/native_speedup.rb +98 -0
- data/lib/throttle_machines/rack_middleware/allow2_ban.rb +51 -0
- data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
- data/lib/throttle_machines/rack_middleware/blocklist.rb +14 -0
- data/lib/throttle_machines/rack_middleware/configuration.rb +103 -0
- data/lib/throttle_machines/rack_middleware/fail2_ban.rb +78 -0
- data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
- data/lib/throttle_machines/rack_middleware/request.rb +12 -0
- data/lib/throttle_machines/rack_middleware/safelist.rb +14 -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 +87 -0
- data/lib/throttle_machines/storage/base.rb +72 -0
- data/lib/throttle_machines/storage/memory.rb +267 -0
- data/lib/throttle_machines/storage/null.rb +67 -0
- data/lib/throttle_machines/storage/redis/gcra.lua +22 -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/token_bucket.lua +23 -0
- data/lib/throttle_machines/storage/redis.rb +231 -0
- data/lib/throttle_machines/throttled_error.rb +14 -0
- data/lib/throttle_machines/version.rb +5 -0
- data/lib/throttle_machines.rb +200 -0
- data/lib/throttle_machines_native/throttle_machines_native.so +0 -0
- metadata +218 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5f9628dc1bbdfd6dbddda17334b4ded5fe385eab742020e55b319f9afbccff6e
|
|
4
|
+
data.tar.gz: 9abf56ccc3a307799525af9609eeac60f1358270de52e7e322cd09e34892ff68
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: caa6563ca3c929f62ce90a35fb11972d81f548c81d5017be752ece07d23fdc01a5eb7a7bd1ada983a115f920f2df5ef416defe58c76e6b007ed012cca4d33210
|
|
7
|
+
data.tar.gz: 7db73825771edc88ba207306878ecb7f3604464ddf83fc016276d71fc64fc6c9de4317b94908a52b317ab9b03f8a36c3728dce68277b72c2d23e6e2871da1ada
|
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
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# ThrottleMachines 🚀
|
|
2
|
+
|
|
3
|
+
> **Ultra-thin rate limiting for the cosmos** - Where every request has its own trajectory through spacetime
|
|
4
|
+
|
|
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.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
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
|
|
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
|
+
# For fiber-safe async throttling
|
|
38
|
+
gem 'async'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then initialize systems:
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🎯 Quick Mission Brief
|
|
49
|
+
|
|
50
|
+
### Basic Throttling - The Photon Torpedo Approach
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Simple rate limiting - like controlling photon torpedo launches
|
|
54
|
+
torpedo_limiter = ThrottleMachines.limiter("photon_launcher",
|
|
55
|
+
limit: 10, # 10 torpedoes
|
|
56
|
+
period: 60 # per minute
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check and consume approach
|
|
60
|
+
if torpedo_limiter.allow?
|
|
61
|
+
torpedo_limiter.throttle! # Consume one torpedo charge
|
|
62
|
+
launch_torpedo!
|
|
63
|
+
else
|
|
64
|
+
puts "Torpedo bay recharging... Please wait."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# OR use the exception approach
|
|
68
|
+
begin
|
|
69
|
+
torpedo_limiter.throttle!
|
|
70
|
+
launch_torpedo!
|
|
71
|
+
rescue ThrottleMachines::ThrottledError => e
|
|
72
|
+
puts "Torpedo bay recharging... Retry after #{e.limiter.retry_after} seconds"
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### GCRA - The Federation Diplomatic Ship 🛸
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# GCRA: Like a diplomatic vessel that smoothly navigates traffic
|
|
80
|
+
# Instead of sudden stops, it gracefully manages flow
|
|
81
|
+
diplomatic_limiter = ThrottleMachines.limiter("federation_embassy",
|
|
82
|
+
limit: 100,
|
|
83
|
+
period: 60,
|
|
84
|
+
algorithm: :gcra # Generic Cell Rate Algorithm
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# GCRA ensures smooth traffic - no thundering herds at your space dock!
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Block Form - Fire and Forget 🎯
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Use the block form for automatic throttling
|
|
94
|
+
# This handles the throttle! and error handling for you
|
|
95
|
+
ThrottleMachines.limit("warp_drive", limit: 5, period: 60) do
|
|
96
|
+
engage_warp_drive! # This will be throttled automatically
|
|
97
|
+
end
|
|
98
|
+
# Raises ThrottledError if limit exceeded
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🌠 The Ultra-Thin Philosophy
|
|
104
|
+
|
|
105
|
+
Like the best spacecraft, ThrottleMachines follows the principle of **ultra-thin design**:
|
|
106
|
+
|
|
107
|
+
- **No bloat** - Every component serves a critical function
|
|
108
|
+
- **No dependencies** - Operates in deep space without supply lines
|
|
109
|
+
- **Pure Ruby propulsion** - No alien technology required
|
|
110
|
+
- **Modular systems** - Swap components like ship modules
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## ⚡ Warp Factor Features
|
|
115
|
+
|
|
116
|
+
- **🚀 Multiple Algorithms** - GCRA, Token Bucket, Fixed Window, Sliding Window
|
|
117
|
+
- **💫 Distributed Ready** - Redis backend for fleet coordination
|
|
118
|
+
- **🔄 Async Support** - Fiber-safe operations for quantum communications
|
|
119
|
+
- **🏃 Hedged Requests** - Multi-path navigation for reduced latency
|
|
120
|
+
- **🎯 Microsecond Precision** - Navigate the cosmos with temporal accuracy
|
|
121
|
+
- **🔌 Pluggable Storage** - Memory crystals or Redis quantum storage
|
|
122
|
+
- **🌍 Rails Integration** - Seamless planetary docking procedures
|
|
123
|
+
- **📡 Rack Middleware** - Universal translator for all spacecraft
|
|
124
|
+
- **🔍 Full Instrumentation** - Real-time telemetry via ActiveSupport::Notifications
|
|
125
|
+
- **⚡ Thread-Safe** - Safe for multi-threaded spacecraft operations
|
|
126
|
+
- **🎭 Multiple Usage Patterns** - Check-first, exception-based, or block form
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🌌 Why ThrottleMachines?
|
|
131
|
+
|
|
132
|
+
In the vast expanse of cyberspace, your systems face:
|
|
133
|
+
- **Asteroid fields** of concurrent requests
|
|
134
|
+
- **Black holes** of resource exhaustion
|
|
135
|
+
- **Alien attacks** from malicious actors
|
|
136
|
+
- **Temporal anomalies** in distributed systems
|
|
137
|
+
|
|
138
|
+
ThrottleMachines is your navigation system through these dangers, ensuring safe passage for every request in your fleet.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 📜 Captain's Log
|
|
143
|
+
|
|
144
|
+
See our [Mission Archives](CHANGELOG.md) for the full history of our voyages.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🤝 Join the Crew
|
|
149
|
+
|
|
150
|
+
1. Signal your intent (`fork` the repository)
|
|
151
|
+
2. Create your feature branch (`git checkout -b feature/quantum-throttling`)
|
|
152
|
+
3. Document your modifications (`git commit -am 'Add quantum entanglement support'`)
|
|
153
|
+
4. Transmit to mothership (`git push origin feature/quantum-throttling`)
|
|
154
|
+
5. Request docking clearance (`Pull Request`)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 📡 Distress Signals
|
|
159
|
+
|
|
160
|
+
Found a breach in the hull? Encountered an unknown anomaly?
|
|
161
|
+
- **Emergency beacon**: [GitHub Issues](https://github.com/seuros/throttle_machines/issues)
|
|
162
|
+
- **Mission reports**: [Discussions](https://github.com/seuros/throttle_machines/discussions)
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🎖️ Mission Credentials
|
|
167
|
+
|
|
168
|
+
MIT License - See [LICENSE](LICENSE) for full transmission.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## A Message from the Temporal Defense Corps
|
|
173
|
+
|
|
174
|
+
*The Quantum Navigation Computer flickers to life:*
|
|
175
|
+
|
|
176
|
+
"Space is vast. Time is relative. And your API is getting hammered by a bot farm in Eastern Europe.
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
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:
|
|
183
|
+
|
|
184
|
+
**Can your system maintain temporal stability when the thundering herd arrives?**
|
|
185
|
+
|
|
186
|
+
If not, welcome aboard. We have algorithms."
|
|
187
|
+
|
|
188
|
+
*— Quantum Navigation Computer, Log Entry ∞*
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## The Fleet Admiral's Warning
|
|
193
|
+
|
|
194
|
+
So you built a microservice architecture because a YouTube video told you it was 'web scale'?
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
This is your life raft. Don't let go.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
**"In space, nobody can hear your servers scream. But with ThrottleMachines, they won't need to."**
|
|
203
|
+
|
|
204
|
+
*— Fleet Admiral J'Rao, Survivor of the Great DDoS Wars of 2019*
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
# Backward-compatible async-aware limiter class.
|
|
5
|
+
#
|
|
6
|
+
# Example:
|
|
7
|
+
# limiter = ThrottleMachines::AsyncLimiter.new("api",
|
|
8
|
+
# limit: 100,
|
|
9
|
+
# period: 60,
|
|
10
|
+
# algorithm: :gcra
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
# Async do
|
|
14
|
+
# if limiter.allowed_async?
|
|
15
|
+
# # Currently allowed without consuming capacity
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# limiter.throttle_async(max_wait: 5) do
|
|
19
|
+
# # Consumes capacity through the same atomic path as throttle!
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
class AsyncLimiter < Limiter
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Async/Fiber support for ThrottleMachines.
|
|
4
|
+
#
|
|
5
|
+
# The async gem is optional. When an Async task is active, waits yield back to
|
|
6
|
+
# the scheduler; otherwise the limiter falls back to normal sleep.
|
|
7
|
+
|
|
8
|
+
module ThrottleMachines
|
|
9
|
+
# Fiber-aware rate limiter helpers.
|
|
10
|
+
module AsyncSupport
|
|
11
|
+
def allowed_async?
|
|
12
|
+
allow?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
alias allow_async? allowed_async?
|
|
16
|
+
|
|
17
|
+
def check_async
|
|
18
|
+
return false unless allowed_async?
|
|
19
|
+
|
|
20
|
+
yield if block_given?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def throttle_async(max_wait: nil, &block)
|
|
25
|
+
deadline = max_wait.nil? ? nil : ThrottleMachines.monotonic_time + max_wait.to_f
|
|
26
|
+
|
|
27
|
+
loop do
|
|
28
|
+
begin
|
|
29
|
+
throttle!
|
|
30
|
+
rescue ThrottledError => e
|
|
31
|
+
wait_time = [e.retry_after.to_f, 0.0].max
|
|
32
|
+
|
|
33
|
+
if deadline
|
|
34
|
+
remaining_wait = deadline - ThrottleMachines.monotonic_time
|
|
35
|
+
raise if remaining_wait <= 0 || wait_time > remaining_wait
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sleep_for(wait_time)
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return block.call if block
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Use Async's fiber-aware sleep when in an async context.
|
|
51
|
+
def sleep_for(duration)
|
|
52
|
+
if (task = current_async_task)
|
|
53
|
+
task.sleep(duration)
|
|
54
|
+
else
|
|
55
|
+
sleep(duration)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def current_async_task
|
|
60
|
+
return nil unless defined?(::Async::Task)
|
|
61
|
+
|
|
62
|
+
if ::Async::Task.respond_to?(:current?)
|
|
63
|
+
::Async::Task.current?
|
|
64
|
+
else
|
|
65
|
+
::Async::Task.current
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
ThrottleMachines::Limiter.include(ThrottleMachines::AsyncSupport)
|
|
74
|
+
|
|
75
|
+
warn "[ThrottleMachines] Async support loaded" if ENV['DEBUG_MATRYOSHKA']
|
|
@@ -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:, &block)
|
|
25
|
+
key ||= default_throttle_key
|
|
26
|
+
|
|
27
|
+
ThrottleMachines.limit(key, limit: limit, period: period, &block)
|
|
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,18 @@
|
|
|
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
|
+
# No default Rails.cache binding; storage is managed by ThrottleMachines
|
|
17
|
+
end
|
|
18
|
+
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(&block)
|
|
15
|
+
@hedged.run do |attempt|
|
|
16
|
+
breaker = @breakers[attempt]
|
|
17
|
+
next if breaker.nil?
|
|
18
|
+
|
|
19
|
+
breaker.call(&block)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent-ruby'
|
|
4
|
+
|
|
5
|
+
module ThrottleMachines
|
|
6
|
+
# Hedged Request - Multi-path Navigation System
|
|
7
|
+
#
|
|
8
|
+
# Like sending scout ships on multiple routes to find the fastest path.
|
|
9
|
+
# The first ship to reach the destination wins, others are recalled.
|
|
10
|
+
#
|
|
11
|
+
# Reduces latency by racing multiple backends/attempts with staggered delays.
|
|
12
|
+
#
|
|
13
|
+
# Example:
|
|
14
|
+
# hedged = ThrottleMachines::HedgedRequest.new(
|
|
15
|
+
# delay: 0.05, # 50ms between attempts
|
|
16
|
+
# max_attempts: 3
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# result = hedged.run do |attempt|
|
|
20
|
+
# case attempt
|
|
21
|
+
# when 0 then primary_backend.get(key)
|
|
22
|
+
# when 1 then secondary_backend.get(key)
|
|
23
|
+
# when 2 then tertiary_backend.get(key)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class HedgedRequest
|
|
27
|
+
attr_reader :delay, :max_attempts, :timeout
|
|
28
|
+
|
|
29
|
+
def initialize(delay: 0.05, max_attempts: 2, timeout: nil)
|
|
30
|
+
@delay = delay
|
|
31
|
+
@max_attempts = max_attempts
|
|
32
|
+
@timeout = timeout
|
|
33
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
|
34
|
+
min_threads: 1,
|
|
35
|
+
max_threads: max_attempts,
|
|
36
|
+
max_queue: max_attempts,
|
|
37
|
+
fallback_policy: :caller_runs
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Run hedged request with automatic cancellation of slower attempts
|
|
42
|
+
def run(&block)
|
|
43
|
+
raise ArgumentError, 'Block required' unless block
|
|
44
|
+
|
|
45
|
+
# Generate a unique request ID for tracking
|
|
46
|
+
request_id = "#{object_id}-#{Time.now.to_f}"
|
|
47
|
+
|
|
48
|
+
# Instrument the start of the hedged request
|
|
49
|
+
Instrumentation.hedged_request_started(request_id, attempts: @max_attempts)
|
|
50
|
+
|
|
51
|
+
# Use Concurrent::Promises for better async handling
|
|
52
|
+
futures = []
|
|
53
|
+
first_result = Concurrent::Promises.resolvable_future
|
|
54
|
+
start_time = Time.now.to_f
|
|
55
|
+
|
|
56
|
+
@max_attempts.times do |attempt|
|
|
57
|
+
# Schedule with delay
|
|
58
|
+
future = if attempt.zero?
|
|
59
|
+
Concurrent::Promises.future { yield(attempt) }
|
|
60
|
+
else
|
|
61
|
+
Concurrent::Promises.schedule(@delay * attempt) { yield(attempt) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Race to resolve first_result
|
|
65
|
+
future.then do |result|
|
|
66
|
+
if !first_result.resolved? && first_result.fulfill(result)
|
|
67
|
+
# This attempt won the race
|
|
68
|
+
duration = Time.now.to_f - start_time
|
|
69
|
+
Instrumentation.hedged_request_winner(request_id, attempt: attempt, duration: duration)
|
|
70
|
+
end
|
|
71
|
+
result
|
|
72
|
+
end.rescue do |error|
|
|
73
|
+
# Only reject if this was the last attempt and nothing succeeded
|
|
74
|
+
first_result.reject(error) if attempt == @max_attempts - 1 && !first_result.resolved?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
futures << future
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Wait with optional timeout
|
|
81
|
+
if @timeout
|
|
82
|
+
# Use any_resolved_future with timeout
|
|
83
|
+
timeout_future = Concurrent::Promises.schedule(@timeout) do
|
|
84
|
+
raise TimeoutError, "Hedged request timed out after #{@timeout}s"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Concurrent::Promises.any_resolved_future(first_result, timeout_future).value!
|
|
88
|
+
else
|
|
89
|
+
first_result.value!
|
|
90
|
+
end
|
|
91
|
+
ensure
|
|
92
|
+
# Cancel pending futures
|
|
93
|
+
futures.each { |f| f.cancel if f.pending? }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Run async version
|
|
97
|
+
def run_async(&block)
|
|
98
|
+
Concurrent::Promises.future { run(&block) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Shutdown the executor
|
|
102
|
+
def shutdown
|
|
103
|
+
@executor.shutdown
|
|
104
|
+
@executor.wait_for_termination(5)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Convenience method
|
|
109
|
+
def self.hedged_request(**, &)
|
|
110
|
+
hedged = HedgedRequest.new(**)
|
|
111
|
+
begin
|
|
112
|
+
hedged.run(&)
|
|
113
|
+
ensure
|
|
114
|
+
hedged.shutdown
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|