
Health.pudu
Pudu82 lines3.2 KB
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 }181920const WINDOWS: Int = 10212223const RESOLUTION: Int = 20242526export 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}303132export 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}394041export 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}525354export fn reset(health: &Health) -> Health { Health{..*health, windows: []} }555657export fn shouldBreak(totals: &Info, ratio: Decimal, minimumThroughput: Int) -> Bool {58 totals.throughput >= minimumThroughput && Decimal.fromInt(totals.failures) >= ratio * Decimal.fromInt(totals.throughput)59}606162export 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}66676869fn 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