Files

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")