Skip to content

Commit cd66761

Browse files
zikaerohgopherbot
authored andcommitted
database/sql: prevent internal context error from being returned from Rows.Err()
CL 497675 modified Rows such that context errors are propagated through Rows.Err(). This caused an issue where calling Close meant that an internal cancellation error would (eventually) be returned from Err: 1. A caller makes a query using a cancellable context. 2. initContextClose sees that either the query context or the transaction context can be canceled, so will need to spawn a goroutine to capture their errors. 3. initContextClose derives a context from the query context via WithCancel and sets rs.cancel. 4. When a user calls Close, rs.cancel is called. awaitDone's ctx is cancelled, which is good, since we don't want it to hang forever. 5. This internal cancellation (after CL 497675) has its error saved on contextDone. 6. Later, calling Err will return the error in contextDone if present. This leads to a race condition depending on how quickly Err is called after Close. The docs for Close and Err state that calling Close should have no affect on the return result for Err. So, a potential fix is to ensure that awaitDone does not save the error when the cancellation comes from a Close via rs.cancel. This CL does that, using a new context not derived from the query context, whose error is ignored as the query context's error used to be before the original bugfix. The included test fails before the CL, and passes afterward. Fixes #60932 Change-Id: I2bf4c549efd83d62b86e298c9c45ebd06a3ad89a Reviewed-on: https://go-review.googlesource.com/c/go/+/505397 Auto-Submit: Russ Cox <[email protected]> Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Russ Cox <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b2215e4 commit cd66761

File tree

2 files changed

+36
-6
lines changed

2 files changed

+36
-6
lines changed

src/database/sql/sql.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2944,15 +2944,17 @@ func (rs *Rows) initContextClose(ctx, txctx context.Context) {
29442944
if bypassRowsAwaitDone {
29452945
return
29462946
}
2947-
ctx, rs.cancel = context.WithCancel(ctx)
2948-
go rs.awaitDone(ctx, txctx)
2947+
closectx, cancel := context.WithCancel(ctx)
2948+
rs.cancel = cancel
2949+
go rs.awaitDone(ctx, txctx, closectx)
29492950
}
29502951

2951-
// awaitDone blocks until either ctx or txctx is canceled. The ctx is provided
2952-
// from the query context and is canceled when the query Rows is closed.
2952+
// awaitDone blocks until ctx, txctx, or closectx is canceled.
2953+
// The ctx is provided from the query context.
29532954
// If the query was issued in a transaction, the transaction's context
2954-
// is also provided in txctx to ensure Rows is closed if the Tx is closed.
2955-
func (rs *Rows) awaitDone(ctx, txctx context.Context) {
2955+
// is also provided in txctx, to ensure Rows is closed if the Tx is closed.
2956+
// The closectx is closed by an explicit call to rs.Close.
2957+
func (rs *Rows) awaitDone(ctx, txctx, closectx context.Context) {
29562958
var txctxDone <-chan struct{}
29572959
if txctx != nil {
29582960
txctxDone = txctx.Done()
@@ -2964,6 +2966,9 @@ func (rs *Rows) awaitDone(ctx, txctx context.Context) {
29642966
case <-txctxDone:
29652967
err := txctx.Err()
29662968
rs.contextDone.Store(&err)
2969+
case <-closectx.Done():
2970+
// rs.cancel was called via Close(); don't store this into contextDone
2971+
// to ensure Err() is unaffected.
29672972
}
29682973
rs.close(ctx.Err())
29692974
}

src/database/sql/sql_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4493,6 +4493,31 @@ func TestContextCancelBetweenNextAndErr(t *testing.T) {
44934493
}
44944494
}
44954495

4496+
func TestNilErrorAfterClose(t *testing.T) {
4497+
db := newTestDB(t, "people")
4498+
defer closeDB(t, db)
4499+
4500+
// This WithCancel is important; Rows contains an optimization to avoid
4501+
// spawning a goroutine when the query/transaction context cannot be
4502+
// canceled, but this test tests a bug which is caused by said goroutine.
4503+
ctx, cancel := context.WithCancel(context.Background())
4504+
defer cancel()
4505+
4506+
r, err := db.QueryContext(ctx, "SELECT|people|name|")
4507+
if err != nil {
4508+
t.Fatal(err)
4509+
}
4510+
4511+
if err := r.Close(); err != nil {
4512+
t.Fatal(err)
4513+
}
4514+
4515+
time.Sleep(10 * time.Millisecond) // increase odds of seeing failure
4516+
if err := r.Err(); err != nil {
4517+
t.Fatal(err)
4518+
}
4519+
}
4520+
44964521
// badConn implements a bad driver.Conn, for TestBadDriver.
44974522
// The Exec method panics.
44984523
type badConn struct{}

0 commit comments

Comments
 (0)