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

Timeout.pudu

Pudu75 lines3.0 KB

GitHub ↗
1/** @Resilience.Timeout.Strategy — cancels a callback that outlives its allowance */2module PuduLangResilience.Timeout34import Std.Concurrent.Cancel as Cancel5import PuduLangResilience.Constants.Events as Events6import PuduLangResilience.Context as Context7import PuduLangResilience as Resilience8import PuduLangResilience.Strategy as Strategy9import PuduLangResilience.Telemetry as Telemetry1011/** @Resilience.Timeout.Elapsed — the execution a timeout cancelled */12export type Elapsed = { context: Context.Context, timeout: Int }1314/** @Resilience.Timeout.Options — how long a callback may run */15export type Options = {16  name: Option[Str],17  timeout: Int,18  generator: Option[fn(Context.Context) -> Int],19  onTimeout: Option[fn(Elapsed) -> ()]20}2122/// The shortest timeout an option may name, in milliseconds.23const SHORTEST: Int = 102425/// The longest timeout an option may name: one day, in milliseconds.26const LONGEST: Int = 864000002728/// A thirty-second timeout.29export fn defaults() -> Options { Options{name: None, timeout: 30000, generator: None, onTimeout: None} }3031/// A timeout of `millis`.32export fn after(millis: Int) -> Options { Options{..defaults(), timeout: millis} }3334/// Every reason the options cannot be followed.35export fn validate(options: &Options) -> Array[Str] {36  if options.timeout < SHORTEST || options.timeout > LONGEST { ["timeout must be from 10 to 86400000 ms"] } else { [] }37}3839/// A timeout strategy following the options.40export fn strategy[T, E](options: Options) -> Strategy.Strategy[T, E] {41  Strategy.Strategy {42    name: options.name,43    kind: "Timeout",44    summary: show(options.timeout) + " ms",45    problems: validate(&options),46    attach: Strategy.detached,47    execute: fn(runtime: Strategy.Runtime, context: Context.Context, callback: Strategy.Callback[T, E]) -> Resilience.Outcome[T, E] {48      execute(&options, &runtime, context, callback)49    }50  }51}5253/// Runs the callback with a token that fires once the timeout passes. When the callback answers54/// a cancellation caused by that token alone, the answer is `TimedOut`; any other outcome passes55/// through. A generated timeout of zero or less runs the callback without one.56fn execute[T, E](options: &Options, runtime: &Strategy.Runtime, context: Context.Context, callback: Strategy.Callback[T, E]) -> Resilience.Outcome[T, E] {57  let timeout = match options.generator {58    case Some(generate) => generate(context)59    case None => options.timeout60  }61  if timeout <= 0 { return callback(context) }62  let bounded = Cancel.childExpiring(&context.token, timeout)63  let outcome = callback(Context.withToken(&context, bounded))64  let expired = Cancel.reason(&bounded) == Some(Cancel.DeadlineExceeded) && !Cancel.stopped(&context.token)65  match outcome {66    case Err(Resilience.Cancelled(_)) => {67      if !expired { return outcome }68      Telemetry.report(&runtime.telemetry, &context, Events.ON_TIMEOUT, Telemetry.Error, None, Telemetry.TimeoutElapsed(timeout))69      if let Some(notify) = options.onTimeout { notify(Elapsed{context: context, timeout: timeout}) }70      Err(Resilience.TimedOut(timeout))71    }72    case _ => outcome73  }74}75