breaker_machines 0.9.2 → 0.10.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d890ea2732633801242760b868020a4ad4d517693a71a91767b27ce8cfde282
4
- data.tar.gz: bb5a5b86d182b90cabdf0871d81f2b82ef58218e77613dc5f505067b6a39a63c
3
+ metadata.gz: cf8ad6f74f26b0e16224162a56d939fe9d74dff96b7d027310df893a4e8a7597
4
+ data.tar.gz: 36980ead62ae1571972b02f5d32daed76a040c5db26c2199d472482d2b82f553
5
5
  SHA512:
6
- metadata.gz: b84c67c557a067f768e9b16e73a0e5e9186e909d211632151b79a17558ac56c78d92a67fc9e86b90ef6fb6382eb2f00ab57dc984a7ff64d4e89291341155dbb5
7
- data.tar.gz: a1e3853602dab814c12e3581939ba95060e0ded7b023c906833530418a3b5071e3af29083303be06e1f98cf4e96028b4e1a1a9657910e18a0e69543940e2b1c8
6
+ metadata.gz: e719c01623af883b96dba7041b713e281ea9cc0212574eb0f924cac49e91a136112116ee0fd6930839c123b7822808617381c1b9b4fc7e1b012b6c88dd01bafd
7
+ data.tar.gz: 07eee7d0b5bbe8ba00abbfac9ce9849bf205b695ba4602c9a98bab477cb56ae9b558b9f1ba168d87cce35ff74d43a6931bfb7eba457ab00c998831fbb7a5376b
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "breaker-machines"
3
- version = "0.6.0"
3
+ version = "0.7.1"
4
4
  edition = "2024"
5
5
  authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
6
  license = "MIT"
@@ -1,5 +1,6 @@
1
1
  //! Callback system for circuit breaker state transitions
2
2
 
3
+ use std::panic::{AssertUnwindSafe, catch_unwind};
3
4
  use std::sync::Arc;
4
5
 
5
6
  /// Type alias for circuit breaker callback functions
@@ -22,21 +23,30 @@ impl Callbacks {
22
23
  }
23
24
  }
24
25
 
26
+ /// Trigger the on_open callback safely, catching any panics to prevent
27
+ /// unwinding across FFI boundaries.
25
28
  pub fn trigger_open(&self, circuit: &str) {
26
29
  if let Some(ref callback) = self.on_open {
27
- callback(circuit);
30
+ let cb = AssertUnwindSafe(callback);
31
+ let _ = catch_unwind(|| cb(circuit));
28
32
  }
29
33
  }
30
34
 
35
+ /// Trigger the on_close callback safely, catching any panics to prevent
36
+ /// unwinding across FFI boundaries.
31
37
  pub fn trigger_close(&self, circuit: &str) {
32
38
  if let Some(ref callback) = self.on_close {
33
- callback(circuit);
39
+ let cb = AssertUnwindSafe(callback);
40
+ let _ = catch_unwind(|| cb(circuit));
34
41
  }
35
42
  }
36
43
 
44
+ /// Trigger the on_half_open callback safely, catching any panics to prevent
45
+ /// unwinding across FFI boundaries.
37
46
  pub fn trigger_half_open(&self, circuit: &str) {
38
47
  if let Some(ref callback) = self.on_half_open {
39
- callback(circuit);
48
+ let cb = AssertUnwindSafe(callback);
49
+ let _ = catch_unwind(|| cb(circuit));
40
50
  }
41
51
  }
42
52
  }
@@ -56,3 +66,82 @@ impl std::fmt::Debug for Callbacks {
56
66
  .finish()
57
67
  }
58
68
  }
