Skip to content

Commit 3959c49

Browse files
authored
feat(spanner): define IsolationLevel enum for Spanner transactions (#15853)
* feat(spanner): add support for snapshot isolation
1 parent 113d8fa commit 3959c49

4 files changed

Lines changed: 133 additions & 4 deletions

File tree

google/cloud/spanner/options.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "google/cloud/spanner/polling_policy.h"
4747
#include "google/cloud/spanner/request_priority.h"
4848
#include "google/cloud/spanner/retry_policy.h"
49+
#include "google/cloud/spanner/transaction.h"
4950
#include "google/cloud/spanner/version.h"
5051
#include "google/cloud/options.h"
5152
#include "absl/types/variant.h"
@@ -415,6 +416,15 @@ struct ExcludeTransactionFromChangeStreamsOption {
415416
using Type = bool;
416417
};
417418

419+
/**
420+
* Option for `google::cloud::Options` to set the transaction isolation level.
421+
*
422+
* @ingroup google-cloud-spanner-options
423+
*/
424+
struct TransactionIsolationLevelOption {
425+
using Type = spanner::Transaction::IsolationLevel;
426+
};
427+
418428
/**
419429
* Option for `google::cloud::Options` to return additional statistics
420430
* about the committed transaction in a `spanner::CommitResult`.

google/cloud/spanner/transaction.cc

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ ProtoReadLockMode(
5454
}
5555
}
5656

57+
google::spanner::v1::TransactionOptions_IsolationLevel ProtoIsolationLevel(
58+
absl::optional<Transaction::IsolationLevel> const& isolation_level) {
59+
if (!isolation_level) {
60+
return google::spanner::v1::TransactionOptions::ISOLATION_LEVEL_UNSPECIFIED;
61+
}
62+
switch (*isolation_level) {
63+
case Transaction::IsolationLevel::kSerializable:
64+
return google::spanner::v1::TransactionOptions::SERIALIZABLE;
65+
case Transaction::IsolationLevel::kRepeatableRead:
66+
return google::spanner::v1::TransactionOptions::REPEATABLE_READ;
67+
default:
68+
return google::spanner::v1::TransactionOptions::
69+
ISOLATION_LEVEL_UNSPECIFIED;
70+
}
71+
}
72+
5773
google::spanner::v1::TransactionOptions MakeOpts(
5874
google::spanner::v1::TransactionOptions_ReadOnly ro_opts) {
5975
google::spanner::v1::TransactionOptions opts;
@@ -62,13 +78,21 @@ google::spanner::v1::TransactionOptions MakeOpts(
6278
}
6379

6480
google::spanner::v1::TransactionOptions MakeOpts(
65-
google::spanner::v1::TransactionOptions_ReadWrite rw_opts) {
81+
google::spanner::v1::TransactionOptions_ReadWrite rw_opts,
82+
absl::optional<Transaction::IsolationLevel> isolation_level) {
6683
google::spanner::v1::TransactionOptions opts;
6784
*opts.mutable_read_write() = std::move(rw_opts);
6885
auto const& current = internal::CurrentOptions();
6986
if (current.get<ExcludeTransactionFromChangeStreamsOption>()) {
7087
opts.set_exclude_txn_from_change_streams(true);
7188
}
89+
if (isolation_level) {
90+
opts.set_isolation_level(ProtoIsolationLevel(isolation_level));
91+
} else if (current.has<TransactionIsolationLevelOption>()) {
92+
opts.set_isolation_level(
93+
ProtoIsolationLevel(current.get<TransactionIsolationLevelOption>()));
94+
}
95+
7296
return opts;
7397
}
7498

@@ -103,6 +127,13 @@ Transaction::ReadWriteOptions& Transaction::ReadWriteOptions::WithTag(
103127
return *this;
104128
}
105129

130+
Transaction::ReadWriteOptions&
131+
Transaction::ReadWriteOptions::WithIsolationLevel(
132+
IsolationLevel isolation_level) {
133+
isolation_level_ = isolation_level;
134+
return *this;
135+
}
136+
106137
Transaction::SingleUseOptions::SingleUseOptions(ReadOnlyOptions opts) {
107138
ro_opts_ = std::move(opts.ro_opts_);
108139
}
@@ -129,7 +160,8 @@ Transaction::Transaction(ReadOnlyOptions opts) {
129160

130161
Transaction::Transaction(ReadWriteOptions opts) {
131162
google::spanner::v1::TransactionSelector selector;
132-
*selector.mutable_begin() = MakeOpts(std::move(opts.rw_opts_));
163+
*selector.mutable_begin() =
164+
MakeOpts(std::move(opts.rw_opts_), opts.isolation_level_);
133165
auto const route_to_leader = true; // read-write
134166
impl_ = std::make_shared<spanner_internal::TransactionImpl>(
135167
std::move(selector), route_to_leader,
@@ -138,7 +170,8 @@ Transaction::Transaction(ReadWriteOptions opts) {
138170

139171
Transaction::Transaction(Transaction const& txn, ReadWriteOptions opts) {
140172
google::spanner::v1::TransactionSelector selector;
141-
*selector.mutable_begin() = MakeOpts(std::move(opts.rw_opts_));
173+
*selector.mutable_begin() =
174+
MakeOpts(std::move(opts.rw_opts_), opts.isolation_level_);
142175
auto const route_to_leader = true; // read-write
143176
impl_ = std::make_shared<spanner_internal::TransactionImpl>(
144177
*txn.impl_, std::move(selector), route_to_leader,
@@ -155,7 +188,8 @@ Transaction::Transaction(SingleUseOptions opts) {
155188

156189
Transaction::Transaction(ReadWriteOptions opts, SingleUseCommitTag) {
157190
google::spanner::v1::TransactionSelector selector;
158-
*selector.mutable_single_use() = MakeOpts(std::move(opts.rw_opts_));
191+
*selector.mutable_single_use() =
192+
MakeOpts(std::move(opts.rw_opts_), opts.isolation_level_);
159193
auto const route_to_leader = true; // write
160194
impl_ = std::make_shared<spanner_internal::TransactionImpl>(
161195
std::move(selector), route_to_leader,

google/cloud/spanner/transaction.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
5757
*/
5858
class Transaction {
5959
public:
60+
/**
61+
* Defines the isolation level for a transaction.
62+
*
63+
* This determines how concurrent transactions interact with each other and
64+
* what consistency guarantees are provided for read and write operations.
65+
*
66+
* @note This setting only applies to read-write transactions.
67+
*
68+
* See the `v1::TransactionOptions` proto for more details.
69+
*
70+
* @see https://docs.cloud.google.com/spanner/docs/isolation-levels
71+
*/
72+
enum class IsolationLevel {
73+
/// The isolation level is not specified, using the backend default.
74+
kUnspecified,
75+
/**
76+
* All transactions appear as if they executed in a serial order.
77+
* This is the default isolation level for read-write transactions.
78+
*/
79+
kSerializable,
80+
/**
81+
* All reads performed during the transaction observe a consistent snapshot
82+
* of the database. The transaction is only successfully committed in the
83+
* absence of conflicts between its updates and any concurrent updates
84+
* that have occurred since that snapshot. Consequently, in contrast to
85+
* `kSerializable` transactions, only write-write conflicts are detected in
86+
* snapshot transactions.
87+
*/
88+
kRepeatableRead,
89+
};
90+
6091
/**
6192
* Options for ReadOnly transactions.
6293
*/
@@ -103,10 +134,17 @@ class Transaction {
103134
// A tag used for collecting statistics about the transaction.
104135
ReadWriteOptions& WithTag(absl::optional<std::string> tag);
105136

137+
// Sets the isolation level for the transaction. This controls how the
138+
// transaction interacts with other concurrent transactions, primarily
139+
// regarding data consistency for reads and writes.
140+
// See `IsolationLevel` enum for possible values.
141+
ReadWriteOptions& WithIsolationLevel(IsolationLevel isolation_level);
142+
106143
private:
107144
friend Transaction;
108145
google::spanner::v1::TransactionOptions_ReadWrite rw_opts_;
109146
absl::optional<std::string> tag_;
147+
absl::optional<IsolationLevel> isolation_level_;
110148
};
111149

112150
/**

google/cloud/spanner/transaction_test.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
#include "google/cloud/spanner/transaction.h"
1616
#include "google/cloud/spanner/internal/session.h"
17+
#include "google/cloud/spanner/options.h"
18+
#include "google/cloud/options.h"
1719
#include <gmock/gmock.h>
1820

1921
namespace google {
@@ -169,6 +171,51 @@ TEST(Transaction, MultiplexedPreviousTransactionId) {
169171
});
170172
}
171173

174+
TEST(Transaction, IsolationLevelPrecedence) {
175+
internal::OptionsSpan span(Options{}.set<TransactionIsolationLevelOption>(
176+
Transaction::IsolationLevel::kSerializable));
177+
178+
// Case 1: Per-call overrides default options
179+
auto opts = Transaction::ReadWriteOptions().WithIsolationLevel(
180+
Transaction::IsolationLevel::kRepeatableRead);
181+
Transaction txn = MakeReadWriteTransaction(opts);
182+
spanner_internal::Visit(
183+
txn, [](spanner_internal::SessionHolder&,
184+
StatusOr<google::spanner::v1::TransactionSelector>& s,
185+
spanner_internal::TransactionContext const&) {
186+
EXPECT_EQ(s->begin().isolation_level(),
187+
google::spanner::v1::TransactionOptions::REPEATABLE_READ);
188+
return 0;
189+
});
190+
191+
// Case 2: Fallback to client default
192+
auto opts_default = Transaction::ReadWriteOptions();
193+
Transaction txn_default = MakeReadWriteTransaction(opts_default);
194+
spanner_internal::Visit(
195+
txn_default, [](spanner_internal::SessionHolder&,
196+
StatusOr<google::spanner::v1::TransactionSelector>& s,
197+
spanner_internal::TransactionContext const&) {
198+
EXPECT_EQ(s->begin().isolation_level(),
199+
google::spanner::v1::TransactionOptions::SERIALIZABLE);
200+
return 0;
201+
});
202+
}
203+
204+
TEST(Transaction, IsolationLevelNotSpecified) {
205+
// Case: Isolation not specified in transaction level or client level
206+
auto opts = Transaction::ReadWriteOptions();
207+
Transaction txn = MakeReadWriteTransaction(opts);
208+
spanner_internal::Visit(
209+
txn, [](spanner_internal::SessionHolder&,
210+
StatusOr<google::spanner::v1::TransactionSelector>& s,
211+
spanner_internal::TransactionContext const&) {
212+
EXPECT_EQ(s->begin().isolation_level(),
213+
google::spanner::v1::TransactionOptions::
214+
ISOLATION_LEVEL_UNSPECIFIED);
215+
return 0;
216+
});
217+
}
218+
172219
TEST(Transaction, ReadWriteOptionsWithTag) {
173220
auto opts = Transaction::ReadWriteOptions().WithTag("test-tag");
174221
Transaction txn = MakeReadWriteTransaction(opts);

0 commit comments

Comments
 (0)