Skip to content

Commit 118a177

Browse files
authored
feat: add isolation level option for disabling internal retries (#327)
Internal retries of aborted transactions is enabled by default for database/sql connections for Spanner. However, when an application or framework knows that it will retry the transaction using a retry-loop, it is more efficient to disable internal retries. Some frameworks, such as gorm, however make this hard to achieve, as they only allow TxOptions to be used to configure the transaction, and do not give access to the underlying driver or connection. This change adds a custom isolation level that can be used to disable internal retries of aborted transactions.
1 parent b4803a6 commit 118a177

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

driver.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,20 @@ func (c *conn) resetTransactionForRetry(ctx context.Context, errDuringCommit boo
12161216
return c.tx.resetForRetry(ctx)
12171217
}
12181218

1219+
type spannerIsolationLevel sql.IsolationLevel
1220+
1221+
const (
1222+
levelNone spannerIsolationLevel = iota
1223+
levelDisableRetryAborts
1224+
)
1225+
1226+
// WithDisableRetryAborts returns a specific Spanner isolation level that contains
1227+
// both the given standard isolation level and a custom Spanner isolation level that
1228+
// disables internal retries for aborted transactions for a single transaction.
1229+
func WithDisableRetryAborts(level sql.IsolationLevel) sql.IsolationLevel {
1230+
return sql.IsolationLevel(levelDisableRetryAborts)<<8 + level
1231+
}
1232+
12191233
func (c *conn) Begin() (driver.Tx, error) {
12201234
return c.BeginTx(context.Background(), driver.TxOptions{})
12211235
}
@@ -1231,6 +1245,15 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
12311245
if c.inBatch() {
12321246
return nil, status.Error(codes.FailedPrecondition, "This connection has an active batch. Run or abort the batch before starting a new transaction.")
12331247
}
1248+
disableRetryAborts := false
1249+
sil := opts.Isolation >> 8
1250+
opts.Isolation = opts.Isolation - sil
1251+
if sil > 0 {
1252+
switch spannerIsolationLevel(sil) {
1253+
case levelDisableRetryAborts:
1254+
disableRetryAborts = true
1255+
}
1256+
}
12341257

12351258
if opts.ReadOnly {
12361259
ro := c.client.ReadOnlyTransaction().WithTimestampBound(c.readOnlyStaleness)
@@ -1259,7 +1282,7 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
12591282
c.commitTs = commitTs
12601283
}
12611284
},
1262-
retryAborts: c.retryAborts,
1285+
retryAborts: c.retryAborts && !disableRetryAborts,
12631286
}
12641287
c.commitTs = nil
12651288
return c.tx, nil

driver_with_mockserver_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2922,6 +2922,78 @@ func TestRunTransactionCommitError(t *testing.T) {
29222922
}
29232923
}
29242924

2925+
func TestTransactionWithLevelDisableRetryAborts(t *testing.T) {
2926+
t.Parallel()
2927+
2928+
ctx := context.Background()
2929+
db, server, teardown := setupTestDBConnection(t)
2930+
defer teardown()
2931+
2932+
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: WithDisableRetryAborts(sql.LevelSerializable)})
2933+
if err != nil {
2934+
t.Fatal(err)
2935+
}
2936+
rows, err := tx.Query(testutil.SelectFooFromBar)
2937+
if err != nil {
2938+
t.Fatal(err)
2939+
}
2940+
defer rows.Close()
2941+
for want := int64(1); rows.Next(); want++ {
2942+
cols, err := rows.Columns()
2943+
if err != nil {
2944+
t.Fatal(err)
2945+
}
2946+
if !cmp.Equal(cols, []string{"FOO"}) {
2947+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"FOO"})
2948+
}
2949+
var got int64
2950+
err = rows.Scan(&got)
2951+
if err != nil {
2952+
t.Fatal(err)
2953+
}
2954+
if got != want {
2955+
t.Fatalf("value mismatch\nGot: %v\nWant: %v", got, want)
2956+
}
2957+
}
2958+
if err := rows.Err(); err != nil {
2959+
t.Fatal(err)
2960+
}
2961+
// Simulate that the transaction was aborted.
2962+
server.TestSpanner.PutExecutionTime(testutil.MethodCommitTransaction, testutil.SimulatedExecutionTime{
2963+
Errors: []error{gstatus.Error(codes.Aborted, "Aborted")},
2964+
})
2965+
// Committing the transaction should fail, as we have disabled internal retries.
2966+
err = tx.Commit()
2967+
if err == nil {
2968+
t.Fatal("missing aborted error after commit")
2969+
}
2970+
code := spanner.ErrCode(err)
2971+
if w, g := code, codes.Aborted; w != g {
2972+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
2973+
}
2974+
2975+
requests := drainRequestsFromServer(server.TestSpanner)
2976+
sqlRequests := requestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{}))
2977+
if g, w := len(sqlRequests), 1; g != w {
2978+
t.Fatalf("ExecuteSqlRequests count mismatch\nGot: %v\nWant: %v", g, w)
2979+
}
2980+
req := sqlRequests[0].(*sppb.ExecuteSqlRequest)
2981+
if req.Transaction == nil {
2982+
t.Fatalf("missing transaction for ExecuteSqlRequest")
2983+
}
2984+
if req.Transaction.GetId() == nil {
2985+
t.Fatalf("missing id selector for ExecuteSqlRequest")
2986+
}
2987+
commitRequests := requestsOfType(requests, reflect.TypeOf(&sppb.CommitRequest{}))
2988+
if g, w := len(commitRequests), 1; g != w {
2989+
t.Fatalf("commit requests count mismatch\nGot: %v\nWant: %v", g, w)
2990+
}
2991+
commitReq := commitRequests[0].(*sppb.CommitRequest)
2992+
if c, e := commitReq.GetTransactionId(), req.Transaction.GetId(); !cmp.Equal(c, e) {
2993+
t.Fatalf("transaction id mismatch\nCommit: %c\nExecute: %v", c, e)
2994+
}
2995+
}
2996+
29252997
func numeric(v string) big.Rat {
29262998
res, _ := big.NewRat(1, 1).SetString(v)
29272999
return *res

0 commit comments

Comments
 (0)