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

Registry.pudu

Pudu114 lines5.4 KB

GitHub ↗
1/** @Resilience.Registry.Module — named pipelines built once and shared */2module PuduLangResilience.Registry34import Std.Map as Map5import PuduLangResilience.Constants.Messages as Messages6import PuduLangResilience.Pipeline as Pipeline7import PuduLangResilience.Strategy as Strategy8import PuduLangResilience.Utils.Shared as Shared9import PuduLangResilience.Utils.Template as Template1011/** @Resilience.Registry.BuilderContext — the key a pipeline is being built for */12export type BuilderContext = { key: Str, builderName: Str, instanceName: Option[Str] }1314/** @Resilience.Registry.Builder — the strategies of the pipeline for one key */15export type Builder[T, E] = fn(BuilderContext) -> Array[Strategy.Strategy[T, E]]1617/** @Resilience.Registry.Options — shared pipeline settings and how keys become names */18export type Options = {19  pipeline: Pipeline.Options,20  builderName: fn(Str) -> Str,21  instanceName: fn(Str) -> Option[Str]22}2324/** @Resilience.Registry.RegistryError — why a pipeline could not be answered */25export type RegistryError = NotFound(Str) | Invalid(Str, Pipeline.Invalid)2627/** @Resilience.Registry.Registry — builders and the pipelines built from them */28export type Registry[T, E] = {29  options: Options,30  entries: Shared.Shared[Map[Str, Entry[T, E]]]31}3233/** @Resilience.Registry.Entry — one key's builder and its current pipeline */34type Entry[T, E] = { builder: Builder[T, E], pipeline: Option[Pipeline.Pipeline[T, E]] }3536/// The default pipeline settings, each key as its builder name, and no instance name.37export fn defaults() -> Options {38  Options{pipeline: Pipeline.defaults(), builderName: fn(key: Str) -> Str { key }, instanceName: fn(_key: Str) -> Option[Str] { None }}39}4041/// An empty registry with the default options.42export fn create[T, E]() -> Registry[T, E] { createWith(defaults()) }4344/// An empty registry with the given options.45export fn createWith[T, E](options: Options) -> Registry[T, E] { Registry{options: options, entries: Shared.shared(Map.empty())} }4647/// Registers the builder for a key; `false`, changing nothing, when the key already has one.48export fn tryAddBuilder[T, E](registry: &Registry[T, E], key: Str, builder: Builder[T, E]) -> Bool {49  Shared.change(&registry.entries, fn(held: Map[Str, Entry[T, E]]) -> (Map[Str, Entry[T, E]], Bool) {50      if Map.containsKey(&held, key) { (held, false) } else { (Map.insert(&held, key, Entry{builder: builder, pipeline: None}), true) }51    })52}5354/// The pipeline for a key, built from its builder on first use and shared afterwards.55export fn get[T, E](registry: &Registry[T, E], key: Str) -> Result[Pipeline.Pipeline[T, E], RegistryError] {56  let options = registry.options57  Shared.change(&registry.entries, fn(held: Map[Str, Entry[T, E]]) -> (Map[Str, Entry[T, E]], Result[Pipeline.Pipeline[T, E], RegistryError]) {58      match Map.get(&held, key) {59        case None => (held, Err(NotFound(key)))60        case Some(entry) => resolve(&options, held, key, entry, false)61      }62    })63}6465/// The pipeline for a key, or `None` when the key has no builder or its pipeline is invalid.66export fn tryGet[T, E](registry: &Registry[T, E], key: Str) -> Option[Pipeline.Pipeline[T, E]] {67  match get(registry, key) {68    case Ok(pipeline) => Some(pipeline)69    case Err(_) => None70  }71}7273/// The pipeline for a key, registering `builder` first when the key has none.74export fn getOrAdd[T, E](registry: &Registry[T, E], key: Str, builder: Builder[T, E]) -> Result[Pipeline.Pipeline[T, E], RegistryError] {75  let _added = tryAddBuilder(registry, key, builder)76  get(registry, key)77}7879/// A new pipeline for a key, built again from its builder; later `get` calls answer it. State80/// such as an open circuit starts afresh in the new pipeline.81export fn reload[T, E](registry: &Registry[T, E], key: Str) -> Result[Pipeline.Pipeline[T, E], RegistryError] {82  let options = registry.options83  Shared.change(&registry.entries, fn(held: Map[Str, Entry[T, E]]) -> (Map[Str, Entry[T, E]], Result[Pipeline.Pipeline[T, E], RegistryError]) {84      match Map.get(&held, key) {85        case None => (held, Err(NotFound(key)))86        case Some(entry) => resolve(&options, held, key, entry, true)87      }88    })89}9091/// Every key with a builder, in order.92export fn keys[T, E](registry: &Registry[T, E]) -> Array[Str] { Map.keys(&Shared.current(&registry.entries)) }9394/// One sentence saying why a pipeline could not be answered.95export fn explain(problem: &RegistryError) -> Str {96  match problem {97    case NotFound(key) => Template.fill(Messages.REGISTRY_NOT_FOUND, &[key])98    case Invalid(key, invalid) => Template.fill(Messages.REGISTRY_INVALID, &[key, invalid.problems.join(Messages.PROBLEM_SEPARATOR)])99  }100}101102/// The entry's pipeline, built when it has none or when `rebuild`, and the entries holding it.103fn resolve[T, E](options: &Options, held: Map[Str, Entry[T, E]], key: Str, entry: Entry[T, E], rebuild: Bool) -> (Map[Str, Entry[T, E]], Result[Pipeline.Pipeline[T, E], RegistryError]) {104  if !rebuild {105    if let Some(existing) = entry.pipeline { return (held, Ok(existing)) }106  }107  let context = BuilderContext{key: key, builderName: (options.builderName)(key), instanceName: (options.instanceName)(key)}108  let settings = Pipeline.Options{..options.pipeline, name: Some(context.builderName), instance: context.instanceName}109  match Pipeline.buildWith(&settings, (entry.builder)(context)) {110    case Ok(built) => (Map.insert(&held, key, Entry{..entry, pipeline: Some(built)}), Ok(built))111    case Err(invalid) => (held, Err(Invalid(key, invalid)))112  }113}114