Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions google/cloud/spanner/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ add_library(
json.h
keys.cc
keys.h
lock_hint.h
mutations.cc
mutations.h
numeric.cc
Expand Down
18 changes: 13 additions & 5 deletions google/cloud/spanner/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ RowStream Client::Read(std::string table, KeySet keys,
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
internal::OptionsSpan span(std::move(opts));
auto lock_hint = ExtractOpt<LockHintOption>(opts);
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
Transaction::ReadOnlyOptions()),
std::move(table), std::move(keys), std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(lock_hint)});
}

RowStream Client::Read(Transaction::SingleUseOptions transaction_options,
Expand All @@ -67,30 +69,35 @@ RowStream Client::Read(Transaction::SingleUseOptions transaction_options,
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
internal::OptionsSpan span(std::move(opts));
auto lock_hint = ExtractOpt<LockHintOption>(opts);
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
std::move(transaction_options)),
std::move(table), std::move(keys), std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(lock_hint)});
}

RowStream Client::Read(Transaction transaction, std::string table, KeySet keys,
std::vector<std::string> columns, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto lock_hint = ExtractOpt<LockHintOption>(opts);
internal::OptionsSpan span(std::move(opts));
return conn_->Read({std::move(transaction), std::move(table), std::move(keys),
std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(lock_hint)});
}

RowStream Client::Read(ReadPartition const& read_partition, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto lock_hint = ExtractOpt<LockHintOption>(opts);
internal::OptionsSpan span(std::move(opts));
return conn_->Read(spanner_internal::MakeReadParams(
read_partition, std::move(directed_read_option)));
read_partition, std::move(directed_read_option), std::move(lock_hint)));
}

StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
Expand All @@ -100,7 +107,8 @@ StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
return conn_->PartitionRead(
{{std::move(transaction), std::move(table), std::move(keys),
std::move(columns), ToReadOptions(internal::CurrentOptions()),
absl::nullopt, false, DirectedReadOption::Type{}},
absl::nullopt, false, DirectedReadOption::Type{},
LockHint::kLockHintUnspecified},
ToPartitionOptions(internal::CurrentOptions())});
}

