127 lines
3.8 KiB
Go
127 lines
3.8 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
)
|
|
|
|
// TxStage identifies the operation which produced the primary error.
|
|
type TxStage string
|
|
|
|
const (
|
|
TxStageBegin TxStage = "begin"
|
|
TxStageWork TxStage = "work"
|
|
TxStageCommit TxStage = "commit"
|
|
)
|
|
|
|
// TxOutcome describes only what InTx can confirm about its managed transaction.
|
|
// An absent/unrecognized value must be treated as unknown, never as rolled back.
|
|
type TxOutcome string
|
|
|
|
const (
|
|
TxOutcomeUnknown TxOutcome = "unknown"
|
|
TxOutcomeNotStarted TxOutcome = "notStarted"
|
|
TxOutcomeRolledBack TxOutcome = "rolledBack"
|
|
)
|
|
|
|
// TxError retains the primary failure and any rollback failure independently.
|
|
// Use errors.As to inspect it. errors.Is/As traverse Err first, then RollbackErr;
|
|
// inspect Err alone when the distinction between the two causes matters.
|
|
// Error text is for internal diagnostics and may include driver/business details.
|
|
type TxError struct {
|
|
Stage TxStage
|
|
Outcome TxOutcome
|
|
Err error
|
|
RollbackErr error
|
|
}
|
|
|
|
func (e *TxError) Error() string {
|
|
if e == nil {
|
|
return "transaction error"
|
|
}
|
|
message := string(e.Stage) + " transaction (" + string(e.Outcome) + ")"
|
|
if e.Err != nil {
|
|
message += ": " + e.Err.Error()
|
|
}
|
|
if e.RollbackErr != nil {
|
|
message += "; rollback transaction: " + e.RollbackErr.Error()
|
|
}
|
|
return message
|
|
}
|
|
|
|
// Unwrap preserves both causes, including sql.ErrTxDone. That sentinel means
|
|
// the local transaction is finished, not that the database confirmed rollback.
|
|
func (e *TxError) Unwrap() []error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
if e.Err == nil {
|
|
if e.RollbackErr == nil {
|
|
return nil
|
|
}
|
|
return []error{e.RollbackErr}
|
|
}
|
|
if e.RollbackErr == nil {
|
|
return []error{e.Err}
|
|
}
|
|
return []error{e.Err, e.RollbackErr}
|
|
}
|
|
|
|
// InTx begins a transaction, calls work once, and commits only if work returns
|
|
// nil. Every returned error is a *TxError; a nil result confirms Commit succeeded.
|
|
// Commit failures always have an unknown outcome, even for cancellation or a
|
|
// driver error normally associated with a lost connection. InTx does not retry.
|
|
//
|
|
// work must use only the supplied transaction for transactional operations and
|
|
// must not Commit/Rollback it, issue transaction-control/implicit-commit SQL, or
|
|
// let it escape to background goroutines. Outcome describes this transaction,
|
|
// not files, other connections, nontransactional tables or external side effects.
|
|
//
|
|
// Panic and runtime.Goexit still attempt rollback and propagate unchanged; they
|
|
// do not return a TxError. Rollback errors during those unwinds cannot be returned.
|
|
// A nil context is treated as context.Background().
|
|
func (db *DB) InTx(ctx context.Context, options *sql.TxOptions, work func(*sql.Tx) error) error {
|
|
if db == nil || db.DB == nil {
|
|
return &TxError{Stage: TxStageBegin, Outcome: TxOutcomeNotStarted, Err: errors.New("database is not initialized")}
|
|
}
|
|
if work == nil {
|
|
return &TxError{Stage: TxStageBegin, Outcome: TxOutcomeNotStarted, Err: errors.New("transaction callback is nil")}
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
tx, err := db.BeginTx(ctx, options)
|
|
if err != nil {
|
|
return &TxError{Stage: TxStageBegin, Outcome: TxOutcomeNotStarted, Err: err}
|
|
}
|
|
finished := false
|
|
defer func() {
|
|
if !finished {
|
|
// Only the panic/Goexit path reaches here without explicit cleanup.
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
if err = work(tx); err != nil {
|
|
failure := txFailure(tx, TxStageWork, err)
|
|
finished = true
|
|
return failure
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
failure := txFailure(tx, TxStageCommit, err)
|
|
finished = true
|
|
return failure
|
|
}
|
|
finished = true
|
|
return nil
|
|
}
|
|
|
|
func txFailure(tx *sql.Tx, stage TxStage, cause error) *TxError {
|
|
rollbackErr := tx.Rollback()
|
|
outcome := TxOutcomeUnknown
|
|
if stage == TxStageWork && rollbackErr == nil {
|
|
outcome = TxOutcomeRolledBack
|
|
}
|
|
return &TxError{Stage: stage, Outcome: outcome, Err: cause, RollbackErr: rollbackErr}
|
|
}
|