use std::time::Duration;
use engenho_apiserver::ServerError;
use engenho_store::StoreError;
use serde::{Deserialize, Serialize};
use crate::error::RuntimeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureClass {
Backoff,
Hold,
}
impl FailureClass {
#[must_use]
pub fn of(error: &RuntimeError) -> Self {
match error {
RuntimeError::Config(_)
| RuntimeError::Unhonoured(_)
| RuntimeError::Scheduler(_)
| RuntimeError::BackendRefused(_)
| RuntimeError::ListenAddr { .. }
| RuntimeError::ExtraSan { .. }
| RuntimeError::PublicCaOnReachableAddress { .. }
| RuntimeError::Kubeconfig(_)
| RuntimeError::DataDirMoved { .. }
| RuntimeError::BootCancelled { .. }
| RuntimeError::StoreStillShared { .. } => Self::Hold,
RuntimeError::LeadershipTimeout { .. }
| RuntimeError::ContainerRuntimeUnavailable { .. }
| RuntimeError::KubeconfigIo { .. }
| RuntimeError::NodeRegistration(_) => Self::Backoff,
RuntimeError::Store(store) => Self::of_store(store),
RuntimeError::Server(server) => Self::of_server(server),
}
}
const fn of_store(error: &StoreError) -> Self {
match error {
StoreError::ConfigInvalid(_) => Self::Hold,
StoreError::InitializeFailed(_)
| StoreError::ClientWriteFailed(_)
| StoreError::Fatal(_)
| StoreError::Persist(_) => Self::Backoff,
}
}
const fn of_server(error: &ServerError) -> Self {
match error {
ServerError::Bind(_) | ServerError::Serve(_) => Self::Backoff,
ServerError::Tls(_) | ServerError::Pki(_) => Self::Hold,
}
}
}
pub const BACKOFF_FLOOR: Duration = Duration::from_secs(1);
pub const BACKOFF_CAP: Duration = Duration::from_secs(60);
#[must_use]
pub fn backoff(streak: u32) -> Duration {
let doublings = streak.saturating_sub(1).min(16);
BACKOFF_FLOOR
.saturating_mul(1 << doublings)
.min(BACKOFF_CAP)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_starts_at_the_floor_and_doubles_to_the_cap() {
let seconds: Vec<u64> = (1..=9).map(|n| backoff(n).as_secs()).collect();
assert_eq!(seconds, [1, 2, 4, 8, 16, 32, 60, 60, 60]);
assert_eq!(backoff(0), BACKOFF_FLOOR);
assert_eq!(backoff(u32::MAX), BACKOFF_CAP);
}
#[test]
fn a_port_someone_else_holds_is_waited_out() {
let bind = RuntimeError::Server(ServerError::Bind(std::io::Error::from(
std::io::ErrorKind::AddrInUse,
)));
assert_eq!(FailureClass::of(&bind), FailureClass::Backoff);
}
#[test]
fn a_config_error_is_held_not_retried() {
let config = RuntimeError::Config(engenho_config::ConfigError::Parse("bad".into()));
assert_eq!(FailureClass::of(&config), FailureClass::Hold);
let cancelled = RuntimeError::BootCancelled {
phase: crate::boot::BootPhase::AwaitLeadership,
};
assert_eq!(FailureClass::of(&cancelled), FailureClass::Hold);
}
}