64 lines
1.9 KiB
Go
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"
|
|
}
|