throttle_machines 0.2.4-x86_64-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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +204 -0
  4. data/lib/throttle_machines/async_breaker.rb +12 -0
  5. data/lib/throttle_machines/async_limiter.rb +24 -0
  6. data/lib/throttle_machines/async_support.rb +75 -0
  7. data/lib/throttle_machines/control.rb +95 -0
  8. data/lib/throttle_machines/controller_helpers.rb +79 -0
  9. data/lib/throttle_machines/dependency_error.rb +6 -0
  10. data/lib/throttle_machines/engine.rb +18 -0
  11. data/lib/throttle_machines/hedged_breaker.rb +23 -0
  12. data/lib/throttle_machines/hedged_request.rb +117 -0
  13. data/lib/throttle_machines/instrumentation.rb +158 -0
  14. data/lib/throttle_machines/limiter.rb +167 -0
  15. data/lib/throttle_machines/middleware.rb +89 -0
  16. data/lib/throttle_machines/native_speedup.rb +98 -0
  17. data/lib/throttle_machines/rack_middleware/allow2_ban.rb +51 -0
  18. data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
  19. data/lib/throttle_machines/rack_middleware/blocklist.rb +14 -0
  20. data/lib/throttle_machines/rack_middleware/configuration.rb +103 -0
  21. data/lib/throttle_machines/rack_middleware/fail2_ban.rb +78 -0
  22. data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
  23. data/lib/throttle_machines/rack_middleware/request.rb +12 -0
  24. data/lib/throttle_machines/rack_middleware/safelist.rb +14 -0
  25. data/lib/throttle_machines/rack_middleware/throttle.rb +95 -0
  26. data/lib/throttle_machines/rack_middleware/track.rb +51 -0
  27. data/lib/throttle_machines/rack_middleware.rb +87 -0
  28. data/lib/throttle_machines/storage/base.rb +72 -0
  29. data/lib/throttle_machines/storage/memory.rb +267 -0
  30. data/lib/throttle_machines/storage/null.rb +67 -0
  31. data/lib/throttle_machines/storage/redis/gcra.lua +22 -0
  32. data/lib/throttle_machines/storage/redis/increment_counter.lua +9 -0
  33. data/lib/throttle_machines/storage/redis/peek_gcra.lua +16 -0
  34. data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +18 -0
  35. data/lib/throttle_machines/storage/redis/token_bucket.lua +23 -0
  36. data/lib/throttle_machines/storage/redis.rb +231 -0
  37. data/lib/throttle_machines/throttled_error.rb +14 -0
  38. data/lib/throttle_machines/version.rb +5 -0
  39. data/lib/throttle_machines.rb +200 -0
  40. data/lib/throttle_machines_native/throttle_machines_native.so +0 -0
  41. metadata +218 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b903b7c1264b244a780a3730bd09d7bdab11db1f28961910980d93d7722ff92e
4
+ data.tar.gz: d6229cb0f861acea1f76e99a8ec4bcd0c3217f10745467f6411c5383519fe84b
5
+ SHA512:
6
+ metadata.gz: f35c2f3f16752f64e34105a201f5854b157a0f5092972acb47310802400502474d3d74ca52852056b77932c442af3d76ab0edec0becdd0142a07d4c17a11159e
7
+ data.tar.gz: 7c20314309fda5045cc7109b03b79dcdac901cb1ce9e83011cf59f6f1906c24f3fd9e6db7b86aff3091deebd21fdbd02118db08d9bce43d7a3f5be6c06111d0c
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThrottleMachines
4
+ class AsyncBreaker
5
+ class << self
6
+ def new(...)
7
+ ThrottleMachines.load_async_runtime!
8
+ BreakerMachines::AsyncCircuit.new(...)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,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,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