fix: preserve rollback errors in runTx (#24598)

Previously, `runTx` could lose a deferred rollback failure when returning an
existing transaction error, because the rollback path could not update the final
return value.

https://go.dev/play/p/AhBK31lO0Gd
This commit is contained in:
George K
2026-04-30 10:27:53 -07:00
committed by GitHub
parent 97cc83c83e
commit fb6e00de18
4 changed files with 32 additions and 1 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ func (q *sqlQuerier) InTx(function func(Store) error, txOpts *TxOptions) error {
}
// InTx performs database operations inside a transaction.
func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) error {
func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) (err error) {
if _, ok := q.db.(*sqlx.Tx); ok {
// If the current inner "db" is already a transaction, we just reuse it.
// We do not need to handle commit/rollback as the outer tx will handle
+29
View File
@@ -5,9 +5,11 @@ import (
"database/sql"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
@@ -82,6 +84,33 @@ func TestNestedInTx(t *testing.T) {
require.Equal(t, uid, user.ID, "user id expected")
}
func TestInTx_CapturesRollbackError(t *testing.T) {
t.Parallel()
sqlDB, mock, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(func() { _ = sqlDB.Close() })
db := database.New(sqlDB)
callbackErr := xerrors.New("callback failed")
rollbackErr := xerrors.New("rollback failed")
mock.ExpectBegin()
mock.ExpectRollback().WillReturnError(rollbackErr)
err = db.InTx(func(_ database.Store) error {
return callbackErr
}, nil)
require.EqualError(t, err, "defer (rollback failed): execute transaction: callback failed")
require.ErrorIs(t, err, callbackErr,
"returned error should still match the callback error when rollback fails")
require.NotErrorIs(t, err, rollbackErr,
"rollback failure should be reported in the message, not wrapped in the error chain")
require.NoError(t, mock.ExpectationsWereMet())
}
func testSQLDB(t testing.TB) *sql.DB {
t.Helper()