breaker_machines 0.4.0 → 0.6.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -3
  3. data/ext/breaker_machines_native/Cargo.toml +8 -0
  4. data/ext/breaker_machines_native/core/Cargo.toml +18 -0
  5. data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
  6. data/ext/breaker_machines_native/core/src/builder.rs +232 -0
  7. data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
  8. data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
  9. data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
  10. data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
  11. data/ext/breaker_machines_native/core/src/errors.rs +47 -0
  12. data/ext/breaker_machines_native/core/src/lib.rs +62 -0
  13. data/ext/breaker_machines_native/core/src/storage.rs +377 -0
  14. data/ext/breaker_machines_native/extconf.rb +40 -0
  15. data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
  16. data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
  17. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
  18. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
  19. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
  20. data/ext/breaker_machines_native/target/debug/build/rb-sys-2bb7281aac8faec8/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  21. data/ext/breaker_machines_native/target/debug/build/rb-sys-54cb99ea6aeab8bc/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  22. data/ext/breaker_machines_native/target/debug/build/rb-sys-9e64a270c6421e93/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  23. data/ext/breaker_machines_native/target/debug/build/rb-sys-e627030114d3fc19/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  24. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
  25. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
  26. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
  27. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
  28. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
  29. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
  30. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
  31. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
  32. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
  33. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
  34. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
  35. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
  36. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
  37. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
  38. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
  39. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
  40. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
  41. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
  42. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
  43. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
  44. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
  45. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
  46. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
  47. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
  48. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
  49. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
  50. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
  51. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
  52. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
  53. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
  54. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
  55. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
  56. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
  57. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
  58. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
  59. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
  60. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
  61. data/ext/breaker_machines_native/target/release/build/rb-sys-064bf9961dd17810/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  62. data/lib/breaker_machines/async_circuit.rb +47 -0
  63. data/lib/breaker_machines/async_support.rb +4 -3
  64. data/lib/breaker_machines/cascading_circuit.rb +5 -3
  65. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  66. data/lib/breaker_machines/circuit/base.rb +59 -0
  67. data/lib/breaker_machines/circuit/callbacks.rb +7 -12
  68. data/lib/breaker_machines/circuit/configuration.rb +6 -26
  69. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  70. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  71. data/lib/breaker_machines/circuit/introspection.rb +1 -0
  72. data/lib/breaker_machines/circuit/native.rb +127 -0
  73. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  74. data/lib/breaker_machines/circuit/state_management.rb +14 -61
  75. data/lib/breaker_machines/circuit.rb +1 -7
  76. data/lib/breaker_machines/circuit_group.rb +153 -0
  77. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  78. data/lib/breaker_machines/dsl.rb +2 -2
  79. data/lib/breaker_machines/errors.rb +20 -0
  80. data/lib/breaker_machines/hedged_async_support.rb +29 -36
  81. data/lib/breaker_machines/native_extension.rb +36 -0
  82. data/lib/breaker_machines/native_speedup.rb +6 -0
  83. data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
  84. data/lib/breaker_machines/storage/memory.rb +4 -1
  85. data/lib/breaker_machines/storage/native.rb +90 -0
  86. data/lib/breaker_machines/version.rb +1 -1
  87. data/lib/breaker_machines.rb +115 -11
  88. data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
  89. data/sig/breaker_machines.rbs +20 -8
  90. metadata +107 -7
  91. data/lib/breaker_machines/hedged_execution.rb +0 -113
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc152fbf822d0eae49f25c2f8acea2c3268c141d8a62a7aa313168007499e8a5
4
- data.tar.gz: 5e7d529cde4b41cbb32fca7502249c15465413c4fad31e9a29056fbc99196e09
3
+ metadata.gz: fae87263df3eaaba47c2a78df338740ae9ed8599e13a42867110270787f9dc8a
4
+ data.tar.gz: 03bb57d8e463c3bd92ed2d7938788563b3df6d94329f87cf176b6934c218a963
5
5
  SHA512:
