Skip to content

Commit d4c1ad8

Browse files
tz70sodeke-em
authored andcommitted
database/sql: fix tx stmt deadlock when rollback
Tx acquires tx.closemu W-lock and then acquires stmt.closemu.W-lock to fully close the transaction and associated prepared statement. Stmt query and execution run in reverse ways - acquires stmt.closemu.R-lock and then acquires tx.closemu.R-lock to grab tx connection, which may cause deadlock. Prevent the lock is held around tx.closePrepared to ensure no deadlock happens. Fixes #40985 Change-Id: If53909822b87bce11861a6e3035ecb9476d2cd17 Reviewed-on: https://go-review.googlesource.com/c/go/+/250178 Run-TryBot: Emmanuel Odeke <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Daniel Theophanes <[email protected]> Trust: Emmanuel Odeke <[email protected]>
1 parent 421d4e7 commit d4c1ad8

File tree

2 files changed

+37
-7
lines changed

2 files changed

+37
-7
lines changed

Diff for: src/database/sql/sql.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -2087,10 +2087,10 @@ func (tx *Tx) isDone() bool {
20872087
// that has already been committed or rolled back.
20882088
var ErrTxDone = errors.New("sql: transaction has already been committed or rolled back")
20892089

2090-
// closeLocked returns the connection to the pool and
2090+
// close returns the connection to the pool and
20912091
// must only be called by Tx.rollback or Tx.Commit while
2092-
// closemu is Locked and tx already canceled.
2093-
func (tx *Tx) closeLocked(err error) {
2092+
// tx is already canceled and won't be executed concurrently.
2093+
func (tx *Tx) close(err error) {
20942094
tx.releaseConn(err)
20952095
tx.dc = nil
20962096
tx.txi = nil
@@ -2164,7 +2164,7 @@ func (tx *Tx) Commit() error {
21642164
// to ensure no other connection has an active query.
21652165
tx.cancel()
21662166
tx.closemu.Lock()
2167-
defer tx.closemu.Unlock()
2167+
tx.closemu.Unlock()
21682168

21692169
var err error
21702170
withLock(tx.dc, func() {
@@ -2173,7 +2173,7 @@ func (tx *Tx) Commit() error {
21732173
if err != driver.ErrBadConn {
21742174
tx.closePrepared()
21752175
}
2176-
tx.closeLocked(err)
2176+
tx.close(err)
21772177
return err
21782178
}
21792179

@@ -2196,7 +2196,7 @@ func (tx *Tx) rollback(discardConn bool) error {
21962196
// to ensure no other connection has an active query.
21972197
tx.cancel()
21982198
tx.closemu.Lock()
2199-
defer tx.closemu.Unlock()
2199+
tx.closemu.Unlock()
22002200

22012201
var err error
22022202
withLock(tx.dc, func() {
@@ -2208,7 +2208,7 @@ func (tx *Tx) rollback(discardConn bool) error {
22082208
if discardConn {
22092209
err = driver.ErrBadConn
22102210
}
2211-
tx.closeLocked(err)
2211+
tx.close(err)
22122212
return err
22132213
}
22142214

Diff for: src/database/sql/sql_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -2810,6 +2810,36 @@ func TestTxCannotCommitAfterRollback(t *testing.T) {
28102810
}
28112811
}
28122812

2813+
// Issue 40985 transaction statement deadlock while context cancel.
2814+
func TestTxStmtDeadlock(t *testing.T) {
2815+
db := newTestDB(t, "people")
2816+
defer closeDB(t, db)
2817+
2818+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
2819+
defer cancel()
2820+
tx, err := db.BeginTx(ctx, nil)
2821+
if err != nil {
2822+
t.Fatal(err)
2823+
}
2824+
2825+
stmt, err := tx.Prepare("SELECT|people|name,age|age=?")
2826+
if err != nil {
2827+
t.Fatal(err)
2828+
}
2829+
// Run number of stmt queries to reproduce deadlock from context cancel
2830+
for i := 0; i < 1e3; i++ {
2831+
_, err = stmt.Query(1)
2832+
if err != nil {
2833+
// Encounter ErrTxDone here is expected due to context cancel
2834+
if err != ErrTxDone {
2835+
t.Fatalf("unexpected error while executing stmt, err: %v", err)
2836+
}
2837+
break
2838+
}
2839+
}
2840+
_ = tx.Rollback()
2841+
}
2842+
28132843
// Issue32530 encounters an issue where a connection may
28142844
// expire right after it comes out of a used connection pool
28152845
// even when a new connection is requested.

0 commit comments

Comments
 (0)