| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
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
- data RefreshConfig = RefreshConfig {}
- defaultRefreshConfig :: RefreshConfig
- refreshingProvider :: RefreshConfig -> IO CredentialProvider
- refreshingProviderWith :: IO () -> RefreshConfig -> IO CredentialProvider
- data RefreshReporter = RefreshReporter {
- onRefreshSucceeded :: Maybe Int -> IO ()
- onRefreshFailed :: Maybe Int -> IO ()
- noRefreshReporter :: RefreshReporter
- data CredentialReporters = CredentialReporters {}
- noCredentialReporters :: CredentialReporters
- data CredentialError
- data CacheState = CacheState {}
- data ServeAction
- decide :: TVar CacheState -> UTCTime -> STM ServeAction
- refreshDueAt :: RefreshConfig -> UTCTime -> AuthToken -> IO (Maybe UTCTime)
- onMintSuccess :: AuthToken -> Maybe UTCTime -> CacheState -> CacheState
- onMintFailure :: RefreshConfig -> UTCTime -> CacheState -> CacheState
- admitMint :: TVar CacheState -> UTCTime -> STM Bool
- releaseSingleFlight :: TVar CacheState -> IO ()
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.
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 |
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 # | |
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
| Show ServeAction Source # | |
Defined in Ecluse.Credential.Refresh.Internal Methods showsPrec :: Int -> ServeAction -> ShowS # show :: ServeAction -> String # showList :: [ServeAction] -> ShowS # | |
| Eq ServeAction Source # | |
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.