Skip to content

Commit 21ed1e3

Browse files
authored
perf: inline BeginTransaction with first statement (#466)
* perf: inline BeginTransaction with first statement Inline the BeginTransaction option with the first statement in the transaction, instead of executing a separate BeginTransaction RPC. This reduces the number of round-trips to Spanner by one for all transactions that have at least one SQL statement. Using line-begin improves performance for most transaction shapes, as it requires one less round-trip to Spanner. Some transaction shapes do not benefit from this. These are: 1. Transactions that only write mutations still need an explicit BeginTransaction RPC to be executed, as mutations are included in the Commit RPC. The Commit RPC can also start a transaction, but such transactions are not guaranteed to be applied only once to Spanner. 2. Transactions that execute multiple parallel queries at the start of the transaction can see higher end-to-end execution times, as only one query can include the BeginTransaction option. All other queries must wait for the first query to return at least one result, which also includes the transaction identifier, before they can proceed. The default for the database/sql driver is to use inline-begin. A follow-up pull request will add an option to the driver to set a different default for a connection. * feat: add BeginTransactionOption (#467) Adds a BeginTransactionOption configuration field that can be used to determine how the database/sql driver should begin transactions. The default is to inline the BeginTransaction option with the first SQL statement of the transaction. This reduces the number of round-trips needed per transaction by one for most transaction shapes.
1 parent a518727 commit 21ed1e3

File tree

7 files changed

+300
-75
lines changed

7 files changed

+300
-75
lines changed

aborted_transactions_test.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func TestQueryWithError_CommitAborted(t *testing.T) {
325325
server.PutExecutionTime(testutil.MethodCommitTransaction, testutil.SimulatedExecutionTime{
326326
Errors: []error{status.Error(codes.Aborted, "Aborted")},
327327
})
328-
}, codes.NotFound, 0, 2, 2)
328+
}, codes.NotFound, 0, 3, 2)
329329
}
330330

