| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Credential.Refresh
Description
The refresh cache expiry / concurrency policy behind a
CredentialProvider.
The interesting part of outbound auth is not the cloud call but the policy
around it: serve a cached token, refresh it proactively before it expires, never
stampede the token API, and stay up across a transient mint outage. That policy
is identical for every cloud, so it lives here once, parameterised over a tiny
per-cloud rcMint leaf (CodeArtifact's GetAuthorizationToken, an ADC OAuth2
token, …) and an injected rcClock. Only rcMint touches a network; everything
else is deterministic, so the whole policy is unit-tested with a fake clock and a
fake mint (see docs/architecture/cloud-backends.md → "Credential Provider").
The policy
- Proactive, background refresh. A token is refreshed when the clock passes a
fraction (
rcRefreshAt, ~80%) of its lifetime, withrcJitterto desynchronise a cohort of instances, plus a hard floor near expiry. Because the current token stays valid during the refresh, the request hot path never blocks on a mint in the common case — the refresh runs in the background and swaps the token in when it lands. - Single-flight. At most one mint is ever in flight per provider (an STM flag), so a cohort of callers crossing the threshold together never stampedes the cloud token API; the rest serve the still-valid cached token.
- Serve-stale on failure, behind a circuit breaker. A failing mint does not
fail the caller while the cached token is still valid — the wrapper keeps serving
it and retries later. Repeated failures trip a circuit breaker that fast-fails
further mints for a cooldown (
rcBreakerCooldown) before a single half-open probe tests recovery, so a sustained outage neither hammers the token API nor adds latency. Only an expired token together with a still-failing mint surfaces as an exception to the caller (the breaker shares its shape with the effectful-rule tier — seedocs/architecture/rules-engine.md→ "Effectful-rule failure").
A CredentialProvider always backs the mirror-target write; under the default
passthrough access strategy that is its only use, so even a fully failed refresh
touches only the mirror publish and never the client serve path. Where a mount
instead puts a provider on the private-upstream read (the service and
service-populated delegated-cache strategies), that dependent operation is a
client read, so an exhausted read credential degrades serving. The refresh policy
here is identical either way (see
docs/architecture/access-model.md → "Credential supply").
The implementation lives in Ecluse.Credential.Refresh.Internal; this module re-exports only the stable surface a caller needs.
Synopsis
- data RefreshConfig = RefreshConfig {}
- defaultRefreshConfig :: RefreshConfig
- refreshingProvider :: RefreshConfig -> IO CredentialProvider
- data RefreshReporter = RefreshReporter {
- onRefreshSucceeded :: Maybe Int -> IO ()
- onRefreshFailed :: Maybe Int -> IO ()
- noRefreshReporter :: RefreshReporter
- data CredentialReporters = CredentialReporters {}
- noCredentialReporters :: CredentialReporters
- data CredentialError
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
| |
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;
rcJittermay 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.
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 |
Instances
| Exception CredentialError Source # | |
Defined in Ecluse.Credential.Refresh.Internal Methods toException :: CredentialError -> SomeException # fromException :: SomeException -> Maybe CredentialError # displayException :: CredentialError -> String # backtraceDesired :: CredentialError -> Bool # | |
| Show CredentialError Source # | |
Defined in Ecluse.Credential.Refresh.Internal Methods showsPrec :: Int -> CredentialError -> ShowS # show :: CredentialError -> String # showList :: [CredentialError] -> ShowS # | |
| Eq CredentialError Source # | |
Defined in Ecluse.Credential.Refresh.Internal Methods (==) :: CredentialError -> CredentialError -> Bool # (/=) :: CredentialError -> CredentialError -> Bool # | |