6
- metadata.gz: fa71a3f703c2d9814a836d2eb160eaf89f1ea991cf80d96a237aa9b59654d13d7b0459c41c4b8a5018fc27b83fd2a6729b7740238372a8691e0aff944f5965a3
7
- data.tar.gz: 59c7fd005d672ad412f9824c4880f2fc9f0141fa56f0bc405d63f9f3a95f05278b54fdceb5e97e93a18116c4e4101dc80db730eecfcd854656e0324fb1074d3d
6
+ metadata.gz: 67a9a1adad11d9d83fba339ccf3fe24317dde8e1da919b5fa7ffd2cf0509908daeb20a6efac0519e30604433617d493bdfa358e9b3de41140a0f440432c626df
7
+ data.tar.gz: 907986bddf430ea0daa163d2c8d51e7c77e3a61031927117529bcdc5586bec664f45c9b9c293cb614ca0c96ee2f539e810fdebef9ea55a04d2d3a6c163d54d25
data/README.md CHANGED
@@ -44,6 +44,10 @@ Built on the battle-tested `state_machines` gem, because I don't reinvent wheels
44
44
 
45
45
  - **Thread-safe** circuit breaker implementation
46
46
  - **Fiber-safe mode** for async Ruby (Falcon, async gem)
47
+ - **AsyncCircuit** class with mutex-protected state transitions
48
+ - **Circuit Groups** for managing related circuits with dependencies
49
+ - **Coordinated State Management** for dependency-aware transitions
50
+ - **Cascading Circuit Breakers** for modeling system dependencies
47
51
  - **Hedged requests** for latency reduction
48
52
  - **Multiple backends** with automatic failover
49
53
  - **Bulkheading** to limit concurrent requests
@@ -52,21 +56,39 @@ Built on the battle-tested `state_machines` gem, because I don't reinvent wheels
52
56
  - **Pluggable storage** (Memory, Redis, Custom)
53
57
  - **Rich callbacks** and instrumentation
54
58
  - **ActiveSupport::Notifications** integration
59
+ - **Cross-platform support** - Optimized for MRI, JRuby, and TruffleRuby
55
60
 
56
61
  ## Documentation
57
62
 
63
+ ### Core Features
58
64
  - **Getting Started Guide** (docs/GETTING_STARTED.md) - Installation and basic usage
59
65
  - **Configuration Reference** (docs/CONFIGURATION.md) - All configuration options
60
66
  - **Advanced Patterns** (docs/ADVANCED_PATTERNS.md) - Complex scenarios and patterns
67
+
68
+ ### Advanced Features
69
+ - **Circuit Groups** (docs/CIRCUIT_GROUPS.md) - Managing related circuits with dependencies
70
+ - **Coordinated State Management** (docs/COORDINATED_STATE_MANAGEMENT.md) - Dependency-aware state transitions
71
+ - **Cascading Circuit Breakers** (docs/CASCADING_CIRCUITS.md) - Modeling system dependencies
72
+
73
+ ### Async & Concurrency
74
+ - **Async Mode** (docs/ASYNC.md) - Fiber-safe operations and AsyncCircuit
75
+ - **Async Storage Examples** (docs/ASYNC_STORAGE_EXAMPLES.md) - Non-blocking storage backends
76
+
77
+ ### Storage & Persistence
61
78
  - **Persistence Options** (docs/PERSISTENCE.md) - Storage backends and distributed state
62
- - **Observability Guide** (docs/OBSERVABILITY.md) - Monitoring and metrics
63
- - **Async Mode** (docs/ASYNC.md) - Fiber-safe operations
79
+
80
+ ### Testing
64
81
  - **Testing Guide** (docs/TESTING.md) - Testing strategies
65
82
  - [RSpec Testing](docs/TESTING_RSPEC.md)
66
83
  - [ActiveSupport Testing](docs/TESTING_ACTIVESUPPORT.md)
84
+
85
+ ### Integration & Monitoring
67
86
  - **Rails Integration** (docs/RAILS_INTEGRATION.md) - Rails-specific patterns
68
- - **Horror Stories** (docs/HORROR_STORIES.md) - Real production failures and lessons learned
87
+ - **Observability Guide** (docs/OBSERVABILITY.md) - Monitoring and metrics
88
+
89
+ ### Reference
69
90
  - **API Reference** (docs/API_REFERENCE.md) - Complete API documentation
91
+ - **Horror Stories** (docs/HORROR_STORIES.md) - Real production failures and lessons learned
70
92
 
71
93
  ## Why BreakerMachines?
72
94
 
@@ -0,0 +1,8 @@
1
+ [workspace]
2
+ members = ["core", "ffi"]
3
+ resolver = "2"
4
+
5
+ [workspace.dependencies]
6
+ breaker-machines = { path = "./core" }
7
+ state-machines = "0.6"
8
+ chrono-machines = "0.2.1"
@@ -0,0 +1,18 @@
1
+ [package]
2
+ name = "breaker-machines"
3
+ version = "0.6.0"
4
+ edition = "2024"
5
+ authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
+ license = "MIT"
7
+ description = "High-performance circuit breaker with fallback support and rate-based thresholds"
8
+ repository = "https://github.com/seuros/breaker_machines"
9
+ readme = "README.md"
10
+ keywords = ["circuit-breaker", "reliability", "fault-tolerance"]
11
+ categories = ["concurrency"]
12
+
13
+ [lib]
14
+ crate-type = ["lib"]
15
+
16
+ [dependencies]
17
+ state-machines = { workspace = true }
18
+ chrono-machines = { workspace = true }
@@ -0,0 +1,61 @@
1
+ //! Basic circuit breaker usage example
2
+
3
+ use breaker_machines::CircuitBreaker;
4
+
5
+ fn main() {
6
+ println!("=== Circuit Breaker Basic Example ===\n");
7
+
8
+ // Create a circuit with builder API
9
+ let mut circuit = CircuitBreaker::builder("payment_api")
10
+ .failure_threshold(3)
11
+ .failure_window_secs(10.0)
12
+ .half_open_timeout_secs(5.0)
13
+ .success_threshold(2)
14
+ .on_open(|name| println!("🔴 Circuit '{}' opened!", name))
15
+ .on_close(|name| println!("🟢 Circuit '{}' closed!", name))
16
+ .on_half_open(|name| println!("🟡 Circuit '{}' half-open, testing...", name))
17
+ .build();
18
+
19
+ println!("Initial state: {}\n", circuit.state_name());
20
+
21
+ // Simulate successful calls
22
+ println!("--- Successful calls ---");
23
+ for i in 1..=2 {
24
+ match circuit.call(move || Ok::<_, String>(format!("Payment {}", i))) {
25
+ Ok(result) => println!("✓ {}", result),
26
+ Err(e) => println!("✗ Error: {}", e),
27
+ }
28
+ }
29
+ println!("State: {}\n", circuit.state_name());
30
+
31
+ // Simulate failures
32
+ println!("--- Triggering failures ---");
33
+ for i in 1..=3 {
34
+ match circuit.call(move || Err::<String, _>(format!("Payment failed {}", i))) {
35
+ Ok(_) => println!("✓ Success"),
36
+ Err(e) => println!("✗ {}", e),
37
+ }
38
+ }
39
+ println!("State: {} (circuit opened)\n", circuit.state_name());
40
+
41
+ // Try calling while open
42
+ println!("--- Attempting call while open ---");
43
+ match circuit.call(|| Ok::<_, String>("Should be rejected")) {
44
+ Ok(_) => println!("✓ Success"),
45
+ Err(e) => println!("✗ {}", e),
46
+ }
47
+ println!();
48
+
49
+ // Reset and demonstrate recovery
50
+ println!("--- Resetting circuit ---");
51
+ circuit.reset();
52
+ println!("State after reset: {}\n", circuit.state_name());
53
+
54
+ // Successful calls after reset
55
+ println!("--- Calls after reset ---");
56
+ match circuit.call(|| Ok::<_, String>("Payment successful")) {
57
+ Ok(result) => println!("✓ {}", result),
58
+ Err(e) => println!("✗ {}", e),
59
+ }
60
+ println!("State: {}", circuit.state_name());
61
+ }
@@ -0,0 +1,232 @@
1
+ //! Builder API for ergonomic circuit breaker configuration
2
+
3
+ use crate::{
4
+ MemoryStorage, StorageBackend,
5
+ bulkhead::BulkheadSemaphore,
6
+ callbacks::Callbacks,
7
+ circuit::{CircuitBreaker, CircuitContext, Config},
8
+ classifier::FailureClassifier,
9
+ };
10
+ use std::sync::Arc;
11
+
12
+ /// Builder for creating circuit breakers with fluent API
13
+ pub struct CircuitBuilder {
14
+ name: String,
15
+ config: Config,
16
+ storage: Option<Arc<dyn StorageBackend>>,
17
+ failure_classifier: Option<Arc<dyn FailureClassifier>>,
18
+ bulkhead: Option<Arc<BulkheadSemaphore>>,
19
+ callbacks: Callbacks,
20
+ }
21
+
22
+ impl CircuitBuilder {
23
+ /// Create a new builder for a circuit with the given name
24
+ pub fn new(name: impl Into<String>) -> Self {
25
+ Self {
26
+ name: name.into(),
27
+ config: Config::default(),
28
+ storage: None,
29
+ failure_classifier: None,
30
+ bulkhead: None,
31
+ callbacks: Callbacks::new(),
32
+ }
33
+ }
34
+
35
+ /// Set the absolute failure threshold (number of failures to open circuit)
36
+ pub fn failure_threshold(mut self, threshold: usize) -> Self {
37
+ self.config.failure_threshold = Some(threshold);
38
+ self
39
+ }
40
+
41
+ /// Disable absolute failure threshold (use only rate-based)
42
+ pub fn disable_failure_threshold(mut self) -> Self {
43
+ self.config.failure_threshold = None;
44
+ self
45
+ }
46
+
47
+ /// Set the failure rate threshold (0.0-1.0)
48
+ /// Circuit opens when (failures / total_calls) >= this value
49
+ pub fn failure_rate(mut self, rate: f64) -> Self {
50
+ self.config.failure_rate_threshold = Some(rate.clamp(0.0, 1.0));
51
+ self
52
+ }
53
+
54
+ /// Set minimum number of calls before rate-based threshold is evaluated
55
+ pub fn minimum_calls(mut self, calls: usize) -> Self {
56
+ self.config.minimum_calls = calls;
57
+ self
58
+ }
59
+
60
+ /// Set the failure window in seconds
61
+ pub fn failure_window_secs(mut self, seconds: f64) -> Self {
62
+ self.config.failure_window_secs = seconds;
63
+ self
64
+ }
65
+
66
+ /// Set the half-open timeout in seconds
67
+ pub fn half_open_timeout_secs(mut self, seconds: f64) -> Self {
68
+ self.config.half_open_timeout_secs = seconds;
69
+ self
70
+ }
71
+
72
+ /// Set the success threshold (successes needed to close from half-open)
73
+ pub fn success_threshold(mut self, threshold: usize) -> Self {
74
+ self.config.success_threshold = threshold;
75
+ self
76
+ }
77
+
78
+ /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
79
+ /// Uses chrono-machines formula: timeout * (1 - jitter + rand * jitter)
80
+ pub fn jitter_factor(mut self, factor: f64) -> Self {
81
+ self.config.jitter_factor = factor;
82
+ self
83
+ }
84
+
85
+ /// Set custom storage backend
86
+ pub fn storage(mut self, storage: Arc<dyn StorageBackend>) -> Self {
87
+ self.storage = Some(storage);
88
+ self
89
+ }
90
+
91
+ /// Set a failure classifier to filter which errors should trip the circuit
92
+ ///
93
+ /// The classifier determines whether a given error should count toward
94
+ /// opening the circuit. Use this to ignore "expected" errors like validation
95
+ /// failures or client errors (4xx), while still tripping on server errors (5xx).
96
+ ///
97
+ /// # Examples
98
+ ///
99
+ /// ```rust
100
+ /// use breaker_machines::{CircuitBreaker, PredicateClassifier};
101
+ /// use std::sync::Arc;
102
+ ///
103
+ /// let circuit = CircuitBreaker::builder("api")
104
+ /// .failure_classifier(Arc::new(PredicateClassifier::new(|ctx| {
105
+ /// // Only trip on slow errors
106
+ /// ctx.duration > 1.0
107
+ /// })))
108
+ /// .build();
109
+ /// ```
110
+ pub fn failure_classifier(mut self, classifier: Arc<dyn FailureClassifier>) -> Self {
111
+ self.failure_classifier = Some(classifier);
112
+ self
113
+ }
114
+
115
+ /// Set maximum concurrency limit (bulkheading)
116
+ ///
117
+ /// When set, the circuit breaker will reject calls with `BulkheadFull` error
118
+ /// if the number of concurrent calls exceeds this limit. This prevents
119
+ /// resource exhaustion by limiting how many operations can run simultaneously.
120
+ ///
121
+ /// # Panics
122
+ ///
123
+ /// Panics if `limit` is 0.
124
+ ///
125
+ /// # Examples
126
+ ///
127
+ /// ```rust
128
+ /// use breaker_machines::CircuitBreaker;
129
+ ///
130
+ /// let mut circuit = CircuitBreaker::builder("api")
131
+ /// .max_concurrency(10) // Allow max 10 concurrent calls
132
+ /// .build();
133
+ ///
134
+ /// // This will succeed until 10 calls are running concurrently
135
+ /// let result = circuit.call(|| Ok::<_, String>("success"));
136
+ /// ```
137
+ pub fn max_concurrency(mut self, limit: usize) -> Self {
138
+ self.bulkhead = Some(Arc::new(BulkheadSemaphore::new(limit)));
139
+ self
140
+ }
141
+
142
+ /// Set callback for when circuit opens
143
+ pub fn on_open<F>(mut self, f: F) -> Self
144
+ where
145
+ F: Fn(&str) + Send + Sync + 'static,
146
+ {
147
+ self.callbacks.on_open = Some(Arc::new(f));
148
+ self
149
+ }
150
+
151
+ /// Set callback for when circuit closes
152
+ pub fn on_close<F>(mut self, f: F) -> Self
153
+ where
154
+ F: Fn(&str) + Send + Sync + 'static,
155
+ {
156
+ self.callbacks.on_close = Some(Arc::new(f));
157
+ self
158
+ }
159
+
160
+ /// Set callback for when circuit enters half-open
161
+ pub fn on_half_open<F>(mut self, f: F) -> Self
162
+ where
163
+ F: Fn(&str) + Send + Sync + 'static,
164
+ {
165
+ self.callbacks.on_half_open = Some(Arc::new(f));
166
+ self
167
+ }
168
+
169
+ /// Build the circuit breaker
170
+ pub fn build(self) -> CircuitBreaker {
171
+ let storage = self
172
+ .storage
173
+ .unwrap_or_else(|| Arc::new(MemoryStorage::new()));
174
+
175
+ let context = CircuitContext {
176
+ name: self.name,
177
+ config: self.config,
178
+ storage,
179
+ failure_classifier: self.failure_classifier,
180
+ bulkhead: self.bulkhead,
181
+ };
182
+
183
+ CircuitBreaker::with_context_and_callbacks(context, self.callbacks)
184
+ }
185
+ }
186
+
187
+ #[cfg(test)]
188
+ mod tests {
189
+ use super::*;
190
+
191
+ #[test]
192
+ fn test_builder_defaults() {
193
+ let circuit = CircuitBuilder::new("test").build();
194
+
195
+ assert_eq!(circuit.state_name(), "Closed");
196
+ assert!(circuit.is_closed());
197
+ }
198
+
199
+ #[test]
200
+ fn test_builder_custom_config() {
201
+ let circuit = CircuitBuilder::new("test")
202
+ .failure_threshold(10)
203
+ .failure_window_secs(120.0)
204
+ .half_open_timeout_secs(60.0)
205
+ .success_threshold(3)
206
+ .build();
207
+
208
+ assert!(circuit.is_closed());
209
+ }
210
+
211
+ #[test]
212
+ fn test_builder_with_callbacks() {
213
+ use std::sync::atomic::{AtomicBool, Ordering};
214
+
215
+ let opened = Arc::new(AtomicBool::new(false));
216
+ let opened_clone = opened.clone();
217
+
218
+ let mut circuit = CircuitBuilder::new("test")
219
+ .failure_threshold(2)
220
+ .on_open(move |_name| {
221
+ opened_clone.store(true, Ordering::SeqCst);
222
+ })
223
+ .build();
224
+
225
+ // Trigger failures to open circuit
226
+ let _ = circuit.call(|| Err::<(), _>("error 1"));
227
+ let _ = circuit.call(|| Err::<(), _>("error 2"));
228
+
229
+ // Callback should have been triggered
230
+ assert!(opened.load(Ordering::SeqCst));
231
+ }
232
+ }
@@ -0,0 +1,223 @@
1
+ //! Bulkhead implementation for concurrency limiting
2
+ //!
3
+ //! This module provides a semaphore-based bulkhead pattern to limit
4
+ //! the number of concurrent calls through a circuit breaker.
5
+
6
+ use std::sync::Arc;
7
+ use std::sync::atomic::{AtomicUsize, Ordering};
8
+
9
+ /// A semaphore-based bulkhead for limiting concurrent operations
10
+ ///
11
+ /// Bulkheading prevents thread pool exhaustion by rejecting requests
12
+ /// when a maximum concurrency limit is reached.
13
+ #[derive(Debug)]
14
+ pub struct BulkheadSemaphore {
15
+ /// Maximum number of concurrent permits
16
+ limit: usize,
17
+ /// Current number of acquired permits
18
+ acquired: AtomicUsize,
19
+ }
20
+
21
+ impl BulkheadSemaphore {
22
+ /// Create a new bulkhead semaphore with the given concurrency limit
23
+ ///
24
+ /// # Panics
25
+ ///
26
+ /// Panics if `limit` is 0.
27
+ pub fn new(limit: usize) -> Self {
28
+ assert!(limit > 0, "Bulkhead limit must be greater than 0");
29
+ Self {
30
+ limit,
31
+ acquired: AtomicUsize::new(0),
32
+ }
33
+ }
34
+
35
+ /// Try to acquire a permit without blocking
36
+ ///
37
+ /// Returns `Some(BulkheadGuard)` if a permit was acquired, or `None` if
38
+ /// the bulkhead is at capacity.
39
+ pub fn try_acquire(self: &Arc<Self>) -> Option<BulkheadGuard> {
40
+ // Try to increment the counter
41
+ let mut current = self.acquired.load(Ordering::Acquire);
42
+
43
+ loop {
44
+ // Check if we're at capacity
45
+ if current >= self.limit {
46
+ return None;
47
+ }
48
+
49
+ // Try to increment atomically
50
+ match self.acquired.compare_exchange_weak(
51
+ current,
52
+ current + 1,
53
+ Ordering::AcqRel,
54
+ Ordering::Acquire,
55
+ ) {
56
+ Ok(_) => {
57
+ // Successfully acquired permit
58
+ return Some(BulkheadGuard {
59
+ semaphore: Arc::clone(self),
60
+ });
61
+ }
62
+ Err(actual) => {
63
+ // Another thread modified the counter, try again
64
+ current = actual;
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ /// Get the current number of acquired permits
71
+ pub fn acquired(&self) -> usize {
72
+ self.acquired.load(Ordering::Acquire)
73
+ }
74
+
75
+ /// Get the maximum number of permits (bulkhead limit)
76
+ pub fn limit(&self) -> usize {
77
+ self.limit
78
+ }
79
+
80
+ /// Get the number of available permits
81
+ pub fn available(&self) -> usize {
82
+ self.limit.saturating_sub(self.acquired())
83
+ }
84
+
85
+ /// Release a permit (called by BulkheadGuard on drop)
86
+ fn release(&self) {
87
+ self.acquired.fetch_sub(1, Ordering::Release);
88
+ }
89
+ }
90
+
91
+ /// Guard that releases a bulkhead permit when dropped
92
+ ///
93
+ /// This ensures that permits are always released, even if the guarded
94
+ /// operation panics.
95
+ #[derive(Debug)]
96
+ pub struct BulkheadGuard {
97
+ semaphore: Arc<BulkheadSemaphore>,
98
+ }
99
+
100
+ impl Drop for BulkheadGuard {
101
+ fn drop(&mut self) {
102
+ self.semaphore.release();
103
+ }
104
+ }
105
+
106
+ #[cfg(test)]
107
+ mod tests {
108
+ use super::*;
109
+ use std::thread;
110
+
111
+ #[test]
112
+ fn test_bulkhead_basic_acquire_release() {
113
+ let bulkhead = Arc::new(BulkheadSemaphore::new(3));
114
+
115
+ assert_eq!(bulkhead.limit(), 3);
116
+ assert_eq!(bulkhead.acquired(), 0);
117
+ assert_eq!(bulkhead.available(), 3);
118
+
119
+ // Acquire first permit
120
+ let guard1 = bulkhead.try_acquire();
121
+ assert!(guard1.is_some());
122
+ assert_eq!(bulkhead.acquired(), 1);
123
+ assert_eq!(bulkhead.available(), 2);
124
+
125
+ // Acquire second permit
126
+ let guard2 = bulkhead.try_acquire();
127
+ assert!(guard2.is_some());
128
+ assert_eq!(bulkhead.acquired(), 2);
129
+
130
+ // Release first permit
131
+ drop(guard1);
132
+ assert_eq!(bulkhead.acquired(), 1);
133
+ assert_eq!(bulkhead.available(), 2);
134
+
135
+ // Release second permit
136
+ drop(guard2);
137
+ assert_eq!(bulkhead.acquired(), 0);
138
+ assert_eq!(bulkhead.available(), 3);
139
+ }
140
+
141
+ #[test]
142
+ fn test_bulkhead_at_capacity() {
143
+ let bulkhead = Arc::new(BulkheadSemaphore::new(2));
144
+
145
+ let guard1 = bulkhead.try_acquire().expect("Should acquire");
146
+ let guard2 = bulkhead.try_acquire().expect("Should acquire");
147
+
148
+ // At capacity - should fail
149
+ let guard3 = bulkhead.try_acquire();
150
+ assert!(guard3.is_none(), "Should not acquire when at capacity");
151
+ assert_eq!(bulkhead.acquired(), 2);
152
+
153
+ // Release one permit
154
+ drop(guard1);
155
+
156
+ // Now should succeed
157
+ let guard4 = bulkhead.try_acquire();
158
+ assert!(guard4.is_some(), "Should acquire after release");
159
+ assert_eq!(bulkhead.acquired(), 2);
160
+
161
+ drop(guard2);
162
+ drop(guard4);
163
+ }
164
+
165
+ #[test]
166
+ fn test_bulkhead_concurrent_access() {
167
+ let bulkhead = Arc::new(BulkheadSemaphore::new(5));
168
+ let mut handles = vec![];
169
+
170
+ // Spawn 10 threads trying to acquire permits
171
+ for _ in 0..10 {
172
+ let bulkhead_clone = Arc::clone(&bulkhead);
173
+ let handle = thread::spawn(move || {
174
+ if let Some(_guard) = bulkhead_clone.try_acquire() {
175
+ // Hold the permit briefly
176
+ thread::sleep(std::time::Duration::from_millis(10));
177
+ true
178
+ } else {
179
+ false
180
+ }
181
+ });
182
+ handles.push(handle);
183
+ }
184
+
185
+ // Wait for all threads
186
+ let mut acquired_count = 0;
187
+ for handle in handles {
188
+ if handle.join().unwrap() {
189
+ acquired_count += 1;
190
+ }
191
+ }
192
+
193
+ // At least 5 should have succeeded (limit is 5)
194
+ assert!(
195
+ acquired_count >= 5,
196
+ "At least 5 threads should acquire permits"
197
+ );
198
+
199
+ // All permits should be released now
200
+ assert_eq!(bulkhead.acquired(), 0);
201
+ }
202
+
203
+ #[test]
204
+ #[should_panic(expected = "Bulkhead limit must be greater than 0")]
205
+ fn test_bulkhead_zero_limit() {
206
+ BulkheadSemaphore::new(0);
207
+ }
208
+
209
+ #[test]
210
+ fn test_bulkhead_guard_releases_on_panic() {
211
+ let bulkhead = Arc::new(BulkheadSemaphore::new(2));
212
+
213
+ let bulkhead_clone = Arc::clone(&bulkhead);
214
+ let result = std::panic::catch_unwind(move || {
215
+ let _guard = bulkhead_clone.try_acquire().unwrap();
216
+ panic!("Simulated panic");
217
+ });
218
+
219
+ assert!(result.is_err());
220
+ // Guard should have been dropped and permit released
221
+ assert_eq!(bulkhead.acquired(), 0);
222
+ }
223
+ }
@@ -0,0 +1,58 @@
1
+ //! Callback system for circuit breaker state transitions
2
+
3
+ use std::sync::Arc;
4
+
5
+ /// Type alias for circuit breaker callback functions
6
+ pub type CallbackFn = Arc<dyn Fn(&str) + Send + Sync>;
7
+
8
+ /// Callbacks for circuit breaker events
9
+ #[derive(Clone)]
10
+ pub struct Callbacks {
11
+ pub on_open: Option<CallbackFn>,
12
+ pub on_close: Option<CallbackFn>,
13
+ pub on_half_open: Option<CallbackFn>,
14
+ }
15
+
16
+ impl Callbacks {
17
+ pub fn new() -> Self {
18
+ Self {
19
+ on_open: None,
20
+ on_close: None,
21
+ on_half_open: None,
22
+ }
23
+ }
24
+
25
+ pub fn trigger_open(&self, circuit: &str) {
26
+ if let Some(ref callback) = self.on_open {
27
+ callback(circuit);
28
+ }
29
+ }
30
+
31
+ pub fn trigger_close(&self, circuit: &str) {
32
+ if let Some(ref callback) = self.on_close {
33
+ callback(circuit);
34
+ }
35
+ }
36
+
37
+ pub fn trigger_half_open(&self, circuit: &str) {
38
+ if let Some(ref callback) = self.on_half_open {
39
+ callback(circuit);
40
+ }
41
+ }
42
+ }
43
+
44
+ impl Default for Callbacks {
45
+ fn default() -> Self {
46
+ Self::new()
47
+ }
48
+ }
49
+
50
+ impl std::fmt::Debug for Callbacks {
51
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52
+ f.debug_struct("Callbacks")
53
+ .field("on_open", &self.on_open.is_some())
54
+ .field("on_close", &self.on_close.is_some())
55
+ .field("on_half_open", &self.on_half_open.is_some())
56
+ .finish()
57
+ }
58
+ }