Skip to content

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.