ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Rules.Effectful

Description

The resilience harness for effectful rules: the per-attempt timeout, bounded retry with backoff, and per-source circuit breaker wrapped around a rule evaluation that does IO. Ecluse.Core.Rules attaches a Resilience to each effectful rule at prepare and runs it through runResilient; the pure built-ins never enter this module.

A resilient evaluation runs under its breaker's admission gate, a per-attempt timeout, and bounded retry with backoff. Any RuleVerdict the rule returns -- a deterministic CannotVet included -- resets the breaker and is passed on Decided, taken at face value and never retried; only a fault the harness observes (a timeout, an exception, or the breaker already open) advances the breaker and resolves to Unavailable transience alignment reason, the alignment from the rule's Resilience (fail-closed FailDeny or fail-open FailNoDecision). Total: runResilient never throws; a rule failure becomes a result.

The breaker timing reads the injected resilience clock (resClock), read fresh at each breaker decision, so it is deterministic under test and independent of the request snapshot the age rules hold constant. Reading it again after the retry run means a tripped breaker's cooldown starts when the failure commits, not when the run began.

Synopsis

The resilience policy

data Resilience Source #

The resilience policy wrapped around an effectful rule's IO: the timeout/retry/ breaker knobs, the per-source circuit-breaker state, its observer, and the failure alignment an exhausted evaluation resolves to (fail-closed FailDeny or fail-open FailNoDecision). The alignment rides on the prepared rule, folding away the separate failure-policy the two-tier design once carried.

Constructors

Resilience 

Fields

  • resConfig :: EffectfulConfig

    The per-attempt timeout, retry budget/backoff, and breaker threshold/cooldown.

  • resAlignment :: FailureAlignment

    Whether an exhausted evaluation fails closed (FailDeny) or open (FailNoDecision).

  • resBreaker :: TVar Breaker

    This rule's per-source circuit-breaker state, shared across evaluations.

  • resBreakerReporter :: BreakerReporter

    The observer this rule's breaker reports its state transitions to (ecluse.rule.breaker.state). Inert (noBreakerReporter) for an unobserved rule; the composition root installs the live one.

  • resClock :: IO UTCTime

    The injected wall clock the breaker reads for its admission gate and its cooldown arithmetic. getCurrentTime in production, overridable under test for deterministic breaker timing. Deliberately separate from the request snapshot ctxNow (which the age rules hold constant across a packument): the breaker is a wall-clock device, and reading it fresh at the point a failure commits is what makes the cooldown start when the failure is recorded, not when the retry run began.

  • resFaultReporter :: FaultReporter

    The observer an exhausted evaluation reports its fault detail to (the rendered exception, or a timeout), so a live-database query fault is diagnosable from the operator log rather than collapsing to a bare Unavailable. Inert (noFaultReporter) for an unobserved rule; the composition root installs the live one. It never reaches the client-facing decision message.

data EffectfulConfig Source #

The resilience knobs around an effectful rule's IO: a per-attempt timeout, how many retries to make on failure with the backoff before each, and the breaker threshold and cooldown. The breaker's timing reads the injected resilience clock (resClock) fresh at failure commit, not the request snapshot ctxNow.

Constructors

EffectfulConfig 

Fields

  • ecTimeout :: Int

    The per-attempt timeout in microseconds. An attempt that does not return within it is treated as a failure (a transient, retryable cause).

  • ecBackoff :: [Int]

    The backoff delays in microseconds, one per retry, applied before the corresponding retry attempt. Its length is the retry budget: [] means the single initial attempt only, [100, 200] means up to two retries after it.

  • ecBreakerThreshold :: Int

    Consecutive exhausted-rule failures that trip the breaker.

  • ecBreakerCooldown :: NominalDiffTime

    How long the breaker stays open (fast-failing the rule) before a single half-open probe is allowed to test recovery.

  • ecRetryAfter :: Maybe RetryAfter

    The Retry-After delay to suggest to a client when this rule's unavailability surfaces on a concrete-artifact request; Nothing suggests none.

defaultEffectfulConfig :: EffectfulConfig Source #

Sensible defaults for the resilience knobs: a 2-second per-attempt timeout, two retries at 100ms then 250ms, and a breaker tripping after 5 consecutive failures and cooling for 30 seconds. The caller supplies the rule's IO; the knobs are policy with these defaults.

newBreaker :: IO (TVar Breaker) Source #

A fresh, healthy breaker (no failures recorded) in a new TVar.

Effectful-fault observation

newtype FaultReporter Source #

The observer an exhausted effectful evaluation reports its fault detail to: the deciding rule's name and the rendered fault (an exception's displayException, or a timeout). A telemetry-agnostic callback in the shape of BreakerReporter, so the pure rules engine names no logger; the composition root closes a katip line over it. Fires once per exhausted evaluation, never on a verdict or a still-cooling breaker.

Constructors

FaultReporter (Text -> Text -> IO ()) 

reportFault :: FaultReporter -> Text -> Text -> IO () Source #

Report one exhausted evaluation's fault: the rule name and the rendered detail.

Running an evaluation through it

runResilient :: Resilience -> Text -> (PackageDetails -> IO RuleVerdict) -> PackageDetails -> IO RuleEvaluation Source #

Run one effectful rule evaluation through its Resilience policy: the breaker admission gate, then the per-attempt timeout under bounded retry, then the breaker settlement. The rule's name tags the audit reason; the evaluator is the rule's raw per-version IO with the evaluation context already applied. See the module header for the verdict-vs-fault contract; runEffectfulRule is the engine-level entry that dispatches a prepared rule here.

backoffPolicy :: [Int] -> RetryPolicyM IO Source #

An ecBackoff schedule compiled to a Control.Retry policy: the retry at iteration n waits the n-th delay (microseconds) before it, and the policy stops (yields Nothing) once the schedule is exhausted -- so the list's length is the retry budget. [] admits no retry (a single attempt); [a, b] admits up to two. Inspect the resulting delays without sleeping with simulatePolicy.