
Timeout.pudu
Pudu75 lines3.0 KB
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}212223const SHORTEST: Int = 10242526const LONGEST: Int = 86400000272829export fn defaults() -> Options { Options{name: None, timeout: 30000, generator: None, onTimeout: None} }303132export fn after(millis: Int) -> Options { Options{..defaults(), timeout: millis} }333435export fn validate(options: &Options) -> Array[Str] {36 if options.timeout < SHORTEST || options.timeout > LONGEST { ["timeout must be from 10 to 86400000 ms"] } else { [] }37}383940export 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}5253545556fn 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