331331
func TestQueryWithErrorHalfway_CommitAborted(t *testing.T) {
@@ -1080,7 +1080,7 @@ func TestBatchUpdateAbortedWithError_DifferentErrorDuringRetry(t *testing.T) {
10801080
t.Fatalf("dml statement failed: %v", err)
10811081
}
10821082
if _, err := tx.ExecContext(ctx, "RUN BATCH"); spanner.ErrCode(err) != codes.NotFound {
1083-
t.Fatalf("error code mismatch\nGot: %v\nWant: %v", spanner.ErrCode(err), codes.NotFound)
1083+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", spanner.ErrCode(err), codes.NotFound)
10841084
}
10851085

10861086
// Remove the error for the DML statement and cause a retry. The missing
@@ -1094,19 +1094,37 @@ func TestBatchUpdateAbortedWithError_DifferentErrorDuringRetry(t *testing.T) {
10941094
})
10951095
err = tx.Commit()
10961096
if err != ErrAbortedDueToConcurrentModification {
1097-
t.Fatalf("commit error mismatch\nGot: %v\nWant: %v", err, ErrAbortedDueToConcurrentModification)
1097+
t.Fatalf("commit error mismatch\n Got: %v\nWant: %v", err, ErrAbortedDueToConcurrentModification)
10981098
}
10991099
reqs := drainRequestsFromServer(server.TestSpanner)
11001100
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
1101-
if g, w := len(execReqs), 2; g != w {
1102-
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
1101+
// There are 3 ExecuteBatchDmlRequests sent to Spanner:
1102+
// 1. An initial attempt with a BeginTransaction RPC, but this returns a NotFound error.
1103+
// This causes the transaction to be retried with an explicit BeginTransaction request.
1104+
// 2. Another attempt with a transaction ID.
1105+
// 3. A third attempt after the initial transaction is aborted.
1106+
if g, w := len(execReqs), 3; g != w {
1107+
t.Fatalf("batch request count mismatch\n Got: %v\nWant: %v", g, w)
11031108
}
11041109
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
11051110
// The commit should be attempted only once.
11061111
if g, w := len(commitReqs), 1; g != w {
1107-
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
1112+
t.Fatalf("commit request count mismatch\n Got: %v\nWant: %v", g, w)
1113+
}
1114+
// The first ExecuteBatchDml request should try to use an inline-begin.
1115+
// After that, we should have two BeginTransaction requests.
1116+
req1 := execReqs[0].(*sppb.ExecuteBatchDmlRequest)
1117+
if req1.GetTransaction() == nil || req1.GetTransaction().GetBegin() == nil {
1118+
t.Fatal("the first ExecuteBatchDmlRequest should have a BeginTransaction")
1119+
}
1120+
req2 := execReqs[1].(*sppb.ExecuteBatchDmlRequest)
1121+
if req2.GetTransaction() == nil || req2.GetTransaction().GetId() == nil {
1122+
t.Fatal("the second ExecuteBatchDmlRequest should have a transaction id")
1123+
}
1124+
beginRequests := requestsOfType(reqs, reflect.TypeOf(&sppb.BeginTransactionRequest{}))
1125+
if g, w := len(beginRequests), 2; g != w {
1126+
t.Fatalf("begin request count mismatch\n Got: %v\nWant: %v", g, w)
11081127
}
1109-
11101128
// Verify that the db is still usable.
11111129
if _, err := db.ExecContext(ctx, testutil.UpdateSingersSetLastName); err != nil {
11121130
t.Fatalf("failed to execute statement after transaction: %v", err)

auto_dml_batch_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,13 @@ func TestAutoBatchDml_FollowedByRollback(t *testing.T) {
303303
if g, w := len(commitRequests), 0; g != w {
304304
t.Fatalf("num commit requests mismatch\n Got: %v\nWant: %v", g, w)
305305
}
306+
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
307+
if g, w := len(beginRequests), 0; g != w {
308+
t.Fatalf("num BeginTransaction requests mismatch\n Got: %v\nWant: %v", g, w)
309+
}
306310
rollbackRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.RollbackRequest{}))
307-
if g, w := len(rollbackRequests), 1; g != w {
311+
// There are no rollback requests sent to Spanner, as the transaction is never started.
312+
if g, w := len(rollbackRequests), 0; g != w {
308313
t.Fatalf("num rollback requests mismatch\n Got: %v\nWant: %v", g, w)
309314
}
310315
}

conn.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ type conn struct {
248248
// transactions on this connection. This default is ignored if the BeginTx function is
249249
// called with an isolation level other than sql.LevelDefault.
250250
isolationLevel sql.IsolationLevel
251+
// beginTransactionOption determines the default transactions start mode.
252+
beginTransactionOption spanner.BeginTransactionOption
251253

252254
// execOptions are applied to the next statement or transaction that is executed
253255
// on this connection. It can also be set by passing it in as an argument to
@@ -660,6 +662,7 @@ func (c *conn) ResetSession(_ context.Context) error {
660662
c.autoBatchDmlUpdateCountVerification = !c.connector.connectorConfig.DisableAutoBatchDmlUpdateCountVerification
661663
c.retryAborts = c.connector.retryAbortsInternally
662664
c.isolationLevel = c.connector.connectorConfig.IsolationLevel
665+
c.beginTransactionOption = c.connector.connectorConfig.BeginTransactionOption
663666
// TODO: Reset the following fields to the connector default
664667
c.autocommitDMLMode = Transactional
665668
c.readOnlyStaleness = spanner.TimestampBound{}
@@ -927,7 +930,9 @@ func (c *conn) withTempTransactionOptions(options *ReadWriteTransactionOptions)
927930
func (c *conn) getTransactionOptions() ReadWriteTransactionOptions {
928931
if c.tempTransactionOptions != nil {
929932
defer func() { c.tempTransactionOptions = nil }()
930-
return *c.tempTransactionOptions
933+
opts := *c.tempTransactionOptions
934+
opts.TransactionOptions.BeginTransactionOption = c.convertDefaultBeginTransactionOption(opts.TransactionOptions.BeginTransactionOption)
935+
return opts
931936
}
932937
// Clear the transaction tag that has been set on the connection after returning
933938
// from this function.
@@ -947,6 +952,9 @@ func (c *conn) getTransactionOptions() ReadWriteTransactionOptions {
947952
txOpts.TransactionOptions.IsolationLevel = level
948953
}
949954
}
955+
if txOpts.TransactionOptions.BeginTransactionOption == spanner.DefaultBeginTransaction {
956+
txOpts.TransactionOptions.BeginTransactionOption = c.convertDefaultBeginTransactionOption(c.beginTransactionOption)
957+
}
950958
return txOpts
951959
}
952960

@@ -957,9 +965,11 @@ func (c *conn) withTempReadOnlyTransactionOptions(options *ReadOnlyTransactionOp
957965
func (c *conn) getReadOnlyTransactionOptions() ReadOnlyTransactionOptions {
958966
if c.tempReadOnlyTransactionOptions != nil {
959967
defer func() { c.tempReadOnlyTransactionOptions = nil }()
960-
return *c.tempReadOnlyTransactionOptions
968+
opts := *c.tempReadOnlyTransactionOptions
969+
opts.BeginTransactionOption = c.convertDefaultBeginTransactionOption(opts.BeginTransactionOption)
970+
return opts
961971
}
962-
return ReadOnlyTransactionOptions{TimestampBound: c.readOnlyStaleness}
972+
return ReadOnlyTransactionOptions{TimestampBound: c.readOnlyStaleness, BeginTransactionOption: c.convertDefaultBeginTransactionOption(c.beginTransactionOption)}
963973
}
964974

965975
func (c *conn) withTempBatchReadOnlyTransactionOptions(options *BatchReadOnlyTransactionOptions) {
@@ -1034,7 +1044,7 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
10341044
ro = &bo.ReadOnlyTransaction
10351045
} else {
10361046
logger = c.logger.With("tx", "ro")
1037-
ro = c.client.ReadOnlyTransaction().WithTimestampBound(readOnlyTxOpts.TimestampBound)
1047+
ro = c.client.ReadOnlyTransaction().WithBeginTransactionOption(readOnlyTxOpts.BeginTransactionOption).WithTimestampBound(readOnlyTxOpts.TimestampBound)
10381048
}
10391049
c.tx = &readOnlyTransaction{
10401050
roTx: ro,
@@ -1080,6 +1090,16 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
10801090
return c.tx, nil
10811091
}
10821092

1093+
func (c *conn) convertDefaultBeginTransactionOption(opt spanner.BeginTransactionOption) spanner.BeginTransactionOption {
1094+
if opt == spanner.DefaultBeginTransaction {
1095+
if c.beginTransactionOption == spanner.DefaultBeginTransaction {
1096+
return spanner.InlinedBeginTransaction
1097+
}
1098+
return c.beginTransactionOption
1099+
}
1100+
return opt
1101+
}
1102+
10831103
func (c *conn) inTransaction() bool {
10841104
return c.tx != nil
10851105
}

conn_with_mockserver_test.go

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,69 @@ func TestBeginTx(t *testing.T) {
3939

4040
requests := drainRequestsFromServer(server.TestSpanner)
4141
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
42-
if g, w := len(beginRequests), 1; g != w {
42+
if g, w := len(beginRequests), 0; g != w {
4343
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
4444
}
45-
request := beginRequests[0].(*spannerpb.BeginTransactionRequest)
46-
if g, w := request.Options.GetIsolationLevel(), spannerpb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED; g != w {
45+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
46+
if g, w := len(executeRequests), 1; g != w {
47+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
48+
}
49+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
50+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
51+
t.Fatal("missing begin transaction on ExecuteSqlRequest")
52+
}
53+
if g, w := request.GetTransaction().GetBegin().GetIsolationLevel(), spannerpb.TransactionOptions_ISOLATION_LEVEL_UNSPECIFIED; g != w {
4754
t.Fatalf("begin isolation level mismatch\n Got: %v\nWant: %v", g, w)
4855
}
4956
}
5057

58+
func TestExplicitBeginTx(t *testing.T) {
59+
t.Parallel()
60+
61+
db, server, teardown := setupTestDBConnectionWithConnectorConfig(t, ConnectorConfig{
62+
Project: "p",
63+
Instance: "i",
64+
Database: "d",
65+
66+
BeginTransactionOption: spanner.ExplicitBeginTransaction,
67+
})
68+
defer teardown()
69+
ctx := context.Background()
70+
71+
for _, readOnly := range []bool{true, false} {
72+
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: readOnly})
73+
if err != nil {
74+
t.Fatal(err)
75+
}
76+
res, err := tx.QueryContext(ctx, testutil.SelectFooFromBar)
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
for res.Next() {
81+
}
82+
if err := res.Err(); err != nil {
83+
t.Fatal(err)
84+
}
85+
if err := tx.Rollback(); err != nil {
86+
t.Fatal(err)
87+
}
88+
89+
requests := drainRequestsFromServer(server.TestSpanner)
90+
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
91+
if g, w := len(beginRequests), 1; g != w {
92+
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
93+
}
94+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
95+
if g, w := len(executeRequests), 1; g != w {
96+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
97+
}
98+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
99+
if request.GetTransaction() == nil || request.GetTransaction().GetId() == nil {
100+
t.Fatal("missing transaction id on ExecuteSqlRequest")
101+
}
102+
}
103+
}
104+
51105
func TestBeginTxWithIsolationLevel(t *testing.T) {
52106
t.Parallel()
53107

@@ -76,12 +130,19 @@ func TestBeginTxWithIsolationLevel(t *testing.T) {
76130

77131
requests := drainRequestsFromServer(server.TestSpanner)
78132
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
79-
if g, w := len(beginRequests), 1; g != w {
133+
if g, w := len(beginRequests), 0; g != w {
80134
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
81135
}
82-
request := beginRequests[0].(*spannerpb.BeginTransactionRequest)
136+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
137+
if g, w := len(executeRequests), 1; g != w {
138+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
139+
}
140+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
141+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
142+
t.Fatalf("execute request does not have a begin transaction")
143+
}
83144
wantIsolationLevel, _ := toProtoIsolationLevel(originalLevel)
84-
if g, w := request.Options.GetIsolationLevel(), wantIsolationLevel; g != w {
145+
if g, w := request.GetTransaction().GetBegin().GetIsolationLevel(), wantIsolationLevel; g != w {
85146
t.Fatalf("begin isolation level mismatch\n Got: %v\nWant: %v", g, w)
86147
}
87148
}
@@ -162,12 +223,19 @@ func TestDefaultIsolationLevel(t *testing.T) {
162223

163224
requests := drainRequestsFromServer(server.TestSpanner)
164225
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
165-
if g, w := len(beginRequests), 1; g != w {
226+
if g, w := len(beginRequests), 0; g != w {
166227
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
167228
}
168-
request := beginRequests[0].(*spannerpb.BeginTransactionRequest)
229+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
230+
if g, w := len(executeRequests), 1; g != w {
231+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
232+
}
233+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
234+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
235+
t.Fatalf("ExecuteSqlRequest should have a Begin transaction")
236+
}
169237
wantIsolationLevel, _ := toProtoIsolationLevel(originalLevel)
170-
if g, w := request.Options.GetIsolationLevel(), wantIsolationLevel; g != w {
238+
if g, w := request.GetTransaction().GetBegin().GetIsolationLevel(), wantIsolationLevel; g != w {
171239
t.Fatalf("begin isolation level mismatch\n Got: %v\nWant: %v", g, w)
172240
}
173241
}

driver.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ type ConnectorConfig struct {
305305
// IsolationLevel is the default isolation level for read/write transactions.
306306
IsolationLevel sql.IsolationLevel
307307

308+
// BeginTransactionOption determines the default for how to begin transactions.
309+
// The Spanner database/sql driver uses spanner.InlinedBeginTransaction by default
310+
// for both read-only and read/write transactions.
311+
BeginTransactionOption spanner.BeginTransactionOption
312+
308313
// DecodeToNativeArrays determines whether arrays that have a Go native
309314
// type should be decoded to those types rather than the corresponding
310315
// spanner.NullTypeName type.
@@ -552,6 +557,11 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
552557
connectorConfig.IsolationLevel = val
553558
}
554559
}
560+
if strval, ok := connectorConfig.Params[strings.ToLower("BeginTransactionOption")]; ok {
561+
if val, err := parseBeginTransactionOption(strval); err == nil {
562+
connectorConfig.BeginTransactionOption = val
563+
}
564+
}
555565
if strval, ok := connectorConfig.Params[strings.ToLower("StatementCacheSize")]; ok {
556566
if val, err := strconv.Atoi(strval); err == nil {
557567
connectorConfig.StatementCacheSize = val
@@ -1038,7 +1048,8 @@ func clearTempReadWriteTransactionOptions(conn *sql.Conn) {
10381048
// ReadOnlyTransactionOptions can be used to create a read-only transaction
10391049
// on a Spanner connection.
10401050
type ReadOnlyTransactionOptions struct {
1041-
TimestampBound spanner.TimestampBound
1051+
TimestampBound spanner.TimestampBound
1052+
BeginTransactionOption spanner.BeginTransactionOption
10421053

10431054
close func()
10441055
}
@@ -1295,6 +1306,18 @@ func checkIsValidType(v driver.Value) bool {
12951306
return true
12961307
}
12971308

1309+
func parseBeginTransactionOption(val string) (spanner.BeginTransactionOption, error) {
1310+
switch strings.ToLower(val) {
1311+
case strings.ToLower("DefaultBeginTransaction"):
1312+
return spanner.DefaultBeginTransaction, nil
1313+
case strings.ToLower("InlinedBeginTransaction"):
1314+
return spanner.InlinedBeginTransaction, nil
1315+
case strings.ToLower("ExplicitBeginTransaction"):
1316+
return spanner.ExplicitBeginTransaction, nil
1317+
}
1318+
return spanner.DefaultBeginTransaction, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "invalid or unsupported BeginTransactionOption: %v", val))
1319+
}
1320+
12981321
func parseIsolationLevel(val string) (sql.IsolationLevel, error) {
12991322
switch strings.Replace(strings.ToLower(strings.TrimSpace(val)), " ", "_", 1) {
13001323
case "default":

0 commit comments

Comments
 (0)