Invalidate a stale credential¶
You hold a value that cannot renew itself — a token with an expiry — and
rebuilding it on every operation is too expensive. Invalidatable holds and
reuses it, and re-resolves when you say so.
src := clientlifecycle.Invalidatable(func(ctx context.Context) (string, error) {
return fetchToken(ctx)
})
tok, err := src.Get(ctx) // resolves once, shared
...
src.Invalidate() // that token is no longer valid
tok, err = src.Get(ctx) // resolves afresh
Invalidate is idempotent, safe to call concurrently with Get, and a no-op on
a resolver that has never built.
Invalidate only when the error asserts the credential is invalid¶
This is the whole of the difficulty, and getting it wrong is worse than not using the strategy at all.
Invalidate when the error asserts this credential is no longer valid. Never on "the call failed" — whatever its status code, and whether or not it looks transient.
| The provider says | Invalidate? | Why |
|---|---|---|
| your credential has expired or been revoked | yes | this is the case the strategy exists for |
| you are authenticated but not permitted | no | re-resolving yields the same principal; it loops forever |
| you are rate limited | no | back off — see below |
| something failed | no | you do not know that it was the credential |
Stated by what the error asserts rather than by status code, because not every
provider speaks HTTP — and because on some that do, one code carries two
meanings. GitHub returns 403 for both "not permitted" and, with
X-RateLimit-Remaining: 0, "rate limited". A caller who reads the number rather
than the meaning gets the second case wrong every time.
Why the rate-limit case is the dangerous one¶
Because single-flight is per generation, invalidation is the one operation here that can increase concurrency: attempts at different generations may overlap, and generations are created by you. A caller that invalidates on every failure does not merely retry in a loop — it aims a growing number of concurrent resolutions at a provider that is already refusing them, which is how a temporary throttle becomes a lockout.
That makes the ability to tell the three cases apart a prerequisite for wiring invalidation, not an improvement to it. If your errors can only say "something failed", you cannot use this safely yet. Fix the error taxonomy first.
What Invalidate deliberately does not do¶
It does not cancel an attempt already in flight. That attempt still returns its result to the callers waiting on it. Failing them to spare them a value that is merely about to be replaced would be worse — and the staleness is self-correcting, since a caller who receives an old credential and is refused will invalidate too.
It does not dispose the discarded value. A caller may still be mid-operation with one it obtained earlier, and nothing here can know. Which leads to the one real restriction:
Not for values that must be closed¶
If your T must be closed — a gRPC connection, a pooled client — do not use
Invalidatable. The discarded value has no owner at the moment it is dropped,
because a memoised value only ever existed inside the resolver. Nothing can close
it safely, and a disposal hook could not decide the question either: it would
have to answer "is anyone still using this?" with no information capable of
answering it. That is undecidable rather than merely deferred.
Closeable values belong to a consumer-owned type that holds and closes them — which is where the rest of the estate already puts them.