breaker_machines 0.5.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/ext/breaker_machines_native/Cargo.toml +8 -0
  3. data/ext/breaker_machines_native/core/Cargo.toml +18 -0
  4. data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
  5. data/ext/breaker_machines_native/core/src/builder.rs +232 -0
  6. data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
  7. data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
  8. data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
  9. data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
  10. data/ext/breaker_machines_native/core/src/errors.rs +47 -0
  11. data/ext/breaker_machines_native/core/src/lib.rs +62 -0
  12. data/ext/breaker_machines_native/core/src/storage.rs +377 -0
  13. data/ext/breaker_machines_native/extconf.rb +40 -0
  14. data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
  15. data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
  16. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
  17. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
  18. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
  19. 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
  20. 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
  21. 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
  22. 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
  23. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
  24. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
  25. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
  26. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
  27. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
  28. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
  29. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
  30. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
  31. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
  32. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
  33. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
  34. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
  35. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
  36. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
  37. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
  38. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
  39. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
  40. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
  41. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
  42. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
  43. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
  44. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
  45. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
  46. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
  47. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
  48. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
  49. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
  50. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
  51. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
  52. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
  53. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
  54. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
  55. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
  56. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
  57. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
  58. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
  59. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
  60. 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
  61. data/lib/breaker_machines/circuit/async_state_management.rb +1 -1
  62. data/lib/breaker_machines/circuit/base.rb +2 -1
  63. data/lib/breaker_machines/circuit/coordinated_state_management.rb +3 -3
  64. data/lib/breaker_machines/circuit/native.rb +127 -0
  65. data/lib/breaker_machines/circuit/state_callbacks.rb +17 -5
  66. data/lib/breaker_machines/circuit/state_management.rb +1 -1
  67. data/lib/breaker_machines/native_extension.rb +36 -0
  68. data/lib/breaker_machines/native_speedup.rb +6 -0
  69. data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
  70. data/lib/breaker_machines/storage/memory.rb +4 -1
  71. data/lib/breaker_machines/storage/native.rb +90 -0
  72. data/lib/breaker_machines/version.rb +1 -1
  73. data/lib/breaker_machines.rb +98 -11
  74. data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
  75. data/sig/breaker_machines.rbs +20 -8
  76. metadata +99 -6
@@ -0,0 +1,377 @@
1
+ //! Storage backends for circuit breaker events
2
+ //!
3
+ //! This module provides different storage implementations:
4
+ //! - `MemoryStorage`: Thread-safe in-memory storage with sliding window
5
+ //! - `NullStorage`: No-op storage for testing and benchmarking
6
+
7
+ use crate::{Event, EventKind};
8
+ use std::collections::HashMap;
9
+ use std::sync::RwLock;
10
+ use std::time::Instant;
11
+
12
+ /// Abstract storage backend for circuit breaker events
13
+ pub trait StorageBackend: Send + Sync + std::fmt::Debug {
14
+ /// Record a successful operation
15
+ fn record_success(&self, circuit_name: &str, duration: f64);
16
+
17
+ /// Record a failed operation
18
+ fn record_failure(&self, circuit_name: &str, duration: f64);
19
+
20
+ /// Count successful operations within a time window
21
+ fn success_count(&self, circuit_name: &str, window_seconds: f64) -> usize;
22
+
23
+ /// Count failed operations within a time window
24
+ fn failure_count(&self, circuit_name: &str, window_seconds: f64) -> usize;
25
+
26
+ /// Clear all events for a circuit
27
+ fn clear(&self, circuit_name: &str);
28
+
29
+ /// Clear all events for all circuits
30
+ fn clear_all(&self);
31
+
32
+ /// Get event log for a circuit (limited to last N events)
33
+ fn event_log(&self, circuit_name: &str, limit: usize) -> Vec<Event>;
34
+
35
+ /// Get monotonic time in seconds (relative to storage creation)
36
+ fn monotonic_time(&self) -> f64;
37
+ }
38
+
39
+ /// Thread-safe in-memory storage for circuit breaker events
40
+ #[derive(Debug)]
41
+ pub struct MemoryStorage {
42
+ /// Events keyed by circuit name
43
+ events: RwLock<HashMap<String, Vec<Event>>>,
44
+ /// Maximum events to keep per circuit
45
+ max_events: usize,
46
+ /// Monotonic time anchor (prevents clock skew issues from NTP)
47
+ start_time: Instant,
48
+ }
49
+
50
+ impl MemoryStorage {
51
+ /// Create a new storage instance
52
+ pub fn new() -> Self {
53
+ Self::with_max_events(1000)
54
+ }
55
+
56
+ /// Create storage with custom max events per circuit
57
+ pub fn with_max_events(max_events: usize) -> Self {
58
+ Self {
59
+ events: RwLock::new(HashMap::new()),
60
+ max_events,
61
+ start_time: Instant::now(),
62
+ }
63
+ }
64
+
65
+ // Private helper methods
66
+
67
+ fn record_event(&self, circuit_name: &str, kind: EventKind, duration: f64) {
68
+ let mut events = self.events.write().unwrap();
69
+ let circuit_events = events.entry(circuit_name.to_string()).or_default();
70
+
71
+ circuit_events.push(Event {
72
+ kind,
73
+ timestamp: self.monotonic_time(),
74
+ duration,
75
+ });
76
+
77
+ // Cleanup old events if we exceed max_events
78
+ if circuit_events.len() > self.max_events {
79
+ // Remove oldest 10% to avoid cleanup on every event
80
+ // Ensure we remove at least 1 event even with small max_events
81
+ let remove_count = (self.max_events / 10).max(1);
82
+ circuit_events.drain(0..remove_count);
83
+ }
84
+ }
85
+
86
+ fn count_events(&self, circuit_name: &str, kind: EventKind, window_seconds: f64) -> usize {
87
+ let events = self.events.read().unwrap();
88
+ let cutoff = self.monotonic_time() - window_seconds;
89
+
90
+ events
91
+ .get(circuit_name)
92
+ .map(|ev| {
93
+ ev.iter()
94
+ .filter(|e| e.kind == kind && e.timestamp >= cutoff)
95
+ .count()
96
+ })
97
+ .unwrap_or(0)
98
+ }
99
+ }
100
+
101
+ impl Default for MemoryStorage {
102
+ fn default() -> Self {
103
+ Self::new()
104
+ }
105
+ }
106
+
107
+ impl StorageBackend for MemoryStorage {
108
+ fn record_success(&self, circuit_name: &str, duration: f64) {
109
+ self.record_event(circuit_name, EventKind::Success, duration);
110
+ }
111
+
112
+ fn record_failure(&self, circuit_name: &str, duration: f64) {
113
+ self.record_event(circuit_name, EventKind::Failure, duration);
114
+ }
115
+
116
+ fn success_count(&self, circuit_name: &str, window_seconds: f64) -> usize {
117
+ self.count_events(circuit_name, EventKind::Success, window_seconds)
118
+ }
119
+
120
+ fn failure_count(&self, circuit_name: &str, window_seconds: f64) -> usize {
121
+ self.count_events(circuit_name, EventKind::Failure, window_seconds)
122
+ }
123
+
124
+ fn clear(&self, circuit_name: &str) {
125
+ let mut events = self.events.write().unwrap();
126
+ events.remove(circuit_name);
127
+ }
128
+
129
+ fn clear_all(&self) {
130
+ let mut events = self.events.write().unwrap();
131
+ events.clear();
132
+ }
133
+
134
+ fn event_log(&self, circuit_name: &str, limit: usize) -> Vec<Event> {
135
+ let events = self.events.read().unwrap();
136
+ events
137
+ .get(circuit_name)
138
+ .map(|ev| {
139
+ let start = if ev.len() > limit {
140
+ ev.len() - limit
141
+ } else {
142
+ 0
143
+ };
144
+ ev[start..].to_vec()
145
+ })
146
+ .unwrap_or_default()
147
+ }
148
+
149
+ fn monotonic_time(&self) -> f64 {
150
+ self.start_time.elapsed().as_secs_f64()
151
+ }
152
+ }
153
+
154
+ /// No-op storage backend for testing and benchmarking
155
+ ///
156
+ /// This storage implementation discards all events and always returns zero counts.
157
+ /// Useful for:
158
+ /// - Testing circuit breaker logic without storage overhead
159
+ /// - Benchmarking pure state machine performance
160
+ /// - Scenarios where external systems track metrics
161
+ ///
162
+ /// # Example
163
+ ///
164
+ /// ```rust
165
+ /// use breaker_machines::{CircuitBreaker, NullStorage};
166
+ /// use std::sync::Arc;
167
+ ///
168
+ /// let storage = Arc::new(NullStorage::new());
169
+ /// let mut circuit = CircuitBreaker::builder("test")
170
+ /// .storage(storage)
171
+ /// .build();
172
+ /// ```
173
+ #[derive(Debug, Clone, Copy)]
174
+ pub struct NullStorage {
175
+ start_time: Instant,
176
+ }
177
+
178
+ impl NullStorage {
179
+ /// Create a new null storage instance
180
+ pub fn new() -> Self {
181
+ Self {
182
+ start_time: Instant::now(),
183
+ }
184
+ }
185
+ }
186
+
187
+ impl Default for NullStorage {
188
+ fn default() -> Self {
189
+ Self::new()
190
+ }
191
+ }
192
+
193
+ impl StorageBackend for NullStorage {
194
+ fn record_success(&self, _circuit_name: &str, _duration: f64) {
195
+ // No-op
196
+ }
197
+
198
+ fn record_failure(&self, _circuit_name: &str, _duration: f64) {
199
+ // No-op
200
+ }
201
+
202
+ fn success_count(&self, _circuit_name: &str, _window_seconds: f64) -> usize {
203
+ 0
204
+ }
205
+
206
+ fn failure_count(&self, _circuit_name: &str, _window_seconds: f64) -> usize {
207
+ 0
208
+ }
209
+
210
+ fn clear(&self, _circuit_name: &str) {
211
+ // No-op
212
+ }
213
+
214
+ fn clear_all(&self) {
215
+ // No-op
216
+ }
217
+
218
+ fn event_log(&self, _circuit_name: &str, _limit: usize) -> Vec<Event> {
219
+ Vec::new()
220
+ }
221
+
222
+ fn monotonic_time(&self) -> f64 {
223
+ self.start_time.elapsed().as_secs_f64()
224
+ }
225
+ }
226
+
227
+ #[cfg(test)]
228
+ mod tests {
229
+ use super::*;
230
+
231
+ #[test]
232
+ fn test_memory_storage_record_and_count() {
233
+ let storage = MemoryStorage::new();
234
+
235
+ storage.record_success("test_circuit", 0.1);
236
+ storage.record_success("test_circuit", 0.2);
237
+ storage.record_failure("test_circuit", 0.5);
238
+
239
+ assert_eq!(storage.success_count("test_circuit", 60.0), 2);
240
+ assert_eq!(storage.failure_count("test_circuit", 60.0), 1);
241
+ }
242
+
243
+ #[test]
244
+ fn test_memory_storage_clear() {
245
+ let storage = MemoryStorage::new();
246
+
247
+ storage.record_success("test_circuit", 0.1);
248
+ assert_eq!(storage.success_count("test_circuit", 60.0), 1);
249
+
250
+ storage.clear("test_circuit");
251
+ assert_eq!(storage.success_count("test_circuit", 60.0), 0);
252
+ }
253
+
254
+ #[test]
255
+ fn test_memory_storage_event_log() {
256
+ let storage = MemoryStorage::new();
257
+
258
+ storage.record_success("test_circuit", 0.1);
259
+ storage.record_failure("test_circuit", 0.2);
260
+ storage.record_success("test_circuit", 0.3);
261
+
262
+ let log = storage.event_log("test_circuit", 10);
263
+ assert_eq!(log.len(), 3);
264
+ assert_eq!(log[0].kind, EventKind::Success);
265
+ assert_eq!(log[1].kind, EventKind::Failure);
266
+ assert_eq!(log[2].kind, EventKind::Success);
267
+ }
268
+
269
+ #[test]
270
+ fn test_memory_storage_max_events_cleanup() {
271
+ let storage = MemoryStorage::with_max_events(100);
272
+
273
+ for i in 0..150 {
274
+ storage.record_success("test_circuit", i as f64 * 0.01);
275
+ }
276
+
277
+ let events = storage.events.read().unwrap();
278
+ let circuit_events = events.get("test_circuit").unwrap();
279
+
280
+ assert!(circuit_events.len() <= 100);
281
+ }
282
+
283
+ #[test]
284
+ fn test_memory_storage_small_max_events() {
285
+ let storage = MemoryStorage::with_max_events(5);
286
+
287
+ for i in 0..20 {
288
+ storage.record_success("test_circuit", i as f64 * 0.01);
289
+ }
290
+
291
+ let events = storage.events.read().unwrap();
292
+ let circuit_events = events.get("test_circuit").unwrap();
293
+
294
+ assert!(
295
+ circuit_events.len() <= 5,
296
+ "Expected <= 5 events, got {}",
297
+ circuit_events.len()
298
+ );
299
+ }
300
+
301
+ #[test]
302
+ fn test_memory_storage_monotonic_time() {
303
+ let storage = MemoryStorage::new();
304
+
305
+ storage.record_success("test_circuit", 0.1);
306
+ let time1 = storage.monotonic_time();
307
+
308
+ std::thread::sleep(std::time::Duration::from_millis(10));
309
+
310
+ storage.record_success("test_circuit", 0.2);
311
+ let time2 = storage.monotonic_time();
312
+
313
+ assert!(time2 > time1);
314
+ assert_eq!(storage.success_count("test_circuit", 1.0), 2);
315
+ }
316
+
317
+ #[test]
318
+ fn test_null_storage_discards_events() {
319
+ let storage = NullStorage::new();
320
+
321
+ storage.record_success("test_circuit", 0.1);
322
+ storage.record_failure("test_circuit", 0.2);
323
+
324
+ assert_eq!(storage.success_count("test_circuit", 60.0), 0);
325
+ assert_eq!(storage.failure_count("test_circuit", 60.0), 0);
326
+ }
327
+
328
+ #[test]
329
+ fn test_null_storage_empty_event_log() {
330
+ let storage = NullStorage::new();
331
+
332
+ storage.record_success("test_circuit", 0.1);
333
+ storage.record_failure("test_circuit", 0.2);
334
+
335
+ let log = storage.event_log("test_circuit", 10);
336
+ assert_eq!(log.len(), 0);
337
+ }
338
+
339
+ #[test]
340
+ fn test_null_storage_clear_operations() {
341
+ let storage = NullStorage::new();
342
+
343
+ storage.clear("test_circuit");
344
+ storage.clear_all();
345
+
346
+ assert_eq!(storage.success_count("test_circuit", 60.0), 0);
347
+ }
348
+
349
+ #[test]
350
+ fn test_null_storage_monotonic_time() {
351
+ let storage = NullStorage::new();
352
+
353
+ let time1 = storage.monotonic_time();
354
+ std::thread::sleep(std::time::Duration::from_millis(10));
355
+ let time2 = storage.monotonic_time();
356
+
357
+ assert!(time2 > time1);
358
+ }
359
+
360
+ #[test]
361
+ fn test_null_storage_with_circuit_breaker() {
362
+ use std::sync::Arc;
363
+
364
+ let storage = Arc::new(NullStorage::new());
365
+ let mut circuit = crate::CircuitBreaker::builder("test")
366
+ .storage(storage)
367
+ .failure_threshold(3)
368
+ .build();
369
+
370
+ let _ = circuit.call(|| Err::<(), _>("error 1"));
371
+ let _ = circuit.call(|| Err::<(), _>("error 2"));
372
+ let _ = circuit.call(|| Err::<(), _>("error 3"));
373
+
374
+ assert!(circuit.is_closed());
375
+ assert!(!circuit.is_open());
376
+ }
377
+ }
@@ -0,0 +1,48 @@
1
+ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
2
+ #
3
+ # When uploading crates to the registry Cargo will automatically
4
+ # "normalize" Cargo.toml files for maximal compatibility
5
+ # with all versions of Cargo and also rewrite `path` dependencies
6
+ # to registry (e.g., crates.io) dependencies.
7
+ #
8
+ # If you are reading this file be aware that the original Cargo.toml
9
+ # will likely look very different (and much more reasonable).
10
+ # See Cargo.toml.orig for the original contents.
11
+
12
+ [package]
13
+ edition = "2024"
14
+ name = "breaker-machines"
15
+ version = "0.2.0"
16
+ authors = ["Abdelkader Boudih <terminale@gmail.com>"]
17
+ build = false
18
+ autolib = false
19
+ autobins = false
20
+ autoexamples = false
21
+ autotests = false
22
+ autobenches = false
23
+ description = "High-performance circuit breaker with fallback support and rate-based thresholds"
24
+ readme = "README.md"
25
+ keywords = [
26
+ "circuit-breaker",
27
+ "reliability",
28
+ "fault-tolerance",
29
+ ]
30
+ categories = ["concurrency"]
31
+ license = "MIT"
32
+ repository = "https://github.com/seuros/breaker_machines"
33
+ resolver = "2"
34
+
35
+ [lib]
36
+ name = "breaker_machines"
37
+ crate-type = ["lib"]
38
+ path = "src/lib.rs"
39
+
40
+ [[example]]
41
+ name = "basic"
42
+ path = "examples/basic.rs"
43
+
44
+ [dependencies.chrono-machines]
45
+ version = "0.2.1"
46
+
47
+ [dependencies.state-machines]
48
+ version = "0.6"
@@ -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,173 @@
1
+ //! Builder API for ergonomic circuit breaker configuration
2
+
3
+ use crate::{
4
+ MemoryStorage, StorageBackend,
5
+ callbacks::Callbacks,
6
+ circuit::{CircuitBreaker, CircuitContext, Config},
7
+ };
8
+ use std::sync::Arc;
9
+
10
+ /// Builder for creating circuit breakers with fluent API
11
+ pub struct CircuitBuilder {
12
+ name: String,
13
+ config: Config,
14
+ storage: Option<Arc<dyn StorageBackend>>,
15
+ callbacks: Callbacks,
16
+ }
17
+
18
+ impl CircuitBuilder {
19
+ /// Create a new builder for a circuit with the given name
20
+ pub fn new(name: impl Into<String>) -> Self {
21
+ Self {
22
+ name: name.into(),
23
+ config: Config::default(),
24
+ storage: None,
25
+ callbacks: Callbacks::new(),
26
+ }
27
+ }
28
+
29
+ /// Set the absolute failure threshold (number of failures to open circuit)
30
+ pub fn failure_threshold(mut self, threshold: usize) -> Self {
31
+ self.config.failure_threshold = Some(threshold);
32
+ self
33
+ }
34
+
35
+ /// Disable absolute failure threshold (use only rate-based)
36
+ pub fn disable_failure_threshold(mut self) -> Self {
37
+ self.config.failure_threshold = None;
38
+ self
39
+ }
40
+
41
+ /// Set the failure rate threshold (0.0-1.0)
42
+ /// Circuit opens when (failures / total_calls) >= this value
43
+ pub fn failure_rate(mut self, rate: f64) -> Self {
44
+ self.config.failure_rate_threshold = Some(rate.clamp(0.0, 1.0));
45
+ self
46
+ }
47
+
48
+ /// Set minimum number of calls before rate-based threshold is evaluated
49
+ pub fn minimum_calls(mut self, calls: usize) -> Self {
50
+ self.config.minimum_calls = calls;
51
+ self
52
+ }
53
+
54
+ /// Set the failure window in seconds
55
+ pub fn failure_window_secs(mut self, seconds: f64) -> Self {
56
+ self.config.failure_window_secs = seconds;
57
+ self
58
+ }
59
+
60
+ /// Set the half-open timeout in seconds
61
+ pub fn half_open_timeout_secs(mut self, seconds: f64) -> Self {
62
+ self.config.half_open_timeout_secs = seconds;
63
+ self
64
+ }
65
+
66
+ /// Set the success threshold (successes needed to close from half-open)
67
+ pub fn success_threshold(mut self, threshold: usize) -> Self {
68
+ self.config.success_threshold = threshold;
69
+ self
70
+ }
71
+
72
+ /// Set the jitter factor (0.0 = no jitter, 1.0 = full jitter)
73
+ /// Uses chrono-machines formula: timeout * (1 - jitter + rand * jitter)
74
+ pub fn jitter_factor(mut self, factor: f64) -> Self {
75
+ self.config.jitter_factor = factor;
76
+ self
77
+ }
78
+
79
+ /// Set custom storage backend
80
+ pub fn storage(mut self, storage: Arc<dyn StorageBackend>) -> Self {
81
+ self.storage = Some(storage);
82
+ self
83
+ }
84
+
85
+ /// Set callback for when circuit opens
86
+ pub fn on_open<F>(mut self, f: F) -> Self
87
+ where
88
+ F: Fn(&str) + Send + Sync + 'static,
89
+ {
90
+ self.callbacks.on_open = Some(Arc::new(f));
91
+ self
92
+ }
93
+
94
+ /// Set callback for when circuit closes
95
+ pub fn on_close<F>(mut self, f: F) -> Self
96
+ where
97
+ F: Fn(&str) + Send + Sync + 'static,
98
+ {
99
+ self.callbacks.on_close = Some(Arc::new(f));
100
+ self
101
+ }
102
+
103
+ /// Set callback for when circuit enters half-open
104
+ pub fn on_half_open<F>(mut self, f: F) -> Self
105
+ where
106
+ F: Fn(&str) + Send + Sync + 'static,
107
+ {
108
+ self.callbacks.on_half_open = Some(Arc::new(f));
109
+ self
110
+ }
111
+
112
+ /// Build the circuit breaker
113
+ pub fn build(self) -> CircuitBreaker {
114
+ let storage = self
115
+ .storage
116
+ .unwrap_or_else(|| Arc::new(MemoryStorage::new()));
117
+
118
+ let context = CircuitContext {
119
+ name: self.name,
120
+ config: self.config,
121
+ storage,
122
+ };
123
+
124
+ CircuitBreaker::with_context_and_callbacks(context, self.callbacks)
125
+ }
126
+ }
127
+
128
+ #[cfg(test)]
129
+ mod tests {
130
+ use super::*;
131
+
132
+ #[test]
133
+ fn test_builder_defaults() {
134
+ let circuit = CircuitBuilder::new("test").build();
135
+
136
+ assert_eq!(circuit.state_name(), "Closed");
137
+ assert!(circuit.is_closed());
138
+ }
139
+
140
+ #[test]
141
+ fn test_builder_custom_config() {
142
+ let circuit = CircuitBuilder::new("test")
143
+ .failure_threshold(10)
144
+ .failure_window_secs(120.0)
145
+ .half_open_timeout_secs(60.0)
146
+ .success_threshold(3)
147
+ .build();
148
+
149
+ assert!(circuit.is_closed());
150
+ }
151
+
152
+ #[test]
153
+ fn test_builder_with_callbacks() {
154
+ use std::sync::atomic::{AtomicBool, Ordering};
155
+
156
+ let opened = Arc::new(AtomicBool::new(false));
157
+ let opened_clone = opened.clone();
158
+
159
+ let mut circuit = CircuitBuilder::new("test")
160
+ .failure_threshold(2)
161
+ .on_open(move |_name| {
162
+ opened_clone.store(true, Ordering::SeqCst);
163
+ })
164
+ .build();
165
+
166
+ // Trigger failures to open circuit
167
+ let _ = circuit.call(|| Err::<(), _>("error 1"));
168
+ let _ = circuit.call(|| Err::<(), _>("error 2"));
169
+
170
+ // Callback should have been triggered
171
+ assert!(opened.load(Ordering::SeqCst));
172
+ }
173
+ }