diff --git a/google/cloud/spanner/CMakeLists.txt b/google/cloud/spanner/CMakeLists.txt index a4b54cf773fd7..f2a219e9af82d 100644 --- a/google/cloud/spanner/CMakeLists.txt +++ b/google/cloud/spanner/CMakeLists.txt @@ -150,6 +150,8 @@ add_library( internal/spanner_logging_decorator.h internal/spanner_metadata_decorator.cc internal/spanner_metadata_decorator.h + internal/spanner_request_id.cc + internal/spanner_request_id.h internal/spanner_stub.cc internal/spanner_stub.h internal/spanner_stub_factory.cc @@ -462,6 +464,7 @@ function (spanner_client_define_tests) internal/partial_result_set_source_test.cc internal/route_to_leader_test.cc internal/session_pool_test.cc + internal/spanner_request_id_test.cc internal/spanner_stub_factory_test.cc internal/status_utils_test.cc internal/transaction_impl_test.cc diff --git a/google/cloud/spanner/google_cloud_cpp_spanner.bzl b/google/cloud/spanner/google_cloud_cpp_spanner.bzl index 4ee5cfe878095..28c55f4bd33a8 100644 --- a/google/cloud/spanner/google_cloud_cpp_spanner.bzl +++ b/google/cloud/spanner/google_cloud_cpp_spanner.bzl @@ -81,6 +81,7 @@ google_cloud_cpp_spanner_hdrs = [ "internal/spanner_auth_decorator.h", "internal/spanner_logging_decorator.h", "internal/spanner_metadata_decorator.h", + "internal/spanner_request_id.h", "internal/spanner_stub.h", "internal/spanner_stub_factory.h", "internal/spanner_tracing_stub.h", @@ -172,6 +173,7 @@ google_cloud_cpp_spanner_srcs = [ "internal/spanner_auth_decorator.cc", "internal/spanner_logging_decorator.cc", "internal/spanner_metadata_decorator.cc", + "internal/spanner_request_id.cc", "internal/spanner_stub.cc", "internal/spanner_stub_factory.cc", "internal/spanner_tracing_stub.cc", diff --git a/google/cloud/spanner/internal/spanner_request_id.cc b/google/cloud/spanner/internal/spanner_request_id.cc new file mode 100644 index 0000000000000..c0da0bdc3f2e3 --- /dev/null +++ b/google/cloud/spanner/internal/spanner_request_id.cc @@ -0,0 +1,84 @@ +// Copyright 2026 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. + +#include "google/cloud/spanner/internal/spanner_request_id.h" +#include "google/cloud/internal/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace google { +namespace cloud { +namespace spanner_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +std::string ProcessRandomId() { +#ifndef _WIN32 + static std::atomic pid{0}; + static std::atomic random_id{0}; + pid_t const current_pid = getpid(); + if (pid.load(std::memory_order_acquire) != current_pid) { + auto generator = google::cloud::internal::MakeDefaultPRNG(); + std::uniform_int_distribution dist; + random_id.store(dist(generator), std::memory_order_release); + pid.store(current_pid, std::memory_order_release); + } + return absl::StrFormat("%016x", random_id.load(std::memory_order_acquire)); +#else + static std::string const random_id = [] { + auto generator = google::cloud::internal::MakeDefaultPRNG(); + std::uniform_int_distribution dist; + return absl::StrFormat("%016x", dist(generator)); + }(); + return random_id; +#endif +} + +std::uint64_t NextClientId() { + static std::atomic counter{0}; + return ++counter; +} + +std::string FormatSpannerRequestStaticPrefix(std::uint32_t version, + std::string_view process_random_id, + std::uint64_t client_id) { + return absl::StrCat(version, ".", process_random_id, ".", client_id, "."); +} + +std::string FormatSpannerRequestId(std::string_view static_prefix, + std::uint32_t channel_id, + std::uint64_t request_index, + std::uint32_t attempt_index) { + return absl::StrCat(static_prefix, channel_id, ".", request_index, ".", + attempt_index); +} + +std::string FormatSpannerRequestId(std::uint32_t version, + std::string_view process_random_id, + std::uint64_t client_id, + std::uint32_t channel_id, + std::uint64_t request_index, + std::uint32_t attempt_index) { + return absl::StrCat(version, ".", process_random_id, ".", client_id, ".", + channel_id, ".", request_index, ".", attempt_index); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace spanner_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/spanner/internal/spanner_request_id.h b/google/cloud/spanner/internal/spanner_request_id.h new file mode 100644 index 0000000000000..e8fa29a16b3c1 --- /dev/null +++ b/google/cloud/spanner/internal/spanner_request_id.h @@ -0,0 +1,63 @@ +// Copyright 2026 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_INTERNAL_SPANNER_REQUEST_ID_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_INTERNAL_SPANNER_REQUEST_ID_H + +#include "google/cloud/spanner/version.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace spanner_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/// Generates a 16-character zero-padded lowercase hex random ID from a 64-bit +/// random integer. On POSIX systems, re-generates the ID if a process fork is +/// detected. On non-POSIX systems (e.g. Windows), generates once per process +/// lifecycle. +std::string ProcessRandomId(); + +/// Returns the next sequential process-wide client ID (thread-safe). +std::uint64_t NextClientId(); + +/// Formats the static 3-field prefix: "..." +std::string FormatSpannerRequestStaticPrefix(std::uint32_t version, + std::string_view process_random_id, + std::uint64_t client_id); + +/// Formats full request ID using the cached static prefix: +/// ".." +std::string FormatSpannerRequestId(std::string_view static_prefix, + std::uint32_t channel_id, + std::uint64_t request_index, + std::uint32_t attempt_index); + +/// Convenience overload formatting all 6 fields: +/// "....." +std::string FormatSpannerRequestId(std::uint32_t version, + std::string_view process_random_id, + std::uint64_t client_id, + std::uint32_t channel_id, + std::uint64_t request_index, + std::uint32_t attempt_index); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace spanner_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_INTERNAL_SPANNER_REQUEST_ID_H diff --git a/google/cloud/spanner/internal/spanner_request_id_test.cc b/google/cloud/spanner/internal/spanner_request_id_test.cc new file mode 100644 index 0000000000000..0ce37245dd886 --- /dev/null +++ b/google/cloud/spanner/internal/spanner_request_id_test.cc @@ -0,0 +1,124 @@ +// Copyright 2026 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. + +#include "google/cloud/spanner/internal/spanner_request_id.h" +#include +#include +#include +#ifndef _WIN32 +#include +#include +#endif + +namespace google { +namespace cloud { +namespace spanner_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::testing::Eq; +using ::testing::Ge; +using ::testing::Gt; +using ::testing::Ne; + +MATCHER_P(MatchesStdRegex, pattern, "") { + if (std::regex_match(arg, std::regex(pattern))) { + return true; + } + *result_listener << "which does not match regex \"" << pattern << "\""; + return false; +} + +TEST(SpannerRequestIdTest, ProcessRandomIdFormat) { + std::string const id1 = ProcessRandomId(); + EXPECT_THAT(id1, MatchesStdRegex("^[0-9a-f]{16}$")); + std::string const id2 = ProcessRandomId(); + EXPECT_THAT(id2, Eq(id1)); +} + +TEST(SpannerRequestIdTest, NextClientIdMonotonic) { + std::uint64_t const c1 = NextClientId(); + std::uint64_t const c2 = NextClientId(); + std::uint64_t const c3 = NextClientId(); + EXPECT_THAT(c1, Gt(0ULL)); + EXPECT_THAT(c2, Eq(c1 + 1)); + EXPECT_THAT(c3, Eq(c2 + 1)); +} + +TEST(SpannerRequestIdTest, FormatSpannerRequestStaticPrefix) { + std::string const prefix = + FormatSpannerRequestStaticPrefix(1, "0123456789abcdef", 42); + EXPECT_THAT(prefix, Eq("1.0123456789abcdef.42.")); +} + +TEST(SpannerRequestIdTest, FormatSpannerRequestIdWithPrefix) { + std::string const prefix = "1.0123456789abcdef.42."; + std::string const request_id = FormatSpannerRequestId(prefix, 1, 100, 2); + EXPECT_THAT(request_id, Eq("1.0123456789abcdef.42.1.100.2")); +} + +TEST(SpannerRequestIdTest, FormatSpannerRequestIdDirect) { + std::string const request_id = + FormatSpannerRequestId(1, "0123456789abcdef", 42, 3, 200, 1); + EXPECT_THAT(request_id, Eq("1.0123456789abcdef.42.3.200.1")); +} + +#ifndef _WIN32 +TEST(SpannerRequestIdTest, ProcessRandomIdForkRegeneration) { + // Ensure the parent has already initialized ProcessRandomId + std::string const parent_id = ProcessRandomId(); + ASSERT_THAT(parent_id, MatchesStdRegex("^[0-9a-f]{16}$")); + + int pipe_fds[2]; + ASSERT_THAT(pipe(pipe_fds), Eq(0)); + + pid_t const pid = fork(); + ASSERT_THAT(pid, Ge(0)); + + if (pid == 0) { + // Child process: read ProcessRandomId and write to pipe + close(pipe_fds[0]); + std::string const child_id = ProcessRandomId(); + ssize_t const bytes_written = + write(pipe_fds[1], child_id.data(), child_id.size()); + close(pipe_fds[1]); + if (bytes_written != static_cast(child_id.size())) { + _exit(1); + } + _exit(0); + } + + // Parent process: read child's ID from pipe + close(pipe_fds[1]); + char buffer[32] = {0}; + ssize_t const bytes_read = read(pipe_fds[0], buffer, sizeof(buffer) - 1); + close(pipe_fds[0]); + + int status = 0; + waitpid(pid, &status, 0); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_THAT(WEXITSTATUS(status), Eq(0)); + + ASSERT_THAT(bytes_read, Eq(16)); + std::string const child_id(buffer, static_cast(bytes_read)); + EXPECT_THAT(child_id, MatchesStdRegex("^[0-9a-f]{16}$")); + EXPECT_THAT(child_id, Ne(parent_id)); +} +#endif + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace spanner_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/spanner/spanner_client_unit_tests.bzl b/google/cloud/spanner/spanner_client_unit_tests.bzl index 6a501b33fdeb2..80753ba184c09 100644 --- a/google/cloud/spanner/spanner_client_unit_tests.bzl +++ b/google/cloud/spanner/spanner_client_unit_tests.bzl @@ -37,6 +37,7 @@ spanner_client_unit_tests = [ "internal/partial_result_set_source_test.cc", "internal/route_to_leader_test.cc", "internal/session_pool_test.cc", + "internal/spanner_request_id_test.cc", "internal/spanner_stub_factory_test.cc", "internal/status_utils_test.cc", "internal/transaction_impl_test.cc",