Skip to content
10 changes: 10 additions & 0 deletions google/cloud/spanner/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "google/cloud/spanner/polling_policy.h"
#include "google/cloud/spanner/request_priority.h"
#include "google/cloud/spanner/retry_policy.h"
#include "google/cloud/spanner/transaction.h"
#include "google/cloud/spanner/version.h"
#include "google/cloud/options.h"
#include "absl/types/variant.h"
Expand Down Expand Up @@ -415,6 +416,15 @@ struct ExcludeTransactionFromChangeStreamsOption {
using Type = bool;
};

/**
* Option for `google::cloud::Options` to set the transaction isolation level.
*
* @ingroup google-cloud-spanner-options
*/
struct TransactionIsolationLevelOption {
using Type = spanner::Transaction::IsolationLevel;
};

/**
* Option for `google::cloud::Options` to return additional statistics
* about the committed transaction in a `spanner::CommitResult`.
Expand Down
42 changes: 38 additions & 4 deletions google/cloud/spanner/transaction.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ ProtoReadLockMode(
}
}

google::spanner::v1::TransactionOptions_IsolationLevel ProtoIsolationLevel(
absl::optional<Transaction::IsolationLevel> const& isolation_level) {
if (!isolation_level) {
return google::spanner::v1::TransactionOptions::ISOLATION_LEVEL_UNSPECIFIED;
}
switch (*isolation_level) {
case Transaction::IsolationLevel::kSerializable:
return google::spanner::v1::TransactionOptions::SERIALIZABLE;
case Transaction::IsolationLevel::kRepeatableRead:
return google::spanner::v1::TransactionOptions::REPEATABLE_READ;
default:
return google::spanner::v1::TransactionOptions::
ISOLATION_LEVEL_UNSPECIFIED;
}
}

google::spanner::v1::TransactionOptions MakeOpts(
google::spanner::v1::TransactionOptions_ReadOnly ro_opts) {
google::spanner::v1::TransactionOptions opts;
Expand All @@ -62,13 +78,21 @@ google::spanner::v1::TransactionOptions MakeOpts(
}

google::spanner::v1::TransactionOptions MakeOpts(
google::spanner::v1::TransactionOptions_ReadWrite rw_opts) {
google::spanner::v1::TransactionOptions_ReadWrite rw_opts,
absl::optional<Transaction::IsolationLevel> isolation_level) {
google::spanner::v1::TransactionOptions opts;
*opts.mutable_read_write() = std::move(rw_opts);
auto const& current = internal::CurrentOptions();
if (current.get<ExcludeTransactionFromChangeStreamsOption>()) {
opts.set_exclude_txn_from_change_streams(true);
}
if (isolation_level) {
opts.set_isolation_level(ProtoIsolationLevel(isolation_level));
} else if (current.has<TransactionIsolationLevelOption>()) {
opts.set_isolation_level(
ProtoIsolationLevel(current.get<TransactionIsolationLevelOption>()));
}

return opts;
}

Expand Down Expand Up @@ -103,6 +127,13 @@ Transaction::ReadWriteOptions& Transaction::ReadWriteOptions::WithTag(
return *this;
}

Transaction::ReadWriteOptions&
Transaction::ReadWriteOptions::WithIsolationLevel(
IsolationLevel isolation_level) {
isolation_level_ = isolation_level;
return *this;
}

Transaction::SingleUseOptions::SingleUseOptions(ReadOnlyOptions opts) {
ro_opts_ = std::move(opts.ro_opts_);
}
Expand All @@ -129,7 +160,8 @@ Transaction::Transaction(ReadOnlyOptions opts) {

Transaction::Transaction(ReadWriteOptions opts) {
google::spanner::v1::TransactionSelector selector;
*selector.mutable_begin() = MakeOpts(std::move(opts.rw_opts_));
*selector.mutable_begin() =
MakeOpts(std::move(opts.rw_opts_), opts.isolation_level_);
auto const route_to_leader = true; // read-write
impl_ = std::make_shared<spanner_internal::TransactionImpl>(
std::move(selector), route_to_leader,
Expand All @@ -138,7 +170,8 @@ Transaction::Transaction(ReadWriteOptions opts) {

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

Transaction::Transaction(ReadWriteOptions opts, SingleUseCommitTag) {
google::spanner::v1::TransactionSelector selector;
*selector.mutable_single_use() = MakeOpts(std::move(opts.rw_opts_));
*selector.mutable_single_use() =
MakeOpts(std::move(opts.rw_opts_), opts.isolation_level_);
auto const route_to_leader = true; // write
impl_ = std::make_shared<spanner_internal::TransactionImpl>(
std::move(selector), route_to_leader,
Expand Down
38 changes: 38 additions & 0 deletions google/cloud/spanner/transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
*/
class Transaction {
public:
/**
* Defines the isolation level for a transaction.
*
* This determines how concurrent transactions interact with each other and
* what consistency guarantees are provided for read and write operations.
* @note This setting only applies to read-write transactions.
*
* See the `v1::TransactionOptions` proto for more details.
*
* @see https://docs.cloud.google.com/spanner/docs/isolation-levels
*/
enum class IsolationLevel {
/// The isolation level is not specified, using the backend default.
kUnspecified,
/**
* All transactions appear as if they executed in a serial order.
* This is the default isolation level for read-write transactions.
*/
kSerializable,
/**
* All reads performed during the transaction observe a consistent snapshot
* of the database. The transaction is only successfully committed in the
* absence of conflicts between its updates and any concurrent updates
* that have occurred since that snapshot. Consequently, in contrast to
* `kSerializable` transactions, only write-write conflicts are detected in
* snapshot transactions.
*/
kRepeatableRead,
};

/**
* Options for ReadOnly transactions.
*/
Expand Down Expand Up @@ -100,13 +130,21 @@ class Transaction {

explicit ReadWriteOptions(ReadLockMode read_lock_mode);


// A tag used for collecting statistics about the transaction.
ReadWriteOptions& WithTag(absl::optional<std::string> tag);

// Sets the isolation level for the transaction. This controls how the
// transaction interacts with other concurrent transactions, primarily
// regarding data consistency for reads and writes.
// See `IsolationLevel` enum for possible values.
ReadWriteOptions& WithIsolationLevel(IsolationLevel isolation_level);

private:
friend Transaction;
google::spanner::v1::TransactionOptions_ReadWrite rw_opts_;
absl::optional<std::string> tag_;
absl::optional<IsolationLevel> isolation_level_;
};

/**
Expand Down
46 changes: 46 additions & 0 deletions google/cloud/spanner/transaction_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#include "google/cloud/spanner/transaction.h"
#include "google/cloud/spanner/internal/session.h"
#include "google/cloud/spanner/options.h"
#include "google/cloud/options.h"
#include <gmock/gmock.h>

namespace google {
Expand Down Expand Up @@ -169,6 +171,50 @@ TEST(Transaction, MultiplexedPreviousTransactionId) {
});
}

TEST(Transaction, IsolationLevelPrecedence) {
internal::OptionsSpan span(Options{}.set<TransactionIsolationLevelOption>(
Transaction::IsolationLevel::kSerializable));

// Case 1: Per-call overrides default options
auto opts = Transaction::ReadWriteOptions().WithIsolationLevel(
Transaction::IsolationLevel::kRepeatableRead);
Transaction txn = MakeReadWriteTransaction(opts);
spanner_internal::Visit(
txn, [](spanner_internal::SessionHolder&,
StatusOr<google::spanner::v1::TransactionSelector>& s,
spanner_internal::TransactionContext const&) {
EXPECT_EQ(s->begin().isolation_level(),
google::spanner::v1::TransactionOptions::REPEATABLE_READ);
return 0;
});

// Case 2: Fallback to client default
auto opts_default = Transaction::ReadWriteOptions();
Transaction txn_default = MakeReadWriteTransaction(opts_default);
spanner_internal::Visit(
txn_default, [](spanner_internal::SessionHolder&,
StatusOr<google::spanner::v1::TransactionSelector>& s,
spanner_internal::TransactionContext const&) {
EXPECT_EQ(s->begin().isolation_level(),
google::spanner::v1::TransactionOptions::SERIALIZABLE);
return 0;
});
}

TEST(Transaction, IsolationLevelNotSpecified) {
// Case: Isolation not specified in transaction level or client level
auto opts = Transaction::ReadWriteOptions();
Transaction txn = MakeReadWriteTransaction(opts);
spanner_internal::Visit(
txn, [](spanner_internal::SessionHolder&,
StatusOr<google::spanner::v1::TransactionSelector>& s,
spanner_internal::TransactionContext const&) {
EXPECT_EQ(s->begin().isolation_level(),
google::spanner::v1::TransactionOptions::ISOLATION_LEVEL_UNSPECIFIED);
return 0;
});
}

TEST(Transaction, ReadWriteOptionsWithTag) {
auto opts = Transaction::ReadWriteOptions().WithTag("test-tag");
Transaction txn = MakeReadWriteTransaction(opts);
Expand Down