Skip to content

Commit a552d5e

Browse files
authored
feat: support read_lock_mode for r/w transactions (#505)
Adds support for setting the read_lock_mode for read/write transactions. The read_lock_mode can be set in any of the following ways: 1. In the connection string using the read_lock_mode connection property. This then becomes the default for all transactions on the connection. 2. Using the SetReadLockMode(..) function on the SpannerConn interface. 3. As part of the TransactionOptions or ExecOptions for a single transaction. 4. Using a `SET [LOCAL] read_lock_mode = {'OPTIMISTIC' | 'PESSIMISTIC'}` SQL statement.
1 parent 02ae6e8 commit a552d5e

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

conn.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,38 @@ type SpannerConn interface {
126126
// transactions on this connection.
127127
SetIsolationLevel(level sql.IsolationLevel) error
128128

129+
// ReadLockMode returns the current read lock mode that is used for read/write
130+
// transactions on this connection.
131+
ReadLockMode() spannerpb.TransactionOptions_ReadWrite_ReadLockMode
132+
// SetReadLockMode sets the read lock mode to use for read/write transactions
133+
// on this connection.
134+
//
135+
// The read lock mode option controls the locking behavior for read operations and queries within a
136+
// read-write transaction. It works in conjunction with the transaction's isolation level.
137+
//
138+
// PESSIMISTIC: Read locks are acquired immediately on read. This mode only applies to SERIALIZABLE
139+
// isolation. This mode prevents concurrent modifications by locking data throughout the transaction.
140+
// This reduces commit-time aborts due to conflicts but can increase how long transactions wait for
141+
// locks and the overall contention.
142+
//
143+
// OPTIMISTIC: Locks for reads within the transaction are not acquired on read. Instead, the locks
144+
// are acquired on commit to validate that read/queried data has not changed since the transaction
145+
// started. If a conflict is detected, the transaction will fail. This mode only applies to SERIALIZABLE
146+
// isolation. This mode defers locking until commit, which can reduce contention and improve throughput.
147+
// However, be aware that this increases the risk of transaction aborts if there's significant write
148+
// competition on the same data.
149+
//
150+
// READ_LOCK_MODE_UNSPECIFIED: This is the default if no mode is set. The locking behavior depends on
151+
// the isolation level:
152+
//
153+
// REPEATABLE_READ isolation: Locking semantics default to OPTIMISTIC. However, validation checks at
154+
// commit are only performed for queries using SELECT FOR UPDATE, statements with LOCK_SCANNED_RANGES
155+
// hints, and DML statements. Note: It is an error to explicitly set ReadLockMode when the isolation
156+
// level is REPEATABLE_READ.
157+
//
158+
// For all other isolation levels: If the read lock mode is not set, it defaults to PESSIMISTIC locking.
159+
SetReadLockMode(mode spannerpb.TransactionOptions_ReadWrite_ReadLockMode) error
160+
129161
// TransactionTag returns the transaction tag that will be applied to the next
130162
// read/write transaction on this connection. The transaction tag that is set
131163
// on the connection is cleared when a read/write transaction is started.
@@ -365,6 +397,14 @@ func (c *conn) SetIsolationLevel(level sql.IsolationLevel) error {
365397
return propertyIsolationLevel.SetValue(c.state, level, connectionstate.ContextUser)
366398
}
367399

400+
func (c *conn) ReadLockMode() spannerpb.TransactionOptions_ReadWrite_ReadLockMode {
401+
return propertyReadLockMode.GetValueOrDefault(c.state)
402+
}
403+
404+
func (c *conn) SetReadLockMode(mode spannerpb.TransactionOptions_ReadWrite_ReadLockMode) error {
405+
return propertyReadLockMode.SetValue(c.state, mode, connectionstate.ContextUser)
406+
}
407+
368408
func (c *conn) MaxCommitDelay() time.Duration {
369409
return propertyMaxCommitDelay.GetValueOrDefault(c.state)
370410
}
@@ -918,6 +958,7 @@ func (c *conn) options(reset bool) *ExecOptions {
918958
ExcludeTxnFromChangeStreams: c.ExcludeTxnFromChangeStreams(),
919959
TransactionTag: c.TransactionTag(),
920960
IsolationLevel: toProtoIsolationLevelOrDefault(c.IsolationLevel()),
961+
ReadLockMode: c.ReadLockMode(),
921962
CommitOptions: spanner.CommitOptions{
922963
MaxCommitDelay: c.maxCommitDelayPointer(),
923964
},

conn_with_mockserver_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,184 @@ func TestIsolationLevelAutoCommit(t *testing.T) {
314314
}
315315
}
316316

317+
func TestDefaultReadLockMode(t *testing.T) {
318+
t.Parallel()
319+
320+
for mode, name := range spannerpb.TransactionOptions_ReadWrite_ReadLockMode_name {
321+
readLockMode := spannerpb.TransactionOptions_ReadWrite_ReadLockMode(mode)
322+
db, server, teardown := setupTestDBConnectionWithParams(t, fmt.Sprintf("read_lock_mode=%v", name))
323+
defer teardown()
324+
ctx := context.Background()
325+
326+
c, err := db.Conn(ctx)
327+
if err != nil {
328+
t.Fatal(err)
329+
}
330+
if err := c.Raw(func(driverConn interface{}) error {
331+
spannerConn, ok := driverConn.(*conn)
332+
if !ok {
333+
return fmt.Errorf("expected spanner conn, got %T", driverConn)
334+
}
335+
if spannerConn.ReadLockMode() != readLockMode {
336+
return fmt.Errorf("expected read lock mode %v, got %v", readLockMode, spannerConn.ReadLockMode())
337+
}
338+
return nil
339+
}); err != nil {
340+
t.Fatal(err)
341+
}
342+
343+
tx, _ := db.BeginTx(ctx, &sql.TxOptions{})
344+
_, _ = tx.ExecContext(ctx, testutil.UpdateBarSetFoo)
345+
_ = tx.Rollback()
346+
347+
requests := drainRequestsFromServer(server.TestSpanner)
348+
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
349+
if g, w := len(beginRequests), 0; g != w {
350+
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
351+
}
352+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
353+
if g, w := len(executeRequests), 1; g != w {
354+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
355+
}
356+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
357+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
358+
t.Fatalf("ExecuteSqlRequest should have a Begin transaction")
359+
}
360+
if g, w := request.GetTransaction().GetBegin().GetReadWrite().GetReadLockMode(), readLockMode; g != w {
361+
t.Fatalf("begin read lock mode mismatch\n Got: %v\nWant: %v", g, w)
362+
}
363+
}
364+
}
365+
366+
func TestSetReadLockMode(t *testing.T) {
367+
t.Parallel()
368+
369+
db, _, teardown := setupTestDBConnection(t)
370+
defer teardown()
371+
ctx := context.Background()
372+
373+
// Repeat twice to ensure that the state is reset after closing the connection.
374+
for i := 0; i < 2; i++ {
375+
c, err := db.Conn(ctx)
376+
if err != nil {
377+
t.Fatal(err)
378+
}
379+
var readLockMode spannerpb.TransactionOptions_ReadWrite_ReadLockMode
380+
_ = c.Raw(func(driverConn interface{}) error {
381+
readLockMode = driverConn.(*conn).ReadLockMode()
382+
return nil
383+
})
384+
if g, w := readLockMode, spannerpb.TransactionOptions_ReadWrite_READ_LOCK_MODE_UNSPECIFIED; g != w {
385+
t.Fatalf("read lock mode mismatch\n Got: %v\nWant: %v", g, w)
386+
}
387+
_ = c.Raw(func(driverConn interface{}) error {
388+
return driverConn.(SpannerConn).SetReadLockMode(spannerpb.TransactionOptions_ReadWrite_OPTIMISTIC)
389+
})
390+
_ = c.Raw(func(driverConn interface{}) error {
391+
readLockMode = driverConn.(SpannerConn).ReadLockMode()
392+
return nil
393+
})
394+
if g, w := readLockMode, spannerpb.TransactionOptions_ReadWrite_OPTIMISTIC; g != w {
395+
t.Fatalf("read lock mode mismatch\n Got: %v\nWant: %v", g, w)
396+
}
397+
_ = c.Close()
398+
}
399+
}
400+
401+
func TestReadLockModeAutoCommit(t *testing.T) {
402+
t.Parallel()
403+
404+
db, server, teardown := setupTestDBConnection(t)
405+
defer teardown()
406+
ctx := context.Background()
407+
408+
for mode := range spannerpb.TransactionOptions_ReadWrite_ReadLockMode_name {
409+
readLockMode := spannerpb.TransactionOptions_ReadWrite_ReadLockMode(mode)
410+
_, _ = db.ExecContext(ctx, testutil.UpdateBarSetFoo, ExecOptions{TransactionOptions: spanner.TransactionOptions{
411+
ReadLockMode: readLockMode,
412+
}})
413+
414+
requests := drainRequestsFromServer(server.TestSpanner)
415+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
416+
if g, w := len(executeRequests), 1; g != w {
417+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
418+
}
419+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
420+
if g, w := request.Transaction.GetBegin().GetReadWrite().GetReadLockMode(), readLockMode; g != w {
421+
t.Fatalf("begin read lock mode mismatch\n Got: %v\nWant: %v", g, w)
422+
}
423+
}
424+
}
425+
426+
func TestSetLocalReadLockMode(t *testing.T) {
427+
t.Parallel()
428+
429+
db, server, teardown := setupTestDBConnection(t)
430+
// Make sure we only have one connection in the pool.
431+
db.SetMaxOpenConns(1)
432+
defer teardown()
433+
ctx := context.Background()
434+
435+
tx, err := db.BeginTx(ctx, nil)
436+
if err != nil {
437+
t.Fatal(err)
438+
}
439+
if _, err := tx.ExecContext(ctx, "set local read_lock_mode='optimistic'"); err != nil {
440+
t.Fatal(err)
441+
}
442+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
443+
t.Fatal(err)
444+
}
445+
if err := tx.Commit(); err != nil {
446+
t.Fatal(err)
447+
}
448+
449+
requests := drainRequestsFromServer(server.TestSpanner)
450+
beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
451+
if g, w := len(beginRequests), 0; g != w {
452+
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
453+
}
454+
executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
455+
if g, w := len(executeRequests), 1; g != w {
456+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
457+
}
458+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
459+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
460+
t.Fatalf("ExecuteSqlRequest should have a Begin transaction")
461+
}
462+
if g, w := request.GetTransaction().GetBegin().GetReadWrite().GetReadLockMode(), spannerpb.TransactionOptions_ReadWrite_OPTIMISTIC; g != w {
463+
t.Fatalf("begin read lock mode mismatch\n Got: %v\nWant: %v", g, w)
464+
}
465+
466+
// Execute another transaction without a specific read lock mode. This should then use the default.
467+
tx, err = db.BeginTx(ctx, nil)
468+
if err != nil {
469+
t.Fatal(err)
470+
}
471+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
472+
t.Fatal(err)
473+
}
474+
if err := tx.Commit(); err != nil {
475+
t.Fatal(err)
476+
}
477+
requests = drainRequestsFromServer(server.TestSpanner)
478+
beginRequests = requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{}))
479+
if g, w := len(beginRequests), 0; g != w {
480+
t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w)
481+
}
482+
executeRequests = requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
483+
if g, w := len(executeRequests), 1; g != w {
484+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
485+
}
486+
request = executeRequests[0].(*spannerpb.ExecuteSqlRequest)
487+
if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil {
488+
t.Fatalf("ExecuteSqlRequest should have a Begin transaction")
489+
}
490+
if g, w := request.GetTransaction().GetBegin().GetReadWrite().GetReadLockMode(), spannerpb.TransactionOptions_ReadWrite_READ_LOCK_MODE_UNSPECIFIED; g != w {
491+
t.Fatalf("begin read lock mode mismatch\n Got: %v\nWant: %v", g, w)
492+
}
493+
}
494+
317495
func TestCreateDatabase(t *testing.T) {
318496
t.Parallel()
319497

connection_properties.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package spannerdriver
1717
import (
1818
"database/sql"
1919
"fmt"
20+
"strings"
2021
"time"
2122

2223
"cloud.google.com/go/spanner"
@@ -73,6 +74,34 @@ var propertyIsolationLevel = createConnectionProperty(
7374
return parseIsolationLevel(value)
7475
},
7576
)
77+
var propertyReadLockMode = createConnectionProperty(
78+
"read_lock_mode",
79+
"This option controls the locking behavior for read operations and queries within a read/write transaction. "+
80+
"It works in conjunction with the transaction's isolation level.\n\n"+
81+
"PESSIMISTIC: Read locks are acquired immediately on read. This mode only applies to SERIALIZABLE isolation. "+
82+
"This mode prevents concurrent modifications by locking data throughout the transaction. This reduces commit-time "+
83+
"aborts due to conflicts, but can increase how long transactions wait for locks and the overall contention.\n\n"+
84+
"OPTIMISTIC: Locks for reads within the transaction are not acquired on read. Instead, the locks are acquired on "+
85+
"commit to validate that read/queried data has not changed since the transaction started. If a conflict is "+
86+
"detected, the transaction will fail. This mode only applies to SERIALIZABLE isolation. This mode defers locking "+
87+
"until commit, which can reduce contention and improve throughput. However, be aware that this increases the "+
88+
"risk of transaction aborts if there's significant write competition on the same data.\n\n"+
89+
"READ_LOCK_MODE_UNSPECIFIED: This is the default if no mode is set. The locking behavior depends on the isolation level:\n\n"+
90+
"REPEATABLE_READ: Locking semantics default to OPTIMISTIC. However, validation checks at commit are only "+
91+
"performed for queries using SELECT FOR UPDATE, statements with {@code LOCK_SCANNED_RANGES} hints, and DML statements.\n\n"+
92+
"For all other isolation levels: If the read lock mode is not set, it defaults to PESSIMISTIC locking.",
93+
spannerpb.TransactionOptions_ReadWrite_READ_LOCK_MODE_UNSPECIFIED,
94+
false,
95+
nil,
96+
connectionstate.ContextUser,
97+
func(value string) (spannerpb.TransactionOptions_ReadWrite_ReadLockMode, error) {
98+
name := strings.ToUpper(value)
99+
if _, ok := spannerpb.TransactionOptions_ReadWrite_ReadLockMode_value[name]; ok {
100+
return spannerpb.TransactionOptions_ReadWrite_ReadLockMode(spannerpb.TransactionOptions_ReadWrite_ReadLockMode_value[name]), nil
101+
}
102+
return spannerpb.TransactionOptions_ReadWrite_READ_LOCK_MODE_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "unknown read lock mode: %v", value)
103+
},
104+
)
76105
var propertyBeginTransactionOption = createConnectionProperty(
77106
"begin_transaction_option",
78107
"BeginTransactionOption determines the default for how to begin transactions. "+

0 commit comments

Comments
 (0)