Files
story/internal/outbox/errors.go
T

64 lines
1.9 KiB
Go

package outbox
import (
"context"
"errors"
"net"
"github.com/jackc/pgx/v5/pgconn"
)
// retryableError is the explicit boundary between a temporary dependency
// failure and a permanent job failure. Handlers must opt in by wrapping an
// error with Retryable unless the infrastructure error is independently
// recognizable as a timeout, network interruption or transient PostgreSQL
// condition.
type retryableError struct{ cause error }
func (e retryableError) Error() string { return e.cause.Error() }
func (e retryableError) Unwrap() error { return e.cause }
// Retryable marks an error as safe to execute again with the same idempotency
// boundary. A nil error remains nil so callers can wrap return values directly.
func Retryable(err error) error {
if err == nil || IsRetryable(err) {
return err
}
return retryableError{cause: err}
}
// IsRetryable deliberately keeps the automatic allow-list narrow. Validation,
// unsupported state and malformed payload errors are permanent by default.
func IsRetryable(err error) bool {
if err == nil {
return false
}
var marked retryableError
if errors.As(err, &marked) {
return true
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
var networkError net.Error
if errors.As(err, &networkError) && (networkError.Timeout() || networkError.Temporary()) {
return true
}
if pgconn.SafeToRetry(err) {
return true
}
var postgresError *pgconn.PgError
if !errors.As(err, &postgresError) {
return false
}
// Connection, transaction rollback, resource exhaustion, lock-not-available
// and administrator restart conditions can succeed on a later attempt.
class := ""
if len(postgresError.Code) >= 2 {
class = postgresError.Code[:2]
}
return class == "08" || class == "40" || class == "53" ||
postgresError.Code == "55P03" || postgresError.Code == "57P01" ||
postgresError.Code == "57P02" || postgresError.Code == "57P03"
}