Skip to content

Commit a27c19a

Browse files
authored
fix(spanner): error instead of panic for iterator after tx end (#13366)
Return an error instead of panic if a RowIterator is used after the transaction that created it is committed or rolled back.
1 parent fc06d2f commit a27c19a

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

spanner/transaction.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ func errMultipleRowsFound(table string, key Key, index string) error {
432432
return spannerErrorf(codes.FailedPrecondition, "more than one row found by index(Table: %v, IndexKey: %v, Index: %v)", table, key, index)
433433
}
434434

435+
func errTransactionNoLongerActive() error {
436+
return spannerError(codes.FailedPrecondition, "the transaction that was used to execute this statement is no longer active")
437+
}
438+
435439
const errInlineBeginTransactionFailedMsg = "failed inline begin transaction"
436440

437441
// errInlineBeginTransactionFailed creates an error that indicates that the first statement in the
@@ -701,6 +705,12 @@ func (t *txReadOnly) query(ctx context.Context, statement Statement, options Que
701705
sh.session.logger,
702706
t.sp.sc.metricsTracerFactory,
703707
func(ctx context.Context, resumeToken []byte, opts ...gax.CallOption) (streamingReceiver, error) {
708+
// The session handle is removed from the transaction when the transaction is committed or rolled back.
709+
// This ensures that we return a reasonable error instead of panic if the application tries to use the
710+
// stream after the transaction has finished.
711+
if t.sh == nil {
712+
return nil, errTransactionNoLongerActive()
713+
}
704714
req.ResumeToken = resumeToken
705715
req.Session = t.sh.getID()
706716
req.Transaction = t.getTransactionSelector()

spanner/transaction_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,6 +2222,53 @@ func TestReadWriteTransactionUsesNewContextForRollback(t *testing.T) {
22222222
}
22232223
}
22242224

2225+
func TestReadFromQueryAfterCommitOrRollback(t *testing.T) {
2226+
t.Parallel()
2227+
2228+
ctx := context.Background()
2229+
_, client, teardown := setupMockedTestServer(t)
2230+
defer teardown()
2231+
2232+
testcases := []struct {
2233+
name string
2234+
commit bool
2235+
}{
2236+
{name: "AfterCommit", commit: true},
2237+
{name: "AfterRollback", commit: false},
2238+
}
2239+
for _, tc := range testcases {
2240+
t.Run(tc.name, func(t *testing.T) {
2241+
// Create a new transaction and execute a query using that transaction.
2242+
// Then try to read data from the row iterator after the transaction has finished.
2243+
tx, err := NewReadWriteStmtBasedTransaction(ctx, client)
2244+
if err != nil {
2245+
t.Fatalf("failed to create transaction: %v", err)
2246+
}
2247+
// 'Execute' the query using the transaction. Note that the query is only actually executed the first time
2248+
// that RowIterator.Next() is called.
2249+
it := tx.Query(ctx, NewStatement(SelectFooFromBar))
2250+
// Commit or rollback the transaction before reading any data.
2251+
if tc.commit {
2252+
if _, err := tx.Commit(ctx); err != nil {
2253+
t.Fatalf("failed to commit: %v", err)
2254+
}
2255+
} else {
2256+
tx.Rollback(ctx)
2257+
}
2258+
2259+
// Now try to read the data from the RowIterator that was returned for the query.
2260+
_, err = it.Next()
2261+
if g, w := ErrCode(err), codes.FailedPrecondition; g != w {
2262+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
2263+
}
2264+
if g, w := err.Error(), `spanner: code = "FailedPrecondition", desc = "the transaction that was used to execute this statement is no longer active"`; g != w {
2265+
t.Fatalf("error message mismatch\n Got: %v\nWant: %v", g, w)
2266+
}
2267+
it.Stop()
2268+
})
2269+
}
2270+
}
2271+
22252272
// shouldHaveReceived asserts that exactly expectedRequests were present in
22262273
// the server's ReceivedRequests channel. It only looks at type, not contents.
22272274
//

0 commit comments

Comments
 (0)