Keep permanent sentinel errors distinguishable (#33194)

This commit is contained in:
andig 2026-08-26 14:48:33 +02:00 • committed by GitHub
parent f8c1dec7b6
commit c314e8e838
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 17 deletions

View file

@ -7,23 +7,28 @@ import (
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
) )
// permanentError is a sentinel error that keeps matching errors.Is after // permanentError is a sentinel error that signals permanence to backoff while
// backoff has stripped the backoff.Permanent wrapper. // remaining distinguishable from the other permanent sentinels.
type permanentError struct { type permanentError struct {
msg string msg string
} }
func (e *permanentError) Error() string { return e.msg } func (e *permanentError) Error() string { return e.msg }
// Is matches the wrapped sentinel, too // As signals permanence to backoff. Wrapping the sentinel in backoff.Permanent
func (e *permanentError) Is(target error) bool { // instead would make errors.Is match any other permanent error, since
var t *permanentError // backoff.PermanentError.Is matches by type rather than identity.
return errors.As(target, &t) && t == e func (e *permanentError) As(target any) bool {
if p, ok := target.(**backoff.PermanentError); ok {
*p = &backoff.PermanentError{Err: e}
return true
}
return false
} }
// permanent creates a permanent sentinel error // permanent creates a permanent sentinel error
func permanent(msg string) error { func permanent(msg string) error {
return backoff.Permanent(&permanentError{msg}) return &permanentError{msg}
} }
// ErrNotAvailable indicates that a feature is not available // ErrNotAvailable indicates that a feature is not available

View file

@ -8,20 +8,47 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// permanentSentinels are all errors created by permanent()
var permanentSentinels = []error{
ErrNotAvailable,
ErrUnsupportedPlatform,
ErrSponsorRequired,
ErrMissingCredentials,
ErrMissingToken,
}
// Backoff returns permanent errors unwrapped. These must still match the sentinel. // Backoff returns permanent errors unwrapped. These must still match the sentinel.
func TestPermanentSentinels(t *testing.T) { func TestPermanentSentinels(t *testing.T) {
for _, tc := range []struct{ err, other error }{ for _, err := range permanentSentinels {
{ErrNotAvailable, ErrUnsupportedPlatform},
{ErrUnsupportedPlatform, ErrNotAvailable},
{ErrMissingCredentials, ErrMissingToken},
{ErrMissingToken, ErrMissingCredentials},
} {
_, unwrapped := backoff.RetryWithData(func() (int, error) { _, unwrapped := backoff.RetryWithData(func() (int, error) {
return 0, tc.err return 0, err
}, &backoff.StopBackOff{}) }, &backoff.StopBackOff{})
assert.ErrorIs(t, unwrapped, tc.err) assert.ErrorIs(t, unwrapped, err)
assert.ErrorIs(t, fmt.Errorf("wrapped: %w", unwrapped), tc.err) assert.ErrorIs(t, fmt.Errorf("wrapped: %w", unwrapped), err)
assert.NotErrorIs(t, unwrapped, tc.other) assert.ErrorIs(t, fmt.Errorf("wrapped: %w", err), err)
} }
} }
// Permanent sentinels must not match each other, whether returned directly or
// unwrapped by backoff.
func TestPermanentSentinelIdentity(t *testing.T) {
for _, err := range permanentSentinels {
_, unwrapped := backoff.RetryWithData(func() (int, error) {
return 0, err
}, &backoff.StopBackOff{})
for _, other := range permanentSentinels {
if other == err {
continue
}
assert.NotErrorIs(t, err, other)
assert.NotErrorIs(t, unwrapped, other)
assert.NotErrorIs(t, fmt.Errorf("wrapped: %w", err), other)
}
}
// login required is permanent, too
assert.NotErrorIs(t, LoginRequiredError("foo"), ErrNotAvailable)
}