mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
database/sql: prevent Tx statement from committing after rollback
It was possible for a Tx that was aborted for rollback asynchronously to execute a query after the rollback had completed on the database, which often would auto commit the query outside of the transaction. By W-locking the tx.closemu prior to issuing the rollback connection it ensures any Tx query either fails or finishes on the Tx, and never after the Tx has rolled back. Fixes #34775 Fixes #32942 Change-Id: I017b7932082f2f4ead70bae08b61ed9068ac1d01 Reviewed-on: https://go-review.googlesource.com/c/go/+/216240 Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com>
This commit is contained in:
parent
b2cff7e091
commit
d8f0a229b5
3 changed files with 136 additions and 12 deletions
|
|
@ -80,6 +80,11 @@ func newTestDBConnector(t testing.TB, fc *fakeConnector, name string) *DB {
|
|||
exec(t, db, "CREATE|magicquery|op=string,millis=int32")
|
||||
exec(t, db, "INSERT|magicquery|op=sleep,millis=10")
|
||||
}
|
||||
if name == "tx_status" {
|
||||
// Magic table name and column, known by fakedb_test.go.
|
||||
exec(t, db, "CREATE|tx_status|tx_status=string")
|
||||
exec(t, db, "INSERT|tx_status|tx_status=invalid")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
|
|
@ -2707,6 +2712,70 @@ func TestManyErrBadConn(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Issue 34755: Ensure that a Tx cannot commit after a rollback.
|
||||
func TestTxCannotCommitAfterRollback(t *testing.T) {
|
||||
db := newTestDB(t, "tx_status")
|
||||
defer closeDB(t, db)
|
||||
|
||||
// First check query reporting is correct.
|
||||
var txStatus string
|
||||
err := db.QueryRow("SELECT|tx_status|tx_status|").Scan(&txStatus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := txStatus, "autocommit"; g != w {
|
||||
t.Fatalf("tx_status=%q, wanted %q", g, w)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ignore dirty session for this test.
|
||||
// A failing test should trigger the dirty session flag as well,
|
||||
// but that isn't exactly what this should test for.
|
||||
tx.txi.(*fakeTx).c.skipDirtySession = true
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
err = tx.QueryRow("SELECT|tx_status|tx_status|").Scan(&txStatus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := txStatus, "transaction"; g != w {
|
||||
t.Fatalf("tx_status=%q, wanted %q", g, w)
|
||||
}
|
||||
|
||||
// 1. Begin a transaction.
|
||||
// 2. (A) Start a query, (B) begin Tx rollback through a ctx cancel.
|
||||
// 3. Check if 2.A has committed in Tx (pass) or outside of Tx (fail).
|
||||
sendQuery := make(chan struct{})
|
||||
hookTxGrabConn = func() {
|
||||
cancel()
|
||||
<-sendQuery
|
||||
}
|
||||
rollbackHook = func() {
|
||||
close(sendQuery)
|
||||
}
|
||||
defer func() {
|
||||
hookTxGrabConn = nil
|
||||
rollbackHook = nil
|
||||
}()
|
||||
|
||||
err = tx.QueryRow("SELECT|tx_status|tx_status|").Scan(&txStatus)
|
||||
if err != nil {
|
||||
// A failure here would be expected if skipDirtySession was not set to true above.
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := txStatus, "transaction"; g != w {
|
||||
t.Fatalf("tx_status=%q, wanted %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue32530 encounters an issue where a connection may
|
||||
// expire right after it comes out of a used connection pool
|
||||
// even when a new connection is requested.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue