mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
70 lines
2.3 KiB
Go
70 lines
2.3 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// poisonedTxSubstrings are error-message substrings emitted when go-libsql or the
|
|
// remote libsql server leaves a connection in a state that cannot safely be reused.
|
|
// Most come from Bunny Database killing a transaction that exceeded its server-side
|
|
// timeout; the follow-on COMMIT then sees the connection in a poisoned state.
|
|
var poisonedTxSubstrings = []string{
|
|
"Transaction timed-out",
|
|
"no transaction is active",
|
|
"connection has reached an invalid state",
|
|
"invalid state, started with",
|
|
}
|
|
|
|
// IsPoisonedTxErr reports whether err indicates the underlying connection is no
|
|
// longer usable for further statements. Callers should evict the connection from
|
|
// the pool when this returns true.
|
|
func IsPoisonedTxErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := err.Error()
|
|
for _, s := range poisonedTxSubstrings {
|
|
if strings.Contains(msg, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ExecResilient borrows a dedicated connection from db, runs fn against it, and
|
|
// evicts the connection from the pool when fn returns a poisoned-transaction
|
|
// error. The connection is always released via Close.
|
|
//
|
|
// Poison eviction works by returning driver.ErrBadConn from within conn.Raw:
|
|
// database/sql treats that as a signal to discard the underlying driver conn
|
|
// rather than returning it to the idle pool.
|
|
//
|
|
// ExecResilient does NOT retry. Callers wrap the call in their own retry policy
|
|
// when that is desired (for example, a single retry on the live Jetstream path).
|
|
func ExecResilient(ctx context.Context, db *sql.DB, fn func(*sql.Conn) error) error {
|
|
conn, err := db.Conn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
execErr := fn(conn)
|
|
if IsPoisonedTxErr(execErr) {
|
|
// Discard the underlying driver conn so it never serves another caller.
|
|
// The Raw callback's return value is what triggers eviction; we ignore
|
|
// any error from Raw itself.
|
|
_ = conn.Raw(func(any) error { return driver.ErrBadConn })
|
|
}
|
|
return execErr
|
|
}
|
|
|
|
// ErrNoPoolConn is returned by ExecResilient when a connection cannot be
|
|
// obtained from the pool (e.g. context cancelled). It wraps the underlying
|
|
// pool error for callers that want to distinguish pool-exhaustion from
|
|
// statement-level errors.
|
|
var ErrNoPoolConn = errors.New("db: failed to acquire pool connection")
|