Add dbx library: PostgreSQL cluster with master/replica routing, retry, health checking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
94
errors.go
Normal file
94
errors.go
Normal file
@@ -0,0 +1,94 @@
|
||||
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}
|
||||
}
|
||||
Reference in New Issue
Block a user