Expand Down
9 changes: 6 additions & 3 deletions google/cloud/spanner/client_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ TEST(ClientTest, CommitMutatorSuccess) {

auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};
Connection::CommitParams actual_commit_params{txn, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
Expand Down Expand Up @@ -453,7 +454,8 @@ TEST(ClientTest, CommitMutatorSuccess) {
TEST(ClientTest, CommitMutatorRollback) {
auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
auto constexpr kText = R"pb(
Expand Down Expand Up @@ -495,7 +497,8 @@ TEST(ClientTest, CommitMutatorRollback) {
TEST(ClientTest, CommitMutatorRollbackError) {
auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
auto constexpr kText = R"pb(
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/spanner/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "google/cloud/spanner/commit_options.h"
#include "google/cloud/spanner/commit_result.h"
#include "google/cloud/spanner/keys.h"
#include "google/cloud/spanner/lock_hint.h"
#include "google/cloud/spanner/mutations.h"
#include "google/cloud/spanner/options.h"
#include "google/cloud/spanner/partition_options.h"
Expand Down Expand Up @@ -81,6 +82,7 @@ class Connection {
absl::optional<std::string> partition_token;
bool partition_data_boost = false; // when partition_token
DirectedReadOption::Type directed_read_option;
LockHint lock_hint;
};

/// Wrap the arguments to `PartitionRead()`.
Expand Down
1 change: 1 addition & 0 deletions google/cloud/spanner/google_cloud_cpp_spanner.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ google_cloud_cpp_spanner_hdrs = [
"interval.h",
"json.h",
"keys.h",
"lock_hint.h",
"mutations.h",
"numeric.h",
"oid.h",
Expand Down
16 changes: 16 additions & 0 deletions google/cloud/spanner/internal/connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ google::spanner::v1::RequestOptions_Priority ProtoRequestPriority(
return google::spanner::v1::RequestOptions::PRIORITY_UNSPECIFIED;
}

google::spanner::v1::ReadRequest_LockHint ProtoLockHint(
absl::optional<spanner::LockHint> const& order_by) {
if (order_by) {
switch (*order_by) {
case spanner::LockHint::kLockHintUnspecified:
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_UNSPECIFIED;
case spanner::LockHint::kLockHintShared:
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_SHARED;
case spanner::LockHint::kLockHintExclusive:
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_EXCLUSIVE;
}
}
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_UNSPECIFIED;
}

// Converts a `google::protobuf::Timestamp` to a `spanner::Timestamp`, but
// substitutes the maximal value for any conversion error. This is needed
// when, for example, a response commit_timestamp is out of range but the
Expand Down Expand Up @@ -548,6 +563,7 @@ spanner::RowStream ConnectionImpl::ReadImpl(
*request->mutable_transaction() = *s;
request->set_table(std::move(params.table));
request->set_index(std::move(params.read_options.index_name));
request->set_lock_hint(ProtoLockHint(params.lock_hint));
for (auto&& column : params.columns) {
request->add_columns(std::move(column));
}
Expand Down
83 changes: 83 additions & 0 deletions google/cloud/spanner/internal/connection_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ MATCHER_P(HasReplicaType, type, "has replica type") {
return arg.type() == type;
}

MATCHER_P(HasLockHint, lock_hint, "has lock_hint") {
return arg.lock_hint() == lock_hint;
}

// Ideally this would be a matcher, but matcher args are `const` and `RowStream`
// only has non-const methods.
bool ContainsNoRows(spanner::RowStream& rows) {
Expand Down Expand Up @@ -4091,6 +4095,85 @@ TEST(ConnectionImplTest, RollbackSessionNotFound) {
EXPECT_THAT(txn, HasBadSession());
}

TEST(ConnectionImplTest, ReadRequestLockHintParameterUnspecified) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("project", "instance", "database");
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
EXPECT_CALL(*mock,
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
.WillOnce(Return(make_ready_future(Status{})));
Sequence s;
EXPECT_CALL(
*mock,
StreamingRead(
_, _,
AllOf(HasSession("test-session-name"),
HasLockHint(
google::spanner::v1::ReadRequest::LOCK_HINT_UNSPECIFIED))))
.InSequence(s)
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));

auto conn = MakeConnectionImpl(db, mock);
internal::OptionsSpan span(MakeLimitedTimeOptions());

// Scenario 1: No explicit OrderBy (should map to UNSPECIFIED)
spanner::ReadOptions read_options;
spanner::Transaction txn1 =
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
auto rows1 = conn->Read(
{txn1, "table", spanner::KeySet::All(), {"col"}, read_options});
for (auto const& row : rows1) {
(void)row;
}
EXPECT_THAT(txn1,
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
}

TEST(ConnectionImplTest, ReadRequestLockHintShared) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("project", "instance", "database");
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
EXPECT_CALL(*mock,
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
.WillOnce(Return(make_ready_future(Status{})));
Sequence s;
EXPECT_CALL(
*mock,
StreamingRead(
_, _,
AllOf(
HasSession("test-session-name"),
HasLockHint(google::spanner::v1::ReadRequest::LOCK_HINT_SHARED))))
.InSequence(s)
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));

auto conn = MakeConnectionImpl(db, mock);
internal::OptionsSpan span(MakeLimitedTimeOptions());
spanner::ReadOptions read_options;
spanner::Transaction txn1 =
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
auto read_params =
spanner::Connection::ReadParams{txn1,
"table",
spanner::KeySet::All(),
{"col"},
read_options,
absl::nullopt,
false,
spanner::DirectedReadOption::Type{},
spanner::LockHint::kLockHintShared};
auto rows1 = conn->Read(read_params);
for (auto const& row : rows1) {
(void)row;
}
EXPECT_THAT(txn1,
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
}

TEST(ConnectionImplTest, OperationsFailOnInvalidatedTransaction) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("placeholder_project", "placeholder_instance",
Expand Down
37 changes: 37 additions & 0 deletions google/cloud/spanner/lock_hint.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H

#include "google/cloud/spanner/version.h"

namespace google {
namespace cloud {
namespace spanner {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

// A lock hint mechanism for reads done within a transaction.
enum class LockHint {
kLockHintUnspecified,
kLockHintShared,
kLockHintExclusive,
};

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace spanner
} // namespace cloud
} // namespace google

#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H
11 changes: 11 additions & 0 deletions google/cloud/spanner/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "google/cloud/spanner/backoff_policy.h"
#include "google/cloud/spanner/directed_read_replicas.h"
#include "google/cloud/spanner/internal/session.h"
#include "google/cloud/spanner/lock_hint.h"
#include "google/cloud/spanner/polling_policy.h"
#include "google/cloud/spanner/request_priority.h"
#include "google/cloud/spanner/retry_policy.h"
Expand Down Expand Up @@ -195,6 +196,16 @@ struct SessionPoolActionOnExhaustionOption {
using Type = spanner::ActionOnExhaustion;
};

/**
* Option for `google::cloud::Options` to set the lock hint mechanism for reads
* done within a transaction.
*
* @ingroup google-cloud-spanner-options
*/
struct LockHintOption {
using Type = spanner::LockHint;
};

/**
* Option for `google::cloud::Options` to set the interval at which we refresh
* sessions so they don't get collected by the backend GC.
Expand Down
11 changes: 7 additions & 4 deletions google/cloud/spanner/read_partition.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ struct ReadPartitionInternals {

static spanner::Connection::ReadParams MakeReadParams(
spanner::ReadPartition const& read_partition,
spanner::DirectedReadOption::Type directed_read_option) {
spanner::DirectedReadOption::Type directed_read_option,
spanner::LockHint lock_hint) {
return spanner::Connection::ReadParams{
MakeTransactionFromIds(
read_partition.SessionId(), read_partition.TransactionId(),
Expand All @@ -185,7 +186,8 @@ struct ReadPartitionInternals {
read_partition.ReadOptions(),
read_partition.PartitionToken(),
read_partition.DataBoost(),
std::move(directed_read_option)};
std::move(directed_read_option),
lock_hint};
}
};

Expand All @@ -204,9 +206,10 @@ inline spanner::ReadPartition MakeReadPartition(

inline spanner::Connection::ReadParams MakeReadParams(
spanner::ReadPartition const& read_partition,
spanner::DirectedReadOption::Type directed_read_option) {
spanner::DirectedReadOption::Type directed_read_option,
spanner::LockHint lock_hint) {
return ReadPartitionInternals::MakeReadParams(
read_partition, std::move(directed_read_option));
read_partition, std::move(directed_read_option), lock_hint);
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
5 changes: 4 additions & 1 deletion google/cloud/spanner/read_partition_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,17 @@ TEST(ReadPartitionTest, MakeReadParams) {
Connection::ReadParams params = spanner_internal::MakeReadParams(
expected_partition.Partition(),
IncludeReplicas({ReplicaSelection(ReplicaType::kReadWrite)},
/*auto_failover_disabled=*/true));
/*auto_failover_disabled=*/true),
google::cloud::spanner::LockHint::kLockHintExclusive);

EXPECT_EQ(*params.partition_token, "token");
EXPECT_EQ(params.read_options, read_options);
EXPECT_FALSE(params.partition_data_boost);
EXPECT_EQ(params.columns, columns);
EXPECT_EQ(params.keys, KeySet::All());
EXPECT_EQ(params.table, "Students");
EXPECT_EQ(params.lock_hint,
google::cloud::spanner::LockHint::kLockHintExclusive);
EXPECT_THAT(params.transaction,
HasSessionAndTransaction("session", "txn-id", true, "tag"));
EXPECT_THAT(
Expand Down
Loading