95 lines
2.1 KiB
Go
95 lines
2.1 KiB
Go
package dbx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
// Sentinel errors.
|
|
var (
|
|
ErrNoHealthyNode = errors.New("dbx: no healthy node available")
|
|
ErrClusterClosed = errors.New("dbx: cluster is closed")
|
|
ErrRetryExhausted = errors.New("dbx: retry attempts exhausted")
|
|
)
|
|
|
|
// IsRetryable reports whether the error is worth retrying.
|
|
// Connection errors, serialization failures, deadlocks, and too_many_connections are retryable.
|
|
func IsRetryable(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if IsConnectionError(err) {
|
|
return true
|
|
}
|
|
code := PgErrorCode(err)
|
|
switch code {
|
|
case "40001", // serialization_failure
|
|
"40P01", // deadlock_detected
|
|
"53300": // too_many_connections
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsConnectionError reports whether the error indicates a connection problem (PG class 08).
|
|
func IsConnectionError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
code := PgErrorCode(err)
|
|
if strings.HasPrefix(code, "08") {
|
|
return true
|
|
}
|
|
// pgx wraps connection errors that may not have a PG code
|
|
msg := err.Error()
|
|
for _, s := range []string{
|
|
"connection refused",
|
|
"connection reset",
|
|
"broken pipe",
|
|
"EOF",
|
|
"no connection",
|
|
"conn closed",
|
|
"timeout",
|
|
} {
|
|
if strings.Contains(msg, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsConstraintViolation reports whether the error is a constraint violation (PG class 23).
|
|
func IsConstraintViolation(err error) bool {
|
|
return strings.HasPrefix(PgErrorCode(err), "23")
|
|
}
|
|
|
|
// PgErrorCode extracts the PostgreSQL error code from err, or returns "" if not a PG error.
|
|
func PgErrorCode(err error) string {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) {
|
|
return pgErr.Code
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// retryError wraps the last error with ErrRetryExhausted context.
|
|
type retryError struct {
|
|
attempts int
|
|
last error
|
|
}
|
|
|
|
func (e *retryError) Error() string {
|
|
return fmt.Sprintf("%s: %d attempts, last error: %v", ErrRetryExhausted, e.attempts, e.last)
|
|
}
|
|
|
|
func (e *retryError) Unwrap() []error {
|
|
return []error{ErrRetryExhausted, e.last}
|
|
}
|
|
|
|
func newRetryError(attempts int, last error) error {
|
|
return &retryError{attempts: attempts, last: last}
|
|
}
|