use alloc::vec::Vec;
use crate::nexus::effects::error::ErrorComputation;
pub const DEFAULT_MAX_RETRIES: usize = 3;
#[derive(Clone, Debug)]
pub enum SpeculativeResult<A, E> {
Success(A),
AllFailed(Vec<E>),
NoBranches,
}
impl<A, E> SpeculativeResult<A, E> {
#[inline]
pub fn ok(self) -> Option<A> {
match self {
SpeculativeResult::Success(a) => Some(a),
_ => None,
}
}
#[inline]
pub fn to_result(self) -> Result<A, Vec<E>> {
match self {
SpeculativeResult::Success(a) => Ok(a),
SpeculativeResult::AllFailed(errs) => Err(errs),
SpeculativeResult::NoBranches => Err(Vec::new()),
}
}
#[inline]
pub fn is_success(&self) -> bool {
matches!(self, SpeculativeResult::Success(_))
}
}
#[inline]
pub fn speculative<A, E>(branches: Vec<ErrorComputation<E, A>>) -> SpeculativeResult<A, E> {
if branches.is_empty() {
return SpeculativeResult::NoBranches;
}
let mut errors = Vec::with_capacity(branches.len());
for branch in branches {
match branch.run() {
Ok(a) => return SpeculativeResult::Success(a),
Err(e) => errors.push(e),
}
}
SpeculativeResult::AllFailed(errors)
}
#[inline]
pub fn race<A, E>(branches: Vec<ErrorComputation<E, A>>) -> Option<Result<A, E>> {
branches
.into_iter()
.next()
.map(super::super::effects::error::ErrorComputation::run)
}
#[inline]
pub fn first_success<A, E>(branches: Vec<ErrorComputation<E, A>>) -> Result<A, Vec<E>> {
speculative(branches).to_result()
}
#[derive(Clone, Copy, Debug)]
pub struct HedgeConfig {
pub max_attempts: usize,
pub cancel_on_success: bool,
}
impl Default for HedgeConfig {
fn default() -> Self {
HedgeConfig {
max_attempts: DEFAULT_MAX_RETRIES,
cancel_on_success: true,
}
}
}
pub fn hedge<A, E, F>(config: HedgeConfig, make_attempt: F) -> SpeculativeResult<A, E>
where
F: Fn() -> ErrorComputation<E, A>,
{
let mut branches: Vec<_> = Vec::with_capacity(config.max_attempts);
branches.extend((0..config.max_attempts).map(|_| make_attempt()));
speculative(branches)
}
#[inline]
pub fn with_fallback<A, E, F1, F2>(primary: F1, fallback: F2) -> Result<A, E>
where
F1: FnOnce() -> ErrorComputation<E, A>,
F2: FnOnce() -> ErrorComputation<E, A>,
{
match primary().run() {
Ok(a) => Ok(a),
Err(_) => fallback().run(),
}
}
pub fn fallback_chain<A, E, F>(attempts: Vec<F>) -> Result<A, Vec<E>>
where
F: FnOnce() -> ErrorComputation<E, A>,
{
let mut errors = Vec::with_capacity(attempts.len());
for attempt in attempts {
match attempt().run() {
Ok(a) => return Ok(a),
Err(e) => errors.push(e),
}
}
Err(errors)
}
pub struct TimedComputation<A, E> {
computation: ErrorComputation<E, A>,
_timeout_ms: u64,
}
impl<A, E> TimedComputation<A, E> {
pub fn new(computation: ErrorComputation<E, A>, timeout_ms: u64) -> Self {
TimedComputation {
computation,
_timeout_ms: timeout_ms,
}
}
pub fn run(self) -> Result<A, E> {
self.computation.run()
}
}
pub fn with_timeout<A, E>(
computation: ErrorComputation<E, A>,
timeout_ms: u64,
) -> TimedComputation<A, E> {
TimedComputation::new(computation, timeout_ms)
}
#[derive(Clone, Copy, Debug)]
pub struct RetryConfig {
pub max_retries: usize,
pub exponential_backoff: bool,
}
impl Default for RetryConfig {
fn default() -> Self {
RetryConfig {
max_retries: DEFAULT_MAX_RETRIES,
exponential_backoff: true,
}
}
}
pub fn retry<A, E, F>(config: RetryConfig, operation: F) -> Result<A, E>
where
F: Fn() -> ErrorComputation<E, A>,
E: Clone,
{
let mut last_error = None;
for _ in 0..=config.max_retries {
match operation().run() {
Ok(a) => return Ok(a),
Err(e) => last_error = Some(e),
}
}
Err(last_error.expect("Should have at least one error after retries"))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
pub struct CircuitBreaker {
state: CircuitState,
failure_count: usize,
failure_threshold: usize,
success_count: usize,
success_threshold: usize,
}
impl CircuitBreaker {
pub fn new(failure_threshold: usize, success_threshold: usize) -> Self {
CircuitBreaker {
state: CircuitState::Closed,
failure_count: 0,
failure_threshold,
success_count: 0,
success_threshold,
}
}
#[inline]
pub fn is_allowed(&self) -> bool {
!matches!(self.state, CircuitState::Open)
}
#[inline]
pub fn record_success(&mut self) {
self.failure_count = 0;
if self.state == CircuitState::HalfOpen {
self.success_count += 1;
if self.success_count >= self.success_threshold {
self.state = CircuitState::Closed;
self.success_count = 0;
}
}
}
#[inline]
pub fn record_failure(&mut self) {
self.failure_count += 1;
self.success_count = 0;
if self.failure_count >= self.failure_threshold {
self.state = CircuitState::Open;
}
}
#[inline]
pub fn attempt_reset(&mut self) {
if self.state == CircuitState::Open {
self.state = CircuitState::HalfOpen;
}
}
#[inline]
pub fn state(&self) -> CircuitState {
self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speculative_first_success() {
let branches = alloc::vec![
ErrorComputation::err("fail1"),
ErrorComputation::ok(42),
ErrorComputation::ok(43),
];
let result = speculative(branches);
assert!(matches!(result, SpeculativeResult::Success(42)));
}
#[test]
fn test_speculative_all_fail() {
let branches: Vec<ErrorComputation<&str, i32>> = alloc::vec![
ErrorComputation::err("fail1"),
ErrorComputation::err("fail2"),
];
let result = speculative(branches);
assert!(matches!(result, SpeculativeResult::AllFailed(_)));
}
#[test]
fn test_speculative_no_branches() {
let branches: Vec<ErrorComputation<&str, i32>> = alloc::vec![];
let result = speculative(branches);
assert!(matches!(result, SpeculativeResult::NoBranches));
}
#[test]
fn test_race() {
let branches: Vec<ErrorComputation<&str, i32>> =
alloc::vec![ErrorComputation::ok(1), ErrorComputation::ok(2),];
let result = race(branches);
assert_eq!(result, Some(Ok(1)));
}
#[test]
fn test_first_success() {
let branches: Vec<ErrorComputation<&str, i32>> =
alloc::vec![ErrorComputation::err("e1"), ErrorComputation::ok(42),];
let result = first_success(branches);
assert_eq!(result, Ok(42));
}
#[test]
fn test_with_fallback_primary_succeeds() {
let result: Result<i32, &str> =
with_fallback(|| ErrorComputation::ok(1), || ErrorComputation::ok(2));
assert_eq!(result, Ok(1));
}
#[test]
fn test_with_fallback_uses_fallback() {
let result: Result<i32, &str> = with_fallback(
|| ErrorComputation::err("primary failed"),
|| ErrorComputation::ok(2),
);
assert_eq!(result, Ok(2));
}
#[test]
fn test_fallback_chain() {
let result = fallback_chain(alloc::vec![
|| ErrorComputation::<&str, i32>::err("e1"),
|| ErrorComputation::<&str, i32>::err("e2"),
|| ErrorComputation::ok(42),
]);
assert_eq!(result, Ok(42));
}
#[test]
fn test_retry_succeeds_first_try() {
let result: Result<i32, &str> = retry(
RetryConfig {
max_retries: 3,
exponential_backoff: false,
},
|| ErrorComputation::<&str, i32>::ok(42),
);
assert_eq!(result, Ok(42));
}
#[test]
fn test_circuit_breaker() {
let mut cb = CircuitBreaker::new(2, 1);
assert!(cb.is_allowed());
assert_eq!(cb.state(), CircuitState::Closed);
cb.record_failure();
assert!(cb.is_allowed());
cb.record_failure();
assert!(!cb.is_allowed());
assert_eq!(cb.state(), CircuitState::Open);
cb.attempt_reset();
assert_eq!(cb.state(), CircuitState::HalfOpen);
assert!(cb.is_allowed());
cb.record_success();
assert_eq!(cb.state(), CircuitState::Closed);
}
#[test]
fn test_hedge() {
let result: SpeculativeResult<i32, &str> = hedge(
HedgeConfig {
max_attempts: 3,
cancel_on_success: true,
},
|| ErrorComputation::<&str, i32>::ok(42),
);
assert!(matches!(result, SpeculativeResult::Success(42)));
}
}