diff --git a/google/cloud/spanner/options.h b/google/cloud/spanner/options.h index 8111acfc5df15..663b605ae17ce 100644 --- a/google/cloud/spanner/options.h +++ b/google/cloud/spanner/options.h @@ -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" @@ -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`. diff --git a/google/cloud/spanner/transaction.cc b/google/cloud/spanner/transaction.cc index 07d9c9edd464d..457b21c943c76 100644 --- a/google/cloud/spanner/transaction.cc +++ b/google/cloud/spanner/transaction.cc @@ -54,6 +54,22 @@ ProtoReadLockMode( } } +google::spanner::v1::TransactionOptions_IsolationLevel ProtoIsolationLevel( + absl::optional 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; @@ -62,13 +78,22 @@ 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 isolation_level) { google::spanner::v1::TransactionOptions opts; *opts.mutable_read_write() = std::move(rw_opts); auto const& current = internal::CurrentOptions(); if (current.get()) { opts.set_exclude_txn_from_change_streams(true); } + if (isolation_level && + *isolation_level != Transaction::IsolationLevel::kUnspecified) { + opts.set_isolation_level(ProtoIsolationLevel(isolation_level)); + } else if (current.has()) { + opts.set_isolation_level( + ProtoIsolationLevel(current.get())); + } + return opts; } @@ -103,6 +128,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_); } @@ -129,7 +161,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( std::move(selector), route_to_leader, @@ -138,7 +171,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( *txn.impl_, std::move(selector), route_to_leader, @@ -155,7 +189,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( std::move(selector), route_to_leader, diff --git a/google/cloud/spanner/transaction.h b/google/cloud/spanner/transaction.h index 0949d52229a7f..2af203cba6fb0 100644 --- a/google/cloud/spanner/transaction.h +++ b/google/cloud/spanner/transaction.h @@ -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. */ @@ -100,13 +130,23 @@ class Transaction { explicit ReadWriteOptions(ReadLockMode read_lock_mode); + explicit ReadWriteOptions(IsolationLevel isolation_level) + : isolation_level_(isolation_level) {} + // A tag used for collecting statistics about the transaction. ReadWriteOptions& WithTag(absl::optional 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 tag_; + IsolationLevel isolation_level_ = IsolationLevel::kUnspecified; }; /** diff --git a/google/cloud/spanner/transaction_test.cc b/google/cloud/spanner/transaction_test.cc index ac7a3b46483b5..c57f2fc7fcc55 100644 --- a/google/cloud/spanner/transaction_test.cc +++ b/google/cloud/spanner/transaction_test.cc @@ -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 namespace google { @@ -169,6 +171,36 @@ TEST(Transaction, MultiplexedPreviousTransactionId) { }); } +TEST(Transaction, IsolationLevelPrecedence) { + internal::OptionsSpan span(Options{}.set( + Transaction::IsolationLevel::kSerializable)); + + // Case 1: Per-call overrides client default + auto opts = Transaction::ReadWriteOptions().WithIsolationLevel( + Transaction::IsolationLevel::kRepeatableRead); + Transaction txn = MakeReadWriteTransaction(opts); + spanner_internal::Visit( + txn, [](spanner_internal::SessionHolder&, + StatusOr& 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& s, + spanner_internal::TransactionContext const&) { + EXPECT_EQ(s->begin().isolation_level(), + google::spanner::v1::TransactionOptions::SERIALIZABLE); + return 0; + }); +} + TEST(Transaction, ReadWriteOptionsWithTag) { auto opts = Transaction::ReadWriteOptions().WithTag("test-tag"); Transaction txn = MakeReadWriteTransaction(opts);