69
+
70
+ #[cfg(test)]
71
+ mod tests {
72
+ use super::*;
73
+ use std::sync::atomic::{AtomicBool, Ordering};
74
+
75
+ #[test]
76
+ fn test_callback_panic_safety() {
77
+ // Callbacks that panic should not crash the program
78
+ let callbacks = Callbacks {
79
+ on_open: Some(Arc::new(|_| panic!("intentional panic in on_open"))),
80
+ on_close: Some(Arc::new(|_| panic!("intentional panic in on_close"))),
81
+ on_half_open: Some(Arc::new(|_| panic!("intentional panic in on_half_open"))),
82
+ };
83
+
84
+ // These should not panic - the panics are caught internally
85
+ callbacks.trigger_open("test");
86
+ callbacks.trigger_close("test");
87
+ callbacks.trigger_half_open("test");
88
+ }
89
+
90
+ #[test]
91
+ fn test_callback_executes_successfully() {
92
+ let open_called = Arc::new(AtomicBool::new(false));
93
+ let close_called = Arc::new(AtomicBool::new(false));
94
+ let half_open_called = Arc::new(AtomicBool::new(false));
95
+
96
+ let open_clone = open_called.clone();
97
+ let close_clone = close_called.clone();
98
+ let half_open_clone = half_open_called.clone();
99
+
100
+ let callbacks = Callbacks {
101
+ on_open: Some(Arc::new(move |_| {
102
+ open_clone.store(true, Ordering::SeqCst);
103
+ })),
104
+ on_close: Some(Arc::new(move |_| {
105
+ close_clone.store(true, Ordering::SeqCst);
106
+ })),
107
+ on_half_open: Some(Arc::new(move |_| {
108
+ half_open_clone.store(true, Ordering::SeqCst);
109
+ })),
110
+ };
111
+
112
+ callbacks.trigger_open("test");
113
+ callbacks.trigger_close("test");
114
+ callbacks.trigger_half_open("test");
115
+
116
+ assert!(
117
+ open_called.load(Ordering::SeqCst),
118
+ "on_open should be called"
119
+ );
120
+ assert!(
121
+ close_called.load(Ordering::SeqCst),
122
+ "on_close should be called"
123
+ );
124
+ assert!(
125
+ half_open_called.load(Ordering::SeqCst),
126
+ "on_half_open should be called"
127
+ );
128
+ }
129
+
130
+ #[test]
131
+ fn test_callback_receives_circuit_name() {
132
+ let received_name = Arc::new(std::sync::Mutex::new(String::new()));
133
+ let name_clone = received_name.clone();
134
+
135
+ let callbacks = Callbacks {
136
+ on_open: Some(Arc::new(move |name| {
137
+ *name_clone.lock().unwrap() = name.to_string();
138
+ })),
139
+ on_close: None,
140
+ on_half_open: None,
141
+ };
142
+
143
+ callbacks.trigger_open("my_circuit");
144
+
145
+ assert_eq!(*received_name.lock().unwrap(), "my_circuit");
146
+ }
147
+ }
@@ -482,11 +482,12 @@ impl CircuitBreaker {
482
482
  // Try to trip the circuit
483
483
  let result = self.machine.handle(CircuitEvent::Trip);
484
484
  if result.is_ok() {
485
- // Transition succeeded - update opened_at timestamp
486
- if let Some(data) = self.machine.open_data_mut() {
487
- data.opened_at = self.context.storage.monotonic_time();
485
+ self.mark_open();
486
+ } else if self.machine.current_state() == "HalfOpen" {
487
+ // Failure did not reopen the circuit; reset consecutive successes
488
+ if let Some(data) = self.machine.half_open_data_mut() {
489
+ data.consecutive_successes = 0;
488
490
  }
489
- self.callbacks.trigger_open(&self.context.name);
490
491
  }
491
492
  }
492
493
 
@@ -495,6 +496,39 @@ impl CircuitBreaker {
495
496
  }
496
497
  }
497
498
 
499
+ /// Record a successful operation and drive HalfOpen -> Closed transitions
500
+ pub fn record_success_and_maybe_close(&mut self, duration: f64) {
501
+ self.context
502
+ .storage
503
+ .record_success(&self.context.name, duration);
504
+
505
+ if self.machine.current_state() == "HalfOpen" {
506
+ if let Some(data) = self.machine.half_open_data_mut() {
507
+ data.consecutive_successes += 1;
508
+ }
509
+
510
+ if self.machine.handle(CircuitEvent::Close).is_ok() {
511
+ self.callbacks.trigger_close(&self.context.name);
512
+ }
513
+ }
514
+ }
515
+
516
+ /// Record a failed operation and attempt to trip the circuit
517
+ pub fn record_failure_and_maybe_trip(&mut self, duration: f64) {
518
+ self.context
519
+ .storage
520
+ .record_failure(&self.context.name, duration);
521
+
522
+ let result = self.machine.handle(CircuitEvent::Trip);
523
+ if result.is_ok() {
524
+ self.mark_open();
525
+ } else if self.machine.current_state() == "HalfOpen"
526
+ && let Some(data) = self.machine.half_open_data_mut()
527
+ {
528
+ data.consecutive_successes = 0;
529
+ }
530
+ }
531
+
498
532
  /// Record a successful operation (for manual tracking)
499
533
  pub fn record_success(&self, duration: f64) {
500
534
  self.context
@@ -512,7 +546,12 @@ impl CircuitBreaker {
512
546
  /// Check failure threshold and attempt to trip the circuit
513
547
  /// This should be called after record_failure() when not using call()
514
548
  pub fn check_and_trip(&mut self) -> bool {
515
- self.machine.handle(CircuitEvent::Trip).is_ok()
549
+ if self.machine.handle(CircuitEvent::Trip).is_ok() {
550
+ self.mark_open();
551
+ true
552
+ } else {
553
+ false
554
+ }
516
555
  }
517
556
 
518
557
  /// Check if circuit is open
@@ -536,6 +575,14 @@ impl CircuitBreaker {
536
575
  // Recreate machine in Closed state
537
576
  self.machine = DynamicCircuit::new(self.context.clone());
538
577
  }
578
+
579
+ /// Apply Open-state bookkeeping (timestamp + callback)
580
+ fn mark_open(&mut self) {
581
+ if let Some(data) = self.machine.open_data_mut() {
582
+ data.opened_at = self.context.storage.monotonic_time();
583
+ }
584
+ self.callbacks.trigger_open(&self.context.name);
585
+ }
539
586
  }
540
587
 
541
588
  #[cfg(test)]
@@ -1153,4 +1200,237 @@ mod tests {
1153
1200
  let result = circuit.call(|| Ok::<_, String>("should fail"));
1154
1201
  assert!(matches!(result, Err(CircuitError::Open { .. })));
1155
1202
  }
1203
+
1204
+ #[test]
1205
+ fn test_check_and_trip_sets_opened_at_and_callback() {
1206
+ use std::sync::atomic::{AtomicBool, Ordering};
1207
+
1208
+ let opened = Arc::new(AtomicBool::new(false));
1209
+ let opened_clone = opened.clone();
1210
+
1211
+ let mut circuit = CircuitBreaker::builder("test")
1212
+ .failure_threshold(1)
1213
+ .on_open(move |_name| {
1214
+ opened_clone.store(true, Ordering::SeqCst);
1215
+ })
1216
+ .build();
1217
+
1218
+ circuit.record_failure(0.1);
1219
+ let tripped = circuit.check_and_trip();
1220
+
1221
+ assert!(tripped, "Trip should succeed");
1222
+ assert!(circuit.is_open(), "Circuit should be open after trip");
1223
+
1224
+ let opened_at = circuit
1225
+ .machine
1226
+ .open_data()
1227
+ .expect("Open data should be present")
1228
+ .opened_at;
1229
+
1230
+ assert!(opened_at > 0.0, "opened_at should be set");
1231
+ assert!(
1232
+ opened.load(Ordering::SeqCst),
1233
+ "on_open callback should fire"
1234
+ );
1235
+ }
1236
+
1237
+ #[test]
1238
+ fn test_half_open_failure_resets_consecutive_successes() {
1239
+ let mut circuit = CircuitBreaker::builder("test")
1240
+ .failure_threshold(2)
1241
+ .half_open_timeout_secs(0.001)
1242
+ .success_threshold(2)
1243
+ .build();
1244
+
1245
+ // Open the circuit
1246
+ let _ = circuit.call(|| Err::<(), _>("error 1"));
1247
+ let _ = circuit.call(|| Err::<(), _>("error 2"));
1248
+ assert!(circuit.is_open());
1249
+
1250
+ // Move to HalfOpen
1251
+ if let Some(data) = circuit.machine.open_data_mut() {
1252
+ data.opened_at = circuit.context.storage.monotonic_time();
1253
+ }
1254
+ std::thread::sleep(std::time::Duration::from_millis(2));
1255
+ circuit
1256
+ .machine
1257
+ .handle(CircuitEvent::AttemptReset)
1258
+ .expect("Should transition to HalfOpen");
1259
+ assert_eq!(circuit.machine.current_state(), "HalfOpen");
1260
+
1261
+ // Clear counts to simulate expired failure window
1262
+ circuit.context.storage.clear("test");
1263
+
1264
+ // First success increments consecutive count
1265
+ let _ = circuit.call(|| Ok::<_, String>("ok"));
1266
+ assert_eq!(
1267
+ circuit
1268
+ .machine
1269
+ .half_open_data()
1270
+ .expect("HalfOpen data")
1271
+ .consecutive_successes,
1272
+ 1
1273
+ );
1274
+
1275
+ // Failure below threshold should not reopen circuit but should reset counter
1276
+ let _ = circuit.call(|| Err::<(), _>("fail"));
1277
+ assert_eq!(circuit.machine.current_state(), "HalfOpen");
1278
+ assert_eq!(
1279
+ circuit
1280
+ .machine
1281
+ .half_open_data()
1282
+ .expect("HalfOpen data")
1283
+ .consecutive_successes,
1284
+ 0
1285
+ );
1286
+
1287
+ // Next success starts count from 1 again
1288
+ let _ = circuit.call(|| Ok::<_, String>("ok2"));
1289
+ assert_eq!(
1290
+ circuit
1291
+ .machine
1292
+ .half_open_data()
1293
+ .expect("HalfOpen data")
1294
+ .consecutive_successes,
1295
+ 1
1296
+ );
1297
+ }
1298
+
1299
+ #[test]
1300
+ fn test_jitter_distribution_within_bounds() {
1301
+ // Test that jitter produces values within expected bounds
1302
+ // With 25% jitter on 1000ms base, expect 750-1000ms range
1303
+ let storage = Arc::new(crate::MemoryStorage::new());
1304
+ let base_timeout = 1.0; // 1 second
1305
+ let jitter_factor = 0.25;
1306
+
1307
+ let config = Config {
1308
+ failure_threshold: Some(1),
1309
+ half_open_timeout_secs: base_timeout,
1310
+ jitter_factor,
1311
+ ..Default::default()
1312
+ };
1313
+
1314
+ let ctx = CircuitContext {
1315
+ failure_classifier: None,
1316
+ bulkhead: None,
1317
+ name: "jitter_test".to_string(),
1318
+ config,
1319
+ storage: storage.clone(),
1320
+ };
1321
+
1322
+ // Run 50 iterations and collect timeout values
1323
+ let mut min_seen = f64::MAX;
1324
+ let mut max_seen = f64::MIN;
1325
+
1326
+ for _ in 0..50 {
1327
+ storage.record_failure("jitter_test", 0.1);
1328
+ let mut circuit = DynamicCircuit::new(ctx.clone());
1329
+ circuit.handle(CircuitEvent::Trip).expect("Should open");
1330
+
1331
+ if let Some(data) = circuit.open_data_mut() {
1332
+ data.opened_at = storage.monotonic_time();
1333
+ }
1334
+
1335
+ // Calculate what the jittered timeout would be
1336
+ let policy = chrono_machines::Policy {
1337
+ max_attempts: 1,
1338
+ base_delay_ms: (base_timeout * 1000.0) as u64,
1339
+ multiplier: 1.0,
1340
+ max_delay_ms: (base_timeout * 1000.0) as u64,
1341
+ };
1342
+ let timeout_ms = policy.calculate_delay(1, jitter_factor);
1343
+ let timeout_secs = (timeout_ms as f64) / 1000.0;
1344
+
1345
+ min_seen = min_seen.min(timeout_secs);
1346
+ max_seen = max_seen.max(timeout_secs);
1347
+
1348
+ storage.clear("jitter_test");
1349
+ }
1350
+
1351
+ // With 25% jitter, minimum should be ~0.75s (75% of base)
1352
+ // Maximum should be ~1.0s (100% of base)
1353
+ let min_expected = base_timeout * (1.0 - jitter_factor);
1354
+ let max_expected = base_timeout;
1355
+
1356
+ assert!(
1357
+ min_seen >= min_expected - 0.01,
1358
+ "Minimum jittered timeout {} should be >= {}",
1359
+ min_seen,
1360
+ min_expected
1361
+ );
1362
+ assert!(
1363
+ max_seen <= max_expected + 0.01,
1364
+ "Maximum jittered timeout {} should be <= {}",
1365
+ max_seen,
1366
+ max_expected
1367
+ );
1368
+ }
1369
+
1370
+ #[test]
1371
+ fn test_jitter_produces_variance() {
1372
+ // Test that jitter actually produces different values (not all same)
1373
+ let storage = Arc::new(crate::MemoryStorage::new());
1374
+
1375
+ let config = Config {
1376
+ failure_threshold: Some(1),
1377
+ half_open_timeout_secs: 1.0,
1378
+ jitter_factor: 0.5, // 50% jitter for more variance
1379
+ ..Default::default()
1380
+ };
1381
+
1382
+ let ctx = CircuitContext {
1383
+ failure_classifier: None,
1384
+ bulkhead: None,
1385
+ name: "jitter_variance".to_string(),
1386
+ config,
1387
+ storage: storage.clone(),
1388
+ };
1389
+
1390
+ let mut values = std::collections::HashSet::new();
1391
+
1392
+ for _ in 0..20 {
1393
+ let policy = chrono_machines::Policy {
1394
+ max_attempts: 1,
1395
+ base_delay_ms: 1000,
1396
+ multiplier: 1.0,
1397
+ max_delay_ms: 1000,
1398
+ };
1399
+ let timeout_ms = policy.calculate_delay(1, 0.5);
1400
+ values.insert(timeout_ms);
1401
+ }
1402
+
1403
+ // With 50% jitter over 20 iterations, we should see at least 2 different values
1404
+ // (statistically, seeing all same values is extremely unlikely)
1405
+ assert!(
1406
+ values.len() >= 2,
1407
+ "Jitter should produce variance, got {} unique values",
1408
+ values.len()
1409
+ );
1410
+ }
1411
+
1412
+ #[test]
1413
+ fn test_zero_jitter_produces_constant_timeout() {
1414
+ // Test that 0% jitter always produces the same timeout
1415
+ let policy = chrono_machines::Policy {
1416
+ max_attempts: 1,
1417
+ base_delay_ms: 1000,
1418
+ multiplier: 1.0,
1419
+ max_delay_ms: 1000,
1420
+ };
1421
+
1422
+ let mut values = std::collections::HashSet::new();
1423
+
1424
+ for _ in 0..10 {
1425
+ let timeout_ms = policy.calculate_delay(1, 0.0);
1426
+ values.insert(timeout_ms);
1427
+ }
1428
+
1429
+ assert_eq!(
1430
+ values.len(),
1431
+ 1,
1432
+ "Zero jitter should produce constant timeout"
1433
+ );
1434
+ assert!(values.contains(&1000), "Timeout should be exactly 1000ms");
1435
+ }
1156
1436
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "breaker_machines_native"
3
- version = "0.5.1"
3
+ version = "0.5.3"
4
4
  edition = "2024"
5
5
  authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
12
  breaker-machines = { workspace = true }
13
- magnus = { version = "0.7", features = ["embed"] }
13
+ magnus = { version = "0.8.2", features = ["embed"] }
14
14
 
15
15
  [build-dependencies]
16
16
  rb-sys = "0.9"
@@ -54,27 +54,23 @@ impl RubyStorage {
54
54
  }
55
55
 
56
56
  /// Get event log for a circuit (returns array of hashes)
57
- fn event_log(&self, circuit_name: String, limit: usize) -> RArray {
58
- let events = self.inner.event_log(&circuit_name, limit);
59
- let array = RArray::new();
57
+ fn event_log(ruby: &Ruby, storage: &RubyStorage, circuit_name: String, limit: usize) -> RArray {
58
+ let events = storage.inner.event_log(&circuit_name, limit);
59
+ let array = ruby.ary_new();
60
60
 
61
61
  for event in events {
62
- // Create a Ruby hash for each event
63
- let hash = RHash::new();
64
-
65
- // Set event type
62
+ let hash = ruby.hash_new();
66
63
  let type_sym = match event.kind {
67
64
  EventKind::Success => "success",
68
65
  EventKind::Failure => "failure",
69
66
  };
70
67
 
71
- let _ = hash.aset(magnus::Symbol::new("type"), type_sym);
72
- let _ = hash.aset(magnus::Symbol::new("timestamp"), event.timestamp);
68
+ let _ = hash.aset(ruby.to_symbol("type"), type_sym);
69
+ let _ = hash.aset(ruby.to_symbol("timestamp"), event.timestamp);
73
70
  let _ = hash.aset(
74
- magnus::Symbol::new("duration_ms"),
71
+ ruby.to_symbol("duration_ms"),
75
72
  (event.duration * 1000.0).round(),
76
73
  );
77
-
78
74
  let _ = array.push(hash);
79
75
  }
80
76
 
@@ -97,41 +93,41 @@ impl RubyCircuit {
97
93
  /// - failure_window_secs: Time window for counting failures (default: 60.0)
98
94
  /// - half_open_timeout_secs: Timeout before attempting reset (default: 30.0)
99
95
  /// - success_threshold: Successes needed to close from half-open (default: 2)
100
- fn new(name: String, config_hash: RHash) -> Result<Self, Error> {
96
+ fn new(ruby: &Ruby, name: String, config_hash: RHash) -> Result<Self, Error> {
101
97
  use magnus::TryConvert;
102
98
 
103
99
  // Extract config values with proper type conversion
104
100
  let failure_threshold: usize = config_hash
105
- .get(magnus::Symbol::new("failure_threshold"))
101
+ .get(ruby.to_symbol("failure_threshold"))
106
102
  .and_then(|v| usize::try_convert(v).ok())
107
103
  .unwrap_or(5);
108
104
 
109
105
  let failure_window_secs: f64 = config_hash
110
- .get(magnus::Symbol::new("failure_window_secs"))
106
+ .get(ruby.to_symbol("failure_window_secs"))
111
107
  .and_then(|v| f64::try_convert(v).ok())
112
108
  .unwrap_or(60.0);
113
109
 
114
110
  let half_open_timeout_secs: f64 = config_hash
115
- .get(magnus::Symbol::new("half_open_timeout_secs"))
111
+ .get(ruby.to_symbol("half_open_timeout_secs"))
116
112
  .and_then(|v| f64::try_convert(v).ok())
117
113
  .unwrap_or(30.0);
118
114
 
119
115
  let success_threshold: usize = config_hash
120
- .get(magnus::Symbol::new("success_threshold"))
116
+ .get(ruby.to_symbol("success_threshold"))
121
117
  .and_then(|v| usize::try_convert(v).ok())
122
118
  .unwrap_or(2);
123
119
 
124
120
  let jitter_factor: f64 = config_hash
125
- .get(magnus::Symbol::new("jitter_factor"))
121
+ .get(ruby.to_symbol("jitter_factor"))
126
122
  .and_then(|v| f64::try_convert(v).ok())
127
123
  .unwrap_or(0.0);
128
124
 
129
125
  let failure_rate_threshold: Option<f64> = config_hash
130
- .get(magnus::Symbol::new("failure_rate_threshold"))
126
+ .get(ruby.to_symbol("failure_rate_threshold"))
131
127
  .and_then(|v| f64::try_convert(v).ok());
132
128
 
133
129
  let minimum_calls: usize = config_hash
134
- .get(magnus::Symbol::new("minimum_calls"))
130
+ .get(ruby.to_symbol("minimum_calls"))
135
131
  .and_then(|v| usize::try_convert(v).ok())
136
132
  .unwrap_or(20);
137
133
 
@@ -152,14 +148,16 @@ impl RubyCircuit {
152
148
 
153
149
  /// Record a successful operation
154
150
  fn record_success(&self, duration: f64) {
155
- self.inner.borrow().record_success(duration);
151
+ self.inner
152
+ .borrow_mut()
153
+ .record_success_and_maybe_close(duration);
156
154
  }
157
155
 
158
156
  /// Record a failed operation and attempt to trip the circuit
159
157
  fn record_failure(&self, duration: f64) {
160
- let mut circuit = self.inner.borrow_mut();
161
- circuit.record_failure(duration);
162
- circuit.check_and_trip();
158
+ self.inner
159
+ .borrow_mut()
160
+ .record_failure_and_maybe_trip(duration);
163
161
  }
164
162
 
165
163
  /// Check if circuit is open
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.9.2'
4
+ VERSION = '0.10.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: breaker_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih