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,177 @@
1
+ //! Failure classification for exception filtering
2
+ //!
3
+ //! This module provides traits and types for determining which errors
4
+ //! should trip the circuit breaker vs. being ignored.
5
+
6
+ use std::any::Any;
7
+
8
+ /// Context provided to failure classifiers for error evaluation
9
+ #[derive(Debug)]
10
+ pub struct FailureContext<'a> {
11
+ /// Circuit name
12
+ pub circuit_name: &'a str,
13
+ /// The error that occurred (can be downcast to specific types)
14
+ pub error: &'a dyn Any,
15
+ /// Duration of the failed call in seconds
16
+ pub duration: f64,
17
+ }
18
+
19
+ /// Trait for classifying failures - determines if an error should trip the circuit
20
+ ///
21
+ /// Implementors can inspect the error type and context to decide whether
22
+ /// this particular failure should count toward opening the circuit.
23
+ ///
24
+ /// # Examples
25
+ ///
26
+ /// ```rust
27
+ /// use breaker_machines::{FailureClassifier, FailureContext};
28
+ /// use std::sync::Arc;
29
+ ///
30
+ /// #[derive(Debug)]
31
+ /// struct ServerErrorClassifier;
32
+ ///
33
+ /// impl FailureClassifier for ServerErrorClassifier {
34
+ /// fn should_trip(&self, ctx: &FailureContext<'_>) -> bool {
35
+ /// // Only trip on server errors (5xx), not client errors (4xx)
36
+ /// // This would require your error type to be downcast-able
37
+ /// true // Default: trip on all errors
38
+ /// }
39
+ /// }
40
+ /// ```
41
+ pub trait FailureClassifier: Send + Sync + std::fmt::Debug {
42
+ /// Determine if this error should count as a failure for circuit breaker logic
43
+ ///
44
+ /// Returns `true` if the error should trip the circuit, `false` to ignore it.
45
+ fn should_trip(&self, ctx: &FailureContext<'_>) -> bool;
46
+ }
47
+
48
+ /// Default classifier that trips on all errors
49
+ #[derive(Debug, Clone, Copy)]
50
+ pub struct DefaultClassifier;
51
+
52
+ impl FailureClassifier for DefaultClassifier {
53
+ fn should_trip(&self, _ctx: &FailureContext<'_>) -> bool {
54
+ true // All errors trip the circuit (backward compatible)
55
+ }
56
+ }
57
+
58
+ impl Default for DefaultClassifier {
59
+ fn default() -> Self {
60
+ Self
61
+ }
62
+ }
63
+
64
+ /// Predicate-based classifier using a closure
65
+ ///
66
+ /// Allows using simple closures for common filtering patterns.
67
+ pub struct PredicateClassifier<F>
68
+ where
69
+ F: Fn(&FailureContext<'_>) -> bool + Send + Sync,
70
+ {
71
+ predicate: F,
72
+ }
73
+
74
+ impl<F> PredicateClassifier<F>
75
+ where
76
+ F: Fn(&FailureContext<'_>) -> bool + Send + Sync,
77
+ {
78
+ /// Create a new predicate-based classifier
79
+ pub fn new(predicate: F) -> Self {
80
+ Self { predicate }
81
+ }
82
+ }
83
+
84
+ impl<F> FailureClassifier for PredicateClassifier<F>
85
+ where
86
+ F: Fn(&FailureContext<'_>) -> bool + Send + Sync,
87
+ {
88
+ fn should_trip(&self, ctx: &FailureContext<'_>) -> bool {
89
+ (self.predicate)(ctx)
90
+ }
91
+ }
92
+
93
+ impl<F> std::fmt::Debug for PredicateClassifier<F>
94
+ where
95
+ F: Fn(&FailureContext<'_>) -> bool + Send + Sync,
96
+ {
97
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98
+ f.debug_struct("PredicateClassifier")
99
+ .field("predicate", &"<closure>")
100
+ .finish()
101
+ }
102
+ }
103
+
104
+ #[cfg(test)]
105
+ mod tests {
106
+ use super::*;
107
+
108
+ #[test]
109
+ fn test_default_classifier_trips_all() {
110
+ let classifier = DefaultClassifier;
111
+ let ctx = FailureContext {
112
+ circuit_name: "test",
113
+ error: &"any error" as &dyn Any,
114
+ duration: 0.1,
115
+ };
116
+
117
+ assert!(classifier.should_trip(&ctx));
118
+ }
119
+
120
+ #[test]
121
+ fn test_predicate_classifier() {
122
+ // Classifier that only trips on slow errors
123
+ let classifier = PredicateClassifier::new(|ctx| ctx.duration > 1.0);
124
+
125
+ let fast_ctx = FailureContext {
126
+ circuit_name: "test",
127
+ error: &"fast error" as &dyn Any,
128
+ duration: 0.5,
129
+ };
130
+
131
+ let slow_ctx = FailureContext {
132
+ circuit_name: "test",
133
+ error: &"slow error" as &dyn Any,
134
+ duration: 2.0,
135
+ };
136
+
137
+ assert!(!classifier.should_trip(&fast_ctx));
138
+ assert!(classifier.should_trip(&slow_ctx));
139
+ }
140
+
141
+ #[test]
142
+ fn test_error_type_downcast() {
143
+ #[derive(Debug)]
144
+ struct MyError {
145
+ is_server_error: bool,
146
+ }
147
+
148
+ let server_error = MyError {
149
+ is_server_error: true,
150
+ };
151
+ let client_error = MyError {
152
+ is_server_error: false,
153
+ };
154
+
155
+ let classifier = PredicateClassifier::new(|ctx| {
156
+ ctx.error
157
+ .downcast_ref::<MyError>()
158
+ .map(|e| e.is_server_error)
159
+ .unwrap_or(true) // Trip on unknown errors
160
+ });
161
+
162
+ let server_ctx = FailureContext {
163
+ circuit_name: "test",
164
+ error: &server_error as &dyn Any,
165
+ duration: 0.1,
166
+ };
167
+
168
+ let client_ctx = FailureContext {
169
+ circuit_name: "test",
170
+ error: &client_error as &dyn Any,
171
+ duration: 0.1,
172
+ };
173
+
174
+ assert!(classifier.should_trip(&server_ctx));
175
+ assert!(!classifier.should_trip(&client_ctx));
176
+ }
177
+ }
@@ -0,0 +1,47 @@
1
+ //! Error types for circuit breaker operations
2
+
3
+ use std::error::Error;
4
+ use std::fmt;
5
+
6
+ /// Errors that can occur during circuit breaker operations
7
+ #[derive(Debug)]
8
+ pub enum CircuitError<E = Box<dyn Error + Send + Sync>> {
9
+ /// Circuit is open, calls are being rejected
10
+ Open { circuit: String, opened_at: f64 },
11
+ /// Half-open request limit has been reached
12
+ HalfOpenLimitReached { circuit: String },
13
+ /// Bulkhead is at capacity, cannot acquire permit
14
+ BulkheadFull { circuit: String, limit: usize },
15
+ /// The wrapped operation failed
16
+ Execution(E),
17
+ }
18
+
19
+ impl<E: fmt::Display> fmt::Display for CircuitError<E> {
20
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21
+ match self {
22
+ CircuitError::Open { circuit, opened_at } => {
23
+ write!(f, "Circuit '{}' is open (opened at {})", circuit, opened_at)
24
+ }
25
+ CircuitError::HalfOpenLimitReached { circuit } => {
26
+ write!(f, "Circuit '{}' half-open request limit reached", circuit)
27
+ }
28
+ CircuitError::BulkheadFull { circuit, limit } => {
29
+ write!(
30
+ f,
31
+ "Circuit '{}' bulkhead is full (limit: {})",
32
+ circuit, limit
33
+ )
34
+ }
35
+ CircuitError::Execution(e) => write!(f, "Circuit execution failed: {}", e),
36
+ }
37
+ }
38
+ }
39
+
40
+ impl<E: Error + 'static> Error for CircuitError<E> {
41
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
42
+ match self {
43
+ CircuitError::Execution(e) => Some(e),
44
+ _ => None,
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,62 @@
1
+ //! BreakerMachines - High-performance circuit breaker implementation
2
+ //!
3
+ //! This crate provides a complete circuit breaker implementation with:
4
+ //! - Thread-safe event storage with sliding window calculations
5
+ //! - State machine for circuit breaker lifecycle (Closed → Open → HalfOpen)
6
+ //! - Monotonic time tracking to prevent NTP clock skew issues
7
+ //! - Configurable failure thresholds and timeouts
8
+ //!
9
+ //! # Example
10
+ //!
11
+ //! ```rust
12
+ //! use breaker_machines::CircuitBreaker;
13
+ //!
14
+ //! let mut circuit = CircuitBreaker::builder("my_service")
15
+ //! .failure_threshold(5)
16
+ //! .failure_window_secs(60.0)
17
+ //! .half_open_timeout_secs(30.0)
18
+ //! .success_threshold(2)
19
+ //! .on_open(|name| println!("Circuit {} opened!", name))
20
+ //! .build();
21
+ //!
22
+ //! // Execute with circuit protection
23
+ //! let result = circuit.call(|| {
24
+ //! // Your service call here
25
+ //! Ok::<_, String>("success")
26
+ //! });
27
+ //!
28
+ //! // Check circuit state
29
+ //! if circuit.is_open() {
30
+ //! println!("Circuit is open, skipping call");
31
+ //! }
32
+ //! ```
33
+
34
+ pub mod builder;
35
+ pub mod bulkhead;
36
+ pub mod callbacks;
37
+ pub mod circuit;
38
+ pub mod classifier;
39
+ pub mod errors;
40
+ pub mod storage;
41
+
42
+ pub use builder::CircuitBuilder;
43
+ pub use bulkhead::{BulkheadGuard, BulkheadSemaphore};
44
+ pub use circuit::{CallOptions, CircuitBreaker, Config, FallbackContext};
45
+ pub use classifier::{DefaultClassifier, FailureClassifier, FailureContext, PredicateClassifier};
46
+ pub use errors::CircuitError;
47
+ pub use storage::{MemoryStorage, NullStorage, StorageBackend};
48
+
49
+ /// Event type for circuit breaker operations
50
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
51
+ pub enum EventKind {
52
+ Success,
53
+ Failure,
54
+ }
55
+
56
+ /// A single event recorded by the circuit breaker
57
+ #[derive(Debug, Clone)]
58
+ pub struct Event {
59
+ pub kind: EventKind,
60
+ pub timestamp: f64,
61
+ pub duration: f64,
62
+ }
@@ -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
+ }