Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-resilience

Resilience pipelines for Pudu: retry, circuit breaker, timeout, fallback, hedging, rate limiting, and chaos injection

0.1.0Apache-2.01

InstallClose

Circuit.pudu

Pudu93 lines3.9 KB

GitHub ↗
1/** @Domain.Circuit.StateMachine — circuit transitions over health counts */2module PuduLangResilience.Domain.Circuit34import Std.Decimal as Decimal5import Std.Math as Math6import PuduLangResilience.Domain.Health as Health7import PuduLangResilience.Utils.Numeric as Numeric89/** @Domain.Circuit.Phase — which executions the circuit admits */10export type Phase = Closed | Open | HalfOpen | Isolated1112/** @Domain.Circuit.Machine — the circuit's phase, deadline, and counts */13export type Machine = {14  phase: Phase,15  blockedUntil: Int,16  halfOpenAttempts: Int,17  health: Health.Health,18  lastHandled: Option[Str]19}2021/** @Domain.Circuit.Admission — the answer to one execution asking to run */22export type Admission = Admitted | Probe | Blocked(Int) | Refused2324/** @Domain.Circuit.Thresholds — when a closed circuit opens */25export type Thresholds = { failureRatio: Decimal, minimumThroughput: Int }2627/// A closed circuit counting over a sampling period of `sampling` milliseconds.28export fn closed(sampling: Int) -> Machine {29  Machine{phase: Closed, blockedUntil: 0, halfOpenAttempts: 0, health: Health.create(sampling), lastHandled: None}30}3132/// Whether an execution may run at `now`. An open circuit past its deadline turns half-open and33/// admits this one execution as its probe, pushing the deadline `breakDuration` further; every34/// other execution waits for the probe's outcome.35export fn admit(machine: &Machine, now: Int, breakDuration: Int) -> (Machine, Admission) {36  match machine.phase {37    case Closed => (*machine, Admitted)38    case Isolated => (*machine, Refused)39    case Open => {40      if now >= machine.blockedUntil {41        let probing = Machine{..*machine, phase: HalfOpen, halfOpenAttempts: machine.halfOpenAttempts + 1, blockedUntil: Numeric.add(now, breakDuration)}42        (probing, Probe)43      } else {44        (*machine, Blocked(machine.blockedUntil - now))45      }46    }47    case HalfOpen => (*machine, Blocked(retryAfter(machine, now)))48  }49}5051/// The circuit after an unhandled outcome at `now`, and whether it closed. A half-open circuit52/// closes; the outcome is counted in every phase.53export fn succeeded(machine: &Machine, now: Int) -> (Machine, Bool) {54  let counted = Machine{..*machine, health: Health.record(&machine.health, now, false)}55  if machine.phase == HalfOpen { (close(&counted), true) } else { (counted, false) }56}5758/// The circuit after a handled outcome at `now`, and whether it must now open: always from59/// half-open, and from closed once the thresholds are met. The failure is counted except while60/// half-open.61export fn failed(machine: &Machine, now: Int, summary: Str, thresholds: &Thresholds) -> (Machine, Bool) {62  let noted = Machine{..*machine, lastHandled: Some(summary)}63  match machine.phase {64    case HalfOpen => (noted, true)65    case Closed => {66      let counted = Machine{..noted, health: Health.record(&noted.health, now, true)}67      let totals = Health.info(&counted.health, now)68      (counted, Health.shouldBreak(&totals, thresholds.failureRatio, thresholds.minimumThroughput))69    }70    case _ => (Machine{..noted, health: Health.record(&noted.health, now, true)}, false)71  }72}7374/// The circuit opened at `now` for `duration` milliseconds.75export fn open(machine: &Machine, now: Int, duration: Int) -> Machine {76  Machine{..*machine, phase: Open, blockedUntil: Numeric.add(now, Math.max(0, duration))}77}7879/// The circuit held open until closed by hand.80export fn isolate(machine: &Machine) -> Machine {81  Machine{..*machine, phase: Isolated, blockedUntil: Numeric.LARGEST, lastHandled: Some("the circuit was isolated")}82}8384/// The circuit closed, with its counts and half-open attempts cleared.85export fn close(machine: &Machine) -> Machine {86  Machine{..*machine, phase: Closed, blockedUntil: 0, halfOpenAttempts: 0, health: Health.reset(&machine.health), lastHandled: None}87}8889/// Milliseconds until the circuit may admit a probe, never below zero.90export fn retryAfter(machine: &Machine, now: Int) -> Int {91  Math.max(0, machine.blockedUntil - now)92}93