nil-client 0.5.2

Multiplayer strategy game
Documentation
// Copyright (C) Call of Nil contributors
// SPDX-License-Identifier: AGPL-3.0-only

use bon::Builder;
use jiff::{SignedDuration, Timestamp};
use std::num::NonZeroU32;
use tap::{Conv, Pipe};

/// See: <https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker>
#[derive(Builder)]
pub struct CircuitBreaker {
  #[builder(skip)]
  state: CircuitState,

  #[builder(skip)]
  failure_count: u32,

  #[builder(default = unsafe { NonZeroU32::new_unchecked(25)})]
  failure_threshold: NonZeroU32,

  #[builder(skip = Timestamp::UNIX_EPOCH)]
  last_failure: Timestamp,

  #[builder(default = unsafe { NonZeroU32::new_unchecked(15_000)})]
  failure_timeout_ms: NonZeroU32,

  #[builder(skip)]
  success_count: u32,

  #[builder(default = unsafe { NonZeroU32::new_unchecked(3)})]
  success_threshold: NonZeroU32,
}

impl CircuitBreaker {
  pub fn new() -> Self {
    Self::default()
  }

  pub(crate) fn update(&mut self) -> CircuitState {
    match self.state {
      CircuitState::Closed => {
        if self.failure_count >= self.failure_threshold.get() {
          self.enter(CircuitState::Open);
        }
      }
      CircuitState::Open => {
        if !self.has_recent_failure() {
          self.enter(CircuitState::HalfOpen);
        }
      }
      CircuitState::HalfOpen => {
        if self.has_recent_failure() {
          self.enter(CircuitState::Open);
        } else if self.success_count >= self.success_threshold.get() {
          self.enter(CircuitState::Closed);
        }
      }
    }

    self.state
  }

  fn enter(&mut self, state: CircuitState) {
    debug_assert_ne!(self.state, state);

    match state {
      CircuitState::Closed => {
        self.failure_count = 0;
      }
      CircuitState::Open | CircuitState::HalfOpen => {
        self.success_count = 0;
      }
    }

    self.state = state;
  }

  pub fn has_recent_failure(&self) -> bool {
    let timeout = self
      .failure_timeout_ms
      .get()
      .conv::<i64>()
      .pipe(SignedDuration::from_millis);

    Timestamp::now()
      .duration_since(self.last_failure)
      .le(&timeout)
  }

  pub(crate) fn record_failure(&mut self) {
    self.last_failure = Timestamp::now();
    if self.state == CircuitState::HalfOpen {
      self.enter(CircuitState::Open);
    } else {
      self.failure_count += 1;
    }
  }

  pub(crate) fn record_success(&mut self) {
    if let CircuitState::HalfOpen = self.state {
      self.success_count += 1;
    }
  }
}

impl Default for CircuitBreaker {
  fn default() -> Self {
    Self::builder().build()
  }
}

#[derive(Copy, Debug)]
#[derive_const(Clone, Default, PartialEq, Eq)]
pub enum CircuitState {
  #[default]
  Closed,
  Open,
  HalfOpen,
}