ecluse
Safe HaskellNone
LanguageGHC2021

Ecluse.Credential.Refresh.Internal

Description

The implementation behind Refresh. This module exposes the provider's innards — including the refreshingProviderWith test hook — that the curated public module deliberately keeps hidden. Importing it opts out of the module's stability promises (the same convention text and bytestring use for their .Internal modules); production code imports Refresh instead. The policy itself is documented on the public module's header.

Synopsis

Configuration

data RefreshConfig Source #

How a refreshingProvider mints, times, and protects its token. The two effectful leaves (rcMint, rcClock) and the jitter source (rcJitter) are injected so the whole policy is deterministic under test; the rest are policy knobs with sensible defaults in defaultRefreshConfig.

Constructors

RefreshConfig 

Fields

  • rcMint :: IO AuthToken

    The per-cloud token mint — the only part that touches a network. A backend supplies just this leaf; everything else is cloud-agnostic.

  • rcClock :: IO UTCTime

    The clock the policy reads. Injected so refresh timing is testable without real time passing.

  • rcJitter :: IO Double

    A jitter fraction in [0, 1), sampled once per token, that pulls the refresh instant earlier to desynchronise a cohort of instances so they do not all refresh at the same moment.

  • rcRefreshAt :: Double

    The fraction of a token's lifetime at which to refresh, before jitter (the ~80% point). Clamped into [0, 1].

  • rcRefreshFloor :: NominalDiffTime

    A hard floor: never schedule the refresh later than this many seconds before expiry, so a token with a very short lifetime is still refreshed ahead of its deadline rather than served right up to it.

  • rcBreakerThreshold :: Int

    Consecutive mint failures that trip the circuit breaker.

  • rcBreakerCooldown :: NominalDiffTime

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

  • rcBreakerReporter :: BreakerReporter

    The observer the mint breaker reports its state transitions to. Inert by default (noBreakerReporter); the composition root installs the live one.

  • rcRefreshReporter :: RefreshReporter

    The observer each refresh attempt's outcome is reported to. Inert by default (noRefreshReporter); the composition root installs the live one.

defaultRefreshConfig :: RefreshConfig Source #

Sensible defaults for the policy knobs. The caller must still supply the effectful leaves — rcMint and rcClock default to a mint/clock that always fails, so a provider built without wiring them up fails loudly rather than silently serving nothing.

  • refresh at 80% of lifetime (no jitter by default; rcJitter may pull it earlier);
  • a 30-second floor before expiry;
  • breaker trips after 5 consecutive failures, cooling down for 60 seconds.

The refreshing provider

refreshingProvider :: RefreshConfig -> IO CredentialProvider Source #

Build a CredentialProvider that caches a token and refreshes it per the RefreshConfig policy (see the module header). Mints once eagerly to seed the cache, so a provider that cannot mint at all fails here at construction rather than on the first request; thereafter currentToken serves the cache and refreshes behind it.

refreshingProviderWith :: IO () -> RefreshConfig -> IO CredentialProvider Source #

As refreshingProvider, but with a hook run on the serving thread at the single-flight claim → mint-runner handoff: the interruptible window between the STM transaction committing the claim and the mint runner installing the scope that releases it. It exists only so a test can deterministically park a serving thread in that window and cancel it there; production always passes pure () via refreshingProvider.

Telemetry reporters

data RefreshReporter Source #

An observer of a refresh attempt's outcome, so the composition root can record the ecluse.credential.* signals without the refresh policy depending on telemetry. A successful mint reports the freshly minted token's remaining lifetime; a failed mint reports the still-cached token's remaining lifetime (so a sustained outage shows the gauge decaying as repeated failures resample the ageing token). The seconds are Nothing for a token with no expiry. noRefreshReporter is the inert default.

Constructors

RefreshReporter 

Fields

noRefreshReporter :: RefreshReporter Source #

The inert refresh reporter: records nothing on either outcome.

data CredentialReporters Source #

The telemetry observers a refreshing provider records through: the mint circuit breaker's state changes and each refresh attempt's outcome. Bundled so the composition root passes one value to the provider constructors; noCredentialReporters is the inert default the provider carries when telemetry is off (or before the substrate exists).

Constructors

CredentialReporters 

Fields

noCredentialReporters :: CredentialReporters Source #

Inert observers for both signals: the provider records nothing.

Failure

data CredentialError Source #

A failure surfaced from the credential-refresh layer.

The runtime case is BreakerOpen: there is no valid token to serve and a fresh mint is unavailable. A still-valid token is always served instead (the refresh fails silently in the background), so this is reached only on the expired-token path. Whether reaching it can affect a client serve depends on what the credential backs: never under the default passthrough strategy (mirror-write only), but it can where a provider sits on the private-upstream read (see the module header).

The degenerate case is Unconfigured: a RefreshConfig from defaultRefreshConfig was used without supplying an effectful leaf, a wiring fault the default raises loudly rather than silently serving nothing.

Constructors

BreakerOpen

The token has expired and the mint circuit breaker is open, so no mint is attempted; the caller must back off and retry later.

Unconfigured Text

A RefreshConfig built from defaultRefreshConfig was used without supplying the named effectful leaf (rcMint or rcClock). A wiring fault, not a runtime token condition.

State and pure/transition helpers (exposed for direct testing)

data CacheState Source #

The mutable state of a refreshing provider: the cached token, when its proactive refresh is due, the single-flight flag, and the breaker.

Constructors

CacheState 

Fields

data ServeAction Source #

What a serve/decide decision resolves to.

Constructors

ServeCached AuthToken

The cached token is valid and no refresh is due: serve it.

ServeAndRefresh AuthToken

Valid but past the refresh threshold: serve it, refresh in background.

MintNow

Expired: the caller must mint synchronously (the slow path).

Instances

Instances details
Show ServeAction Source # 
Instance details

Defined in Ecluse.Credential.Refresh.Internal

Eq ServeAction Source # 
Instance details

Defined in Ecluse.Credential.Refresh.Internal

decide :: TVar CacheState -> UTCTime -> STM ServeAction Source #

The single-flight decision over the current cache state, made atomically so it holds across a concurrent cohort: serve the still-valid token, claim the flag and route to a background refresh when one is due, or — when the token has expired — either claim the flag and mint synchronously or, if a mint is already in flight, retry (block) until it lands rather than launching a second. The flag claim happens here, in the transaction, so at most one mint is ever launched; the claiming caller is responsible for releasing it (see serve / releaseSingleFlight).

refreshDueAt :: RefreshConfig -> UTCTime -> AuthToken -> IO (Maybe UTCTime) Source #

Compute when a freshly minted token's proactive refresh should fire: the rcRefreshAt fraction of its lifetime, pulled earlier by a per-token jitter sample and capped at rcRefreshFloor before expiry. A token with no expiry never refreshes (Nothing).

onMintSuccess :: AuthToken -> Maybe UTCTime -> CacheState -> CacheState Source #

Fold a successful mint into the cache: install the token and reset the breaker. The single-flight flag is released by releaseSingleFlight in the finally around the mint (not here), so it clears even on an async exception.

onMintFailure :: RefreshConfig -> UTCTime -> CacheState -> CacheState Source #

Fold a failed mint into the cache: keep the still-cached token and advance the breaker per the configured threshold and cooldown (recordFailure). The single-flight flag is released separately by releaseSingleFlight (see onMintSuccess).

admitMint :: TVar CacheState -> UTCTime -> STM Bool Source #

The circuit-breaker admission gate, shared by the background and synchronous mint paths. Defers the decision to admit and commits the breaker state it returns: while open and cooling down it denies (fast-fail); once the cooldown elapses it moves to half-open and admits a single probe; a closed or half-open breaker always admits.

releaseSingleFlight :: TVar CacheState -> IO () Source #

Release the single-flight flag. It is run in a finally that is installed in the same masked scope that claimed the flag — serves own finally for the synchronous mint, the background Async for a proactive refresh — so the flag is cleared on every exit: success, a synchronous mint failure, or an asynchronous exception (cancellation / timeout) at any point from the claim onward, including the handoff between the STM commit and the mint runner. Without this an orphaned flag would wedge every later expired caller on the STM retry. The flag is held for the whole operation, so no concurrent mint can re-claim it mid-flight — an unconditional release here therefore cannot clobber another operation's claim.