use std::time::Duration;
use super::env_parsing::{
parse_duration_millis_from_env, parse_from_env, parse_optional_bool_from_env, ValidationBounds,
};
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct PartitionFailoverOptions {
circuit_breaker_enabled: bool,
circuit_breaker_enabled_override: Option<bool>,
read_failure_threshold: u32,
write_failure_threshold: u32,
counter_reset_window: Duration,
partition_unavailability_duration: Duration,
failback_sweep_interval: Duration,
consecutive_hedge_win_threshold: u32,
}
impl Default for PartitionFailoverOptions {
fn default() -> Self {
Self {
circuit_breaker_enabled: true, circuit_breaker_enabled_override: None,
read_failure_threshold: 10,
write_failure_threshold: 5,
counter_reset_window: Duration::from_millis(300_000),
partition_unavailability_duration: Duration::from_millis(5_000),
failback_sweep_interval: Duration::from_millis(300_000),
consecutive_hedge_win_threshold: 5,
}
}
}
impl PartitionFailoverOptions {
pub fn builder() -> PartitionFailoverOptionsBuilder {
PartitionFailoverOptionsBuilder::new()
}
pub fn circuit_breaker_enabled(&self) -> bool {
self.circuit_breaker_enabled
}
pub(crate) fn circuit_breaker_enabled_override(&self) -> Option<bool> {
self.circuit_breaker_enabled_override
}
pub fn read_failure_threshold(&self) -> u32 {
self.read_failure_threshold
}
pub fn write_failure_threshold(&self) -> u32 {
self.write_failure_threshold
}
pub fn counter_reset_window(&self) -> Duration {
self.counter_reset_window
}
pub fn partition_unavailability_duration(&self) -> Duration {
self.partition_unavailability_duration
}
pub fn failback_sweep_interval(&self) -> Duration {
self.failback_sweep_interval
}
pub fn consecutive_hedge_win_threshold(&self) -> u32 {
self.consecutive_hedge_win_threshold
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct PartitionFailoverOptionsBuilder {
circuit_breaker_enabled: Option<bool>,
circuit_breaker_enabled_override: Option<bool>,
read_failure_threshold: Option<u32>,
write_failure_threshold: Option<u32>,
counter_reset_window: Option<Duration>,
partition_unavailability_duration: Option<Duration>,
failback_sweep_interval: Option<Duration>,
consecutive_hedge_win_threshold: Option<u32>,
}
impl PartitionFailoverOptionsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_circuit_breaker_enabled(mut self, value: bool) -> Self {
self.circuit_breaker_enabled = Some(value);
self
}
#[cfg(test)]
pub(crate) fn with_circuit_breaker_enabled_override(mut self, value: bool) -> Self {
self.circuit_breaker_enabled_override = Some(value);
self
}
pub fn with_read_failure_threshold(mut self, value: u32) -> Self {
self.read_failure_threshold = Some(value);
self
}
pub fn with_write_failure_threshold(mut self, value: u32) -> Self {
self.write_failure_threshold = Some(value);
self
}
pub fn with_counter_reset_window(mut self, value: Duration) -> Self {
self.counter_reset_window = Some(value);
self
}
pub fn with_partition_unavailability_duration(mut self, value: Duration) -> Self {
self.partition_unavailability_duration = Some(value);
self
}
pub fn with_failback_sweep_interval(mut self, value: Duration) -> Self {
self.failback_sweep_interval = Some(value);
self
}
pub fn with_consecutive_hedge_win_threshold(mut self, value: u32) -> Self {
self.consecutive_hedge_win_threshold = Some(value);
self
}
pub fn build(self) -> crate::error::Result<PartitionFailoverOptions> {
let defaults = PartitionFailoverOptions::default();
let circuit_breaker_enabled = parse_from_env(
self.circuit_breaker_enabled,
"AZURE_COSMOS_PPCB_ENABLED",
defaults.circuit_breaker_enabled,
ValidationBounds::none(),
)?;
let circuit_breaker_enabled_override = parse_optional_bool_from_env(
self.circuit_breaker_enabled_override,
"AZURE_COSMOS_PPCB_ENABLED_OVERRIDE",
);
let read_failure_threshold = parse_from_env(
self.read_failure_threshold,
"AZURE_COSMOS_PPCB_READ_FAILURE_THRESHOLD",
defaults.read_failure_threshold,
ValidationBounds::min(1),
)?;
let write_failure_threshold = parse_from_env(
self.write_failure_threshold,
"AZURE_COSMOS_PPCB_WRITE_FAILURE_THRESHOLD",
defaults.write_failure_threshold,
ValidationBounds::min(1),
)?;
let counter_reset_window = parse_duration_millis_from_env(
self.counter_reset_window,
"AZURE_COSMOS_PPCB_COUNTER_RESET_WINDOW_MS",
defaults.counter_reset_window.as_millis() as u64,
1_000,
u64::MAX,
)?;
let partition_unavailability_duration = parse_duration_millis_from_env(
self.partition_unavailability_duration,
"AZURE_COSMOS_PPCB_PARTITION_UNAVAILABILITY_DURATION_MS",
defaults.partition_unavailability_duration.as_millis() as u64,
1_000,
u64::MAX,
)?;
let failback_sweep_interval = parse_duration_millis_from_env(
self.failback_sweep_interval,
"AZURE_COSMOS_PPCB_FAILBACK_SWEEP_INTERVAL_MS",
defaults.failback_sweep_interval.as_millis() as u64,
1_000,
u64::MAX,
)?;
let consecutive_hedge_win_threshold = parse_from_env(
self.consecutive_hedge_win_threshold,
"AZURE_COSMOS_PPCB_CONSECUTIVE_HEDGE_WIN_THRESHOLD",
defaults.consecutive_hedge_win_threshold,
ValidationBounds::min(1),
)?;
Ok(PartitionFailoverOptions {
circuit_breaker_enabled,
circuit_breaker_enabled_override,
read_failure_threshold,
write_failure_threshold,
counter_reset_window,
partition_unavailability_duration,
failback_sweep_interval,
consecutive_hedge_win_threshold,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_defaults_match_documented_values() {
let options = PartitionFailoverOptionsBuilder::new().build().unwrap();
assert!(options.circuit_breaker_enabled());
assert_eq!(options.read_failure_threshold(), 10);
assert_eq!(options.write_failure_threshold(), 5);
assert_eq!(options.counter_reset_window(), Duration::from_secs(5 * 60));
assert_eq!(
options.partition_unavailability_duration(),
Duration::from_secs(5)
);
assert_eq!(options.failback_sweep_interval(), Duration::from_secs(300));
assert_eq!(options.consecutive_hedge_win_threshold(), 5);
}
#[test]
fn builder_round_trips_custom_values() {
let options = PartitionFailoverOptionsBuilder::new()
.with_circuit_breaker_enabled(true)
.with_read_failure_threshold(20)
.with_write_failure_threshold(7)
.with_counter_reset_window(Duration::from_secs(60))
.with_partition_unavailability_duration(Duration::from_secs(30))
.with_failback_sweep_interval(Duration::from_secs(120))
.with_consecutive_hedge_win_threshold(3)
.build()
.unwrap();
assert!(options.circuit_breaker_enabled());
assert_eq!(options.read_failure_threshold(), 20);
assert_eq!(options.write_failure_threshold(), 7);
assert_eq!(options.counter_reset_window(), Duration::from_secs(60));
assert_eq!(
options.partition_unavailability_duration(),
Duration::from_secs(30)
);
assert_eq!(options.failback_sweep_interval(), Duration::from_secs(120));
assert_eq!(options.consecutive_hedge_win_threshold(), 3);
}
#[test]
fn circuit_breaker_override_unset_by_default() {
let options = PartitionFailoverOptionsBuilder::new().build().unwrap();
assert_eq!(options.circuit_breaker_enabled_override(), None);
}
#[test]
fn circuit_breaker_override_builder_value_round_trips() {
let off = PartitionFailoverOptionsBuilder::new()
.with_circuit_breaker_enabled_override(false)
.build()
.unwrap();
assert_eq!(off.circuit_breaker_enabled_override(), Some(false));
let on = PartitionFailoverOptionsBuilder::new()
.with_circuit_breaker_enabled_override(true)
.build()
.unwrap();
assert_eq!(on.circuit_breaker_enabled_override(), Some(true));
}
#[test]
fn read_failure_threshold_zero_rejected() {
let err = PartitionFailoverOptionsBuilder::new()
.with_read_failure_threshold(0)
.build()
.unwrap_err()
.to_string();
assert!(
err.contains("read_failure_threshold must be at least 1"),
"unexpected error: {err}",
);
}
#[test]
fn counter_reset_window_below_min_rejected() {
let err = PartitionFailoverOptionsBuilder::new()
.with_counter_reset_window(Duration::from_millis(500))
.build()
.unwrap_err()
.to_string();
assert!(
err.contains("counter_reset_window_ms must be at least 1000ms"),
"unexpected error: {err}",
);
}
#[test]
fn partition_unavailability_duration_below_min_rejected() {
let err = PartitionFailoverOptionsBuilder::new()
.with_partition_unavailability_duration(Duration::from_millis(50))
.build()
.unwrap_err()
.to_string();
assert!(
err.contains("partition_unavailability_duration_ms must be at least 1000ms"),
"unexpected error: {err}",
);
}
#[test]
fn failback_sweep_interval_below_min_rejected() {
let err = PartitionFailoverOptionsBuilder::new()
.with_failback_sweep_interval(Duration::from_millis(100))
.build()
.unwrap_err()
.to_string();
assert!(
err.contains("failback_sweep_interval_ms must be at least 1000ms"),
"unexpected error: {err}",
);
}
}