use std::time::Duration;
use crate::breaker::constants::MIN_SAMPLING_DURATION;
use crate::breaker::engine::probing::ProbesOptions;
#[derive(Debug, Clone, PartialEq)]
pub struct HalfOpenMode {
inner: Mode,
}
impl HalfOpenMode {
#[must_use]
pub fn quick() -> Self {
Self { inner: Mode::Quick }
}
#[must_use]
pub fn progressive(stage_duration: impl Into<Option<Duration>>) -> Self {
Self {
inner: Mode::Progressive(stage_duration.into().map(|d| d.max(MIN_SAMPLING_DURATION))),
}
}
pub(super) fn to_options(&self, default_stage_duration: Duration, failure_threshold: f32) -> ProbesOptions {
match self.inner {
Mode::Quick => ProbesOptions::quick(default_stage_duration),
Mode::Progressive(duration) => ProbesOptions::progressive(duration.unwrap_or(default_stage_duration), failure_threshold),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum Mode {
Quick,
Progressive(Option<Duration>),
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::breaker::engine::probing::ProbeOptions;
#[test]
fn quick_mode_creates_single_probe() {
let mode = HalfOpenMode::quick();
let options = mode.to_options(Duration::from_secs(30), 0.1);
let probes: Vec<_> = options.probes().collect();
assert_eq!(probes.len(), 1);
assert!(matches!(probes[0], ProbeOptions::SingleProbe { .. }));
}
#[test]
fn quick_mode_uses_default_duration() {
let mode = HalfOpenMode::quick();
let default = Duration::from_secs(30);
let options = mode.to_options(default, 0.1);
let probes: Vec<_> = options.probes().collect();
assert!(matches!(
&probes[0],
ProbeOptions::SingleProbe { cooldown } if *cooldown == default
));
}
#[test]
fn progressive_mode_creates_seven_probes() {
let mode = HalfOpenMode::progressive(None);
let options = mode.to_options(Duration::from_secs(30), 0.1);
assert_eq!(options.probes().len(), 7);
}
#[test]
fn progressive_mode_with_custom_duration() {
let custom = Duration::from_secs(45);
let mode = HalfOpenMode::progressive(custom);
let options = mode.to_options(Duration::from_secs(30), 0.1);
let probes: Vec<_> = options.probes().collect();
assert!(matches!(
&probes[0],
ProbeOptions::SingleProbe { cooldown } if *cooldown == custom
));
#[expect(clippy::float_cmp, reason = "Test")]
for probe in &probes[1..] {
if let ProbeOptions::HealthProbe(h) = probe {
assert_eq!(h.stage_duration(), custom);
assert_eq!(h.failure_threshold(), 0.1);
}
}
}
#[test]
fn progressive_mode_with_default_duration() {
let mode = HalfOpenMode::progressive(None);
let default = Duration::from_secs(60);
let options = mode.to_options(default, 0.1);
let probes: Vec<_> = options.probes().collect();
assert!(matches!(
&probes[0],
ProbeOptions::SingleProbe { cooldown } if *cooldown == default
));
for probe in &probes[1..] {
if let ProbeOptions::HealthProbe(h) = probe {
assert_eq!(h.stage_duration(), default);
}
}
}
#[test]
fn progressive_mode_accepts_various_inputs() {
let mode1 = HalfOpenMode::progressive(Duration::from_secs(10));
let mode2 = HalfOpenMode::progressive(Some(Duration::from_secs(10)));
let mode3 = HalfOpenMode::progressive(None);
assert!(matches!(mode1.inner, Mode::Progressive(Some(_))));
assert!(matches!(mode2.inner, Mode::Progressive(Some(_))));
assert!(matches!(mode3.inner, Mode::Progressive(None)));
}
#[test]
fn progressive_mode_clamps_min_duration() {
let mode = HalfOpenMode::progressive(Duration::from_millis(500));
assert!(matches!(mode.inner, Mode::Progressive(duration) if duration == Some(Duration::from_secs(1))));
}
}