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

Health.pudu

Pudu82 lines3.2 KB

GitHub ↗
1/** @Domain.Health.Module — rolling success and failure counts */2module PuduLangResilience.Domain.Health34import Std.Decimal as Decimal5import Std.List as List6import Std.Math as Math7import Std.Option as Option8import PuduLangResilience.Utils.Numeric as Numeric910/** @Domain.Health.Window — the counts of one slice of the sampling period */11export type Window = { start: Int, successes: Int, failures: Int }1213/** @Domain.Health.Health — the windows covering one sampling period */14export type Health = { sampling: Int, windowLength: Int, windows: Array[Window] }1516/** @Domain.Health.Info — totals over the sampling period */17export type Info = { throughput: Int, failures: Int }1819/// The number of windows a sampling period is split into.20const WINDOWS: Int = 102122/// The shortest window worth keeping, in milliseconds; a shorter period uses a single window.23const RESOLUTION: Int = 202425/// Empty counts over a sampling period of `sampling` milliseconds.26export fn create(sampling: Int) -> Health {27  let length = if sampling < RESOLUTION * WINDOWS { sampling } else { sampling / WINDOWS }28  Health{sampling: sampling, windowLength: Math.max(1, length), windows: []}29}3031/// The counts with one more outcome recorded at `now`.32export fn record(health: &Health, now: Int, failed: Bool) -> Health {33  let rolled = roll(health, now)34  let last = rolled.windows.length() - 135  let current = rolled.windows[last]36  let counted = if failed { Window{..current, failures: current.failures + 1} } else { Window{..current, successes: current.successes + 1} }37  Health{..rolled, windows: rolled.windows.slice(0, last).push(counted)}38}3940/// The totals of the windows still inside the sampling period at `now`.41export fn info(health: &Health, now: Int) -> Info {42  var throughput = 043  var failures = 044  for window in health.windows {45    if now - window.start < health.sampling {46      throughput = Numeric.add(throughput, window.successes + window.failures)47      failures = Numeric.add(failures, window.failures)48    }49  }50  Info{throughput: throughput, failures: failures}51}5253/// The counts emptied.54export fn reset(health: &Health) -> Health { Health{..*health, windows: []} }5556/// Whether the totals reach the minimum throughput and a failure share of at least `ratio`.57export fn shouldBreak(totals: &Info, ratio: Decimal, minimumThroughput: Int) -> Bool {58  totals.throughput >= minimumThroughput && Decimal.fromInt(totals.failures) >= ratio * Decimal.fromInt(totals.throughput)59}6061/// The share of failures in the totals, to six places; zero without throughput.62export fn failureRate(totals: &Info) -> Decimal {63  if totals.throughput == 0 { return 0d }64  Option.unwrapOr(Decimal.divideHalfEven(Decimal.fromInt(totals.failures), Decimal.fromInt(totals.throughput), 6), 0d)65}6667/// The windows at `now`: a new window once the current one is full, and none older than the68/// sampling period.69fn roll(health: &Health, now: Int) -> Health {70  var windows = health.windows71  let fresh = match List.last(&windows) {72    case Some(current) => now - current.start >= health.windowLength73    case None => true74  }75  if fresh { windows = windows.push(Window{start: now, successes: 0, failures: 0}) }76  var kept: Array[Window] = []77  for window in windows {78    if now - window.start < health.sampling { kept = kept.push(window) }79  }80  Health{..*health, windows: kept}81}82