
Circuit.pudu
Pudu93 lines3.9 KB
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 }262728export fn closed(sampling: Int) -> Machine {29 Machine{phase: Closed, blockedUntil: 0, halfOpenAttempts: 0, health: Health.create(sampling), lastHandled: None}30}3132333435export 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}50515253export 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}5758596061export 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(¬ed.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(¬ed.health, now, true)}, false)71 }72}737475export fn open(machine: &Machine, now: Int, duration: Int) -> Machine {76 Machine{..*machine, phase: Open, blockedUntil: Numeric.add(now, Math.max(0, duration))}77}787980export fn isolate(machine: &Machine) -> Machine {81 Machine{..*machine, phase: Isolated, blockedUntil: Numeric.LARGEST, lastHandled: Some("the circuit was isolated")}82}838485export fn close(machine: &Machine) -> Machine {86 Machine{..*machine, phase: Closed, blockedUntil: 0, halfOpenAttempts: 0, health: Health.reset(&machine.health), lastHandled: None}87}888990export fn retryAfter(machine: &Machine, now: Int) -> Int {91 Math.max(0, machine.blockedUntil - now)92}93