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.
- checksums.yaml +4 -4
- data/README.md +25 -3
- data/ext/breaker_machines_native/Cargo.toml +8 -0
- data/ext/breaker_machines_native/core/Cargo.toml +18 -0
- data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/core/src/builder.rs +232 -0
- data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
- data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
- data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
- data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
- data/ext/breaker_machines_native/core/src/errors.rs +47 -0
- data/ext/breaker_machines_native/core/src/lib.rs +62 -0
- data/ext/breaker_machines_native/core/src/storage.rs +377 -0
- data/ext/breaker_machines_native/extconf.rb +40 -0
- data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
- data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
- 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
- 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
- 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
- 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
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
- 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
- data/lib/breaker_machines/async_circuit.rb +47 -0
- data/lib/breaker_machines/async_support.rb +4 -3
- data/lib/breaker_machines/cascading_circuit.rb +5 -3
- data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
- data/lib/breaker_machines/circuit/base.rb +59 -0
- data/lib/breaker_machines/circuit/callbacks.rb +7 -12
- data/lib/breaker_machines/circuit/configuration.rb +6 -26
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
- data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
- data/lib/breaker_machines/circuit/introspection.rb +1 -0
- data/lib/breaker_machines/circuit/native.rb +127 -0
- data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
- data/lib/breaker_machines/circuit/state_management.rb +14 -61
- data/lib/breaker_machines/circuit.rb +1 -7
- data/lib/breaker_machines/circuit_group.rb +153 -0
- data/lib/breaker_machines/coordinated_circuit.rb +10 -0
- data/lib/breaker_machines/dsl.rb +2 -2
- data/lib/breaker_machines/errors.rb +20 -0
- data/lib/breaker_machines/hedged_async_support.rb +29 -36
- data/lib/breaker_machines/native_extension.rb +36 -0
- data/lib/breaker_machines/native_speedup.rb +6 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
- data/lib/breaker_machines/storage/memory.rb +4 -1
- data/lib/breaker_machines/storage/native.rb +90 -0
- data/lib/breaker_machines/version.rb +1 -1
- data/lib/breaker_machines.rb +115 -11
- data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
- data/sig/breaker_machines.rbs +20 -8
- metadata +107 -7
- data/lib/breaker_machines/hedged_execution.rb +0 -113
|
@@ -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
|
+
}
|