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