Choose a strategy¶
Three strategies, and the right one is decided by your posture and your provider — not by this package. Three questions settle it.
The three questions¶
1. Does the value refresh itself?
An aws.Config from LoadDefaultConfig renews credentials internally. So does
an azcore.TokenCredential, and a Google cached token provider. A bare token
string — VAULT_TOKEN, a Consul ACL token, a forge API token — does not.
2. Are you willing to hold a credential between operations?
A signing service may deliberately decline to, so that credentials are not resident in memory longer than a mint requires. That is a security posture, and it is yours to choose even when the value would be perfectly safe to hold.
3. Is building it expensive?
A full credential-chain resolution reaching a metadata service is expensive. A struct literal is not.
The answers¶
| Refreshes itself? | Willing to hold? | Strategy |
|---|---|---|
| yes | yes | Memoised |
| yes | no | PerCall |
| no | — | Invalidatable if building is expensive, PerCall if it is cheap |
// The value renews itself and you are happy to hold it.
src := clientlifecycle.Memoised(build)
// You decline to hold a credential between operations.
src := clientlifecycle.PerCall(build)
// The value cannot renew itself, and rebuilding it every time is too expensive.
src := clientlifecycle.Invalidatable(build)
All three satisfy Resolver[T], so switching posture is a one-word change at the
construction site and nothing else. Invalidatable additionally satisfies
Invalidator[T].
Two mistakes worth naming¶
Reaching for PerCall because the value goes stale. If the value cannot
renew itself, PerCall is correct but may be ruinous: you pay a full
resolution on every operation. That is the gap Invalidatable exists to fill —
hold and reuse, then re-resolve when told. Only take PerCall for a stale-able
value when building it is cheap.
Reaching for Invalidatable because it sounds strictly better. It is not. If
your value refreshes itself, Memoised is correct and Invalidatable is worse:
it adds an invalidation path nobody should ever call, and if somebody does call
it wrongly you get the fan-out described in
invalidating a stale credential. Take the
weakest strategy that is correct.
Bounding and scoping¶
Every strategy takes the same options.
// Bound one attempt. Per attempt, not a total budget.
clientlifecycle.Memoised(build, clientlifecycle.WithBuildTimeout(5*time.Second))
// Tie attempts to the life of whatever owns the resolver.
clientlifecycle.Memoised(build, clientlifecycle.WithLifetimeContext(appCtx))
WithLifetimeContext is deliberately not a call's context: a caller's
cancellation must release that caller alone, while this is the separate,
longer-lived scope that says "this whole source is finished". A resolver whose
lifetime context is done refuses to build at all — including after an
invalidation, however many times it has been invalidated.
PerCall ignores it, being already scoped to its caller.