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

Limiter.pudu

Pudu103 lines4.0 KB

GitHub ↗
1/** @Resilience.Limiter.Seam — permits leased by rate and concurrency limiters */2module PuduLangResilience.Limiter34import Std.Concurrent.Cancel as Cancel5import PuduLangResilience.Utils.Shared as Shared67/** @Resilience.Limiter.QueueOrder — which queued request is served first */8export type QueueOrder = OldestFirst | NewestFirst910/** @Resilience.Limiter.Grant — whether a request was given its permits */11export type Grant = Granted | Denied(Option[Int]) | Abandoned(Str)1213/** @Resilience.Limiter.Lease — the answer to one request and how to hand its permits back */14export type Lease = { grant: Grant, release: fn() -> () }1516/** @Resilience.Limiter.Statistics — a limiter's counts at one moment */17export type Statistics = { available: Int, queued: Int, failed: Int, succeeded: Int }1819/** @Resilience.Limiter.Limiter — leases permits, waiting or at once */20export type Limiter = {21  acquire: fn(Int, Cancel.Token) -> Lease,22  attempt: fn(Int) -> Lease,23  statistics: fn() -> Statistics,24  replenish: fn() -> Bool25}2627/// Requests `permits`, waiting in the limiter's queue while it has room; the lease is28/// `Abandoned` when the token fires first.29export fn acquire(limiter: &Limiter, permits: Int, token: &Cancel.Token) -> Lease { (limiter.acquire)(permits, *token) }3031/// Requests `permits` without waiting.32export fn attempt(limiter: &Limiter, permits: Int) -> Lease { (limiter.attempt)(permits) }3334/// The limiter's counts now.35export fn statistics(limiter: &Limiter) -> Statistics { (limiter.statistics)() }3637/// Adds one period's permits to a limiter replenished by hand; whether it did.38export fn replenish(limiter: &Limiter) -> Bool { (limiter.replenish)() }3940/// Whether the lease holds its permits.41export fn isGranted(lease: &Lease) -> Bool { lease.grant == Granted }4243/// Hands a lease's permits back. Releasing twice hands them back once.44export fn release(lease: &Lease) -> () { (lease.release)() }4546/// A lease that holds nothing and releases nothing.47export fn refused(grant: Grant) -> Lease { Lease{grant: grant, release: fn() -> () { () }} }4849/// A lease granted with `handBack` run on its first release only.50export fn granted(handBack: fn() -> ()) -> Lease {51  let spent = Shared.shared(false)52  Lease{grant: Granted, release: fn() -> () {53      let first = Shared.change(&spent, fn(held: Bool) -> (Bool, Bool) { (true, !held) })54      if first { handBack() }55    } }56}5758/// A limiter granting a request only when every limiter in order grants it. A refusal releases59/// the leases already taken and answers that refusal.60export fn chain(limiters: Array[Limiter]) -> Limiter {61  Limiter {62    acquire: fn(permits: Int, token: Cancel.Token) -> Lease { chained(&limiters, fn(limiter: Limiter) -> Lease { acquire(&limiter, permits, &token) }) },63    attempt: fn(permits: Int) -> Lease { chained(&limiters, fn(limiter: Limiter) -> Lease { attempt(&limiter, permits) }) },64    statistics: fn() -> Statistics { combined(&limiters) },65    replenish: fn() -> Bool {66      var any = false67      for limiter in limiters {68        if replenish(&limiter) { any = true }69      }70      any71    }72  }73}7475/// Takes a lease from each limiter in order, stopping at the first refusal.76fn chained(limiters: &Array[Limiter], take: fn(Limiter) -> Lease) -> Lease {77  var held: Array[Lease] = []78  for limiter in limiters {79    let lease = take(limiter)80    if !isGranted(&lease) {81      for taken in held { release(&taken) }82      return lease83    }84    held = held.push(lease)85  }86  granted(fn() -> () {87      for taken in held { release(&taken) }88    })89}9091/// The smallest availability and the summed counts of a chain.92fn combined(limiters: &Array[Limiter]) -> Statistics {93  var total = Statistics{available: 0, queued: 0, failed: 0, succeeded: 0}94  var first = true95  for limiter in limiters {96    let counts = statistics(&limiter)97    let available = if first || counts.available < total.available { counts.available } else { total.available }98    total = Statistics{available: available, queued: total.queued + counts.queued, failed: total.failed + counts.failed, succeeded: total.succeeded + counts.succeeded}99    first = false100  }101  total102}103