diff --git a/ci/cloudbuild/builds/check-api.sh b/ci/cloudbuild/builds/check-api.sh index 97b5106d60d79..7804290967239 100755 --- a/ci/cloudbuild/builds/check-api.sh +++ b/ci/cloudbuild/builds/check-api.sh @@ -124,7 +124,7 @@ function check_abi() { # characters in the string "internal", and it should again be followed # by some other number indicating the length of the symbol within the # "internal" namespace. See: https://en.wikipedia.org/wiki/Name_mangling - -skip-internal-symbols "(8internal|_internal|4absl|4grpc|6google8protobuf|6google3rpc)\d" + -skip-internal-symbols "(8internal|_internal|4absl|4grpc|6google8protobuf|6google3rpc|20storage_experimental)\d" # We ignore the raw gRPC Stub class. The generated gRPC headers that # contain these classes are installed alongside our headers. When a new # RPC is added to a service, these classes gain a pure virtual method. Our diff --git a/google/cloud/storage/async/client.cc b/google/cloud/storage/async/client.cc index 7ba5c0ec50e2e..29b875d7c0f90 100644 --- a/google/cloud/storage/async/client.cc +++ b/google/cloud/storage/async/client.cc @@ -49,6 +49,26 @@ future> AsyncClient::InsertObject( internal::MergeOptions(std::move(opts), connection_->options())}); } +future> AsyncClient::Open( + BucketName const& bucket_name, std::string object_name, Options opts) { + auto spec = google::storage::v2::BidiReadObjectSpec{}; + spec.set_bucket(bucket_name.FullName()); + spec.set_object(std::move(object_name)); + return Open(std::move(spec), std::move(opts)); +} + +future> AsyncClient::Open( + google::storage::v2::BidiReadObjectSpec spec, Options opts) { + return connection_ + ->Open({std::move(spec), + internal::MergeOptions(std::move(opts), connection_->options())}) + .then([](auto f) -> StatusOr { + auto connection = f.get(); + if (!connection) return std::move(connection).status(); + return ObjectDescriptor(*std::move(connection)); + }); +} + future>> AsyncClient::ReadObject( BucketName const& bucket_name, std::string object_name, Options opts) { auto request = google::storage::v2::ReadObjectRequest{}; @@ -92,6 +112,65 @@ future> AsyncClient::ReadObjectRange( internal::MergeOptions(std::move(opts), connection_->options())}); } +future>> +AsyncClient::StartAppendableObjectUpload(BucketName const& bucket_name, + std::string object_name, + Options opts) { + auto request = google::storage::v2::BidiWriteObjectRequest{}; + auto& resource = *request.mutable_write_object_spec()->mutable_resource(); + + resource.set_bucket(BucketName(bucket_name).FullName()); + resource.set_name(std::move(object_name)); + request.mutable_write_object_spec()->set_appendable(true); + + return StartAppendableObjectUpload(std::move(request), std::move(opts)); +} + +future>> +AsyncClient::StartAppendableObjectUpload( + google::storage::v2::BidiWriteObjectRequest request, Options opts) { + return connection_ + ->StartAppendableObjectUpload( + {std::move(request), + internal::MergeOptions(std::move(opts), connection_->options())}) + .then([](auto f) -> StatusOr> { + auto w = f.get(); + if (!w) return std::move(w).status(); + auto t = absl::holds_alternative( + (*w)->PersistedState()) + ? AsyncToken() + : storage_internal::MakeAsyncToken(w->get()); + return std::make_pair(AsyncWriter(*std::move(w)), std::move(t)); + }); +} + +future>> +AsyncClient::ResumeAppendableObjectUpload(BucketName const& bucket_name, + std::string object_name, + std::int64_t generation, + Options opts) { + auto request = google::storage::v2::BidiWriteObjectRequest{}; + auto& append_object_spec = *request.mutable_append_object_spec(); + + append_object_spec.set_bucket(BucketName(bucket_name).FullName()); + append_object_spec.set_object(std::move(object_name)); + append_object_spec.set_generation(generation); + + return connection_ + ->ResumeAppendableObjectUpload( + {std::move(request), + internal::MergeOptions(std::move(opts), connection_->options())}) + .then([](auto f) -> StatusOr> { + auto w = f.get(); + if (!w) return std::move(w).status(); + auto t = absl::holds_alternative( + (*w)->PersistedState()) + ? AsyncToken() + : storage_internal::MakeAsyncToken(w->get()); + return std::make_pair(AsyncWriter(*std::move(w)), std::move(t)); + }); +} + future>> AsyncClient::StartBufferedUpload(BucketName const& bucket_name, std::string object_name, Options opts) { diff --git a/google/cloud/storage/async/client.h b/google/cloud/storage/async/client.h index 1045b7e9bcd04..ee9addb6b6031 100644 --- a/google/cloud/storage/async/client.h +++ b/google/cloud/storage/async/client.h @@ -17,6 +17,7 @@ #include "google/cloud/storage/async/bucket_name.h" #include "google/cloud/storage/async/connection.h" +#include "google/cloud/storage/async/object_descriptor.h" #include "google/cloud/storage/async/reader.h" #include "google/cloud/storage/async/rewriter.h" #include "google/cloud/storage/async/token.h" @@ -231,6 +232,41 @@ class AsyncClient { google::storage::v2::WriteObjectRequest request, WritePayload contents, Options opts = {}); + /** + * Open an object descriptor to perform one or more ranged reads. + * + * @par Idempotency + * This is a read-only operation and is always idempotent. The operation will + * retry until the descriptor is successfully created. The descriptor itself + * will resume any incomplete ranged reads if the connection(s) are + * interrupted. Use `ResumePolicyOption` and `ResumePolicy` to control this. + * + * @param bucket_name the name of the bucket that contains the object. + * @param object_name the name of the object to be read. + * @param opts options controlling the behavior of this RPC, for example + * the application may change the retry policy. + */ + future> Open(BucketName const& bucket_name, + std::string object_name, + Options opts = {}); + + /** + * Open an object descriptor to perform one or more ranged reads. + * + * @par Idempotency + * This is a read-only operation and is always idempotent. The operation will + * retry until the descriptor is successfully created. The descriptor itself + * will resume any incomplete ranged reads if the connection(s) are + * interrupted. Use `ResumePolicyOption` and `ResumePolicy` to control this. + * + * @param spec the BidiReadObjectSpec to use when retrieving the + * ObjectDescriptor. + * @param opts options controlling the behavior of this RPC, for example + * the application may change the retry policy. + */ + future> Open( + google::storage::v2::BidiReadObjectSpec spec, Options opts = {}); + /** * A streaming download for the contents of an object. * @@ -243,7 +279,7 @@ class AsyncClient { * @par Idempotency * This is a read-only operation and is always idempotent. Once the download * starts, this operation will automatically resume the download if is - * interrupted. Use `ResumePolicyOption` and `ResumePolicy` to control this + * interrupted. Use `ResumePolicyOption` and `ResumePolicy` to control this. * * @param bucket_name the name of the bucket that contains the object. * @param object_name the name of the object to be read. @@ -334,10 +370,82 @@ class AsyncClient { google::storage::v2::ReadObjectRequest request, std::int64_t offset, std::int64_t limit, Options opts = {}); + /* + [start-appendable-object-upload] + Initiates a [resumable upload][resumable-link] for an appendable object. + + Appendable objects allow you to create an object and upload data to it + incrementally until it is finalized. This means you can start an upload + and append data to the object later. + + You can finalize an appendable object in the first call itself by providing + all the data in the initial upload. You can also explicitly Flush to ensure + the data is persisted. + + The recovery can be done from most transient errors, including an unexpected + closure of the streaming RPC used for the upload. + + @par Example + @snippet storage_async_samples.cc start-appendable-object-upload + + @par Idempotency + This function is always treated as idempotent, and the library will + automatically retry the function on transient errors. + + [resumable-link]: https://cloud.google.com/storage/docs/resumable-uploads + [start-appendable-object-upload] + */ + + /** + * Starts a new resumable upload session for appendable objects and + * automatic recovery from transient failures. + * + * @snippet{doc} async/client.h start-appendable-object-upload + * + * @param bucket_name the name of the bucket that contains the object. + * @param object_name the name of the object to be read. + * @param opts options controlling the behavior of this RPC, for example + * the application may change the retry policy. + */ + future>> + StartAppendableObjectUpload(BucketName const& bucket_name, + std::string object_name, Options opts = {}); + + /** + * Starts a new resumable upload session for appendable objects and + * automatic recovery from transient failures. + * + * @snippet{doc} async/client.h start-appendable-object-upload + * + * @param request the request contents, it must include the bucket name and + * object names. Many other fields are optional. + * @param opts options controlling the behavior of this RPC, for example + * the application may change the retry policy. + */ + future>> + StartAppendableObjectUpload( + google::storage::v2::BidiWriteObjectRequest request, Options opts = {}); + + /** + * Resume a resumable upload session for appendable objects and automatic + * recovery from transient failures. + * + * @param bucket_name the name of the bucket that contains the object. + * @param object_name the name of the object to be uploaded. + * @param generation the object generation to be uploaded. + * @param opts options controlling the behaviour of this RPC, for example the + * application may change the retry policy. + */ + future>> + ResumeAppendableObjectUpload(BucketName const& bucket_name, + std::string object_name, std::int64_t generation, + Options opts = {}); + /* [start-buffered-upload-common] This function always uses [resumable uploads][resumable-link]. The objects returned by this function buffer data until it is persisted on the service. + If the buffer becomes full, they stop accepting new data until the service has persisted enough data. diff --git a/google/cloud/storage/async/client_test.cc b/google/cloud/storage/async/client_test.cc index cda59fae31901..5ad482ad1bed8 100644 --- a/google/cloud/storage/async/client_test.cc +++ b/google/cloud/storage/async/client_test.cc @@ -14,6 +14,7 @@ #include "google/cloud/storage/async/client.h" #include "google/cloud/storage/mocks/mock_async_connection.h" +#include "google/cloud/storage/mocks/mock_async_object_descriptor_connection.h" #include "google/cloud/storage/mocks/mock_async_reader_connection.h" #include "google/cloud/storage/mocks/mock_async_rewriter_connection.h" #include "google/cloud/storage/mocks/mock_async_writer_connection.h" @@ -33,6 +34,7 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { using ::google::cloud::storage_mocks::MockAsyncConnection; +using ::google::cloud::storage_mocks::MockAsyncObjectDescriptorConnection; using ::google::cloud::storage_mocks::MockAsyncReaderConnection; using ::google::cloud::storage_mocks::MockAsyncRewriterConnection; using ::google::cloud::storage_mocks::MockAsyncWriterConnection; @@ -164,6 +166,117 @@ TEST(AsyncClient, InsertObject3) { EXPECT_THAT(response, IsOkAndHolds(IsProtoEqual(TestProtoObject()))); } +TEST(AsyncClient, Open) { + auto constexpr kExpectedRequest = R"pb( + bucket: "projects/_/buckets/test-bucket" + object: "test-object" + )pb"; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, options) + .WillRepeatedly( + Return(Options{}.set>("O0").set>("O1"))); + + EXPECT_CALL(*mock, Open).WillOnce([&](AsyncConnection::OpenParams const& p) { + EXPECT_THAT(p.options.get>(), "O0"); + EXPECT_THAT(p.options.get>(), "O1-function"); + EXPECT_THAT(p.options.get>(), "O2-function"); + auto expected = google::storage::v2::BidiReadObjectSpec{}; + EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); + EXPECT_THAT(p.read_spec, IsProtoEqual(expected)); + auto descriptor = std::make_shared(); + EXPECT_CALL(*descriptor, Read).WillOnce([] { + auto reader = std::make_unique(); + EXPECT_CALL(*reader, Read).WillOnce([] { + return make_ready_future(AsyncReaderConnection::ReadResponse{Status{}}); + }); + return reader; + }); + return make_ready_future(make_status_or( + std::shared_ptr(std::move(descriptor)))); + }); + + auto client = AsyncClient(mock); + auto descriptor = client + .Open(BucketName("test-bucket"), "test-object", + Options{} + .set>("O1-function") + .set>("O2-function")) + .get(); + ASSERT_STATUS_OK(descriptor); + + AsyncReader r; + AsyncToken t; + std::tie(r, t) = descriptor->Read(100, 200); + EXPECT_TRUE(t.valid()); + + auto pt = r.Read(std::move(t)).get(); + AsyncReaderConnection::ReadResponse p; + AsyncToken t2; + ASSERT_STATUS_OK(pt); + std::tie(p, t2) = *std::move(pt); + EXPECT_FALSE(t2.valid()); + EXPECT_THAT( + p, VariantWith(ResultOf( + "empty response", [](auto const& p) { return p.size(); }, 0))); +} + +TEST(AsyncClient, OpenWithInvalidBucket) { + auto constexpr kExpectedRequest = R"pb( + bucket: "test-only-invalid" + object: "test-object" + )pb"; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, options) + .WillRepeatedly( + Return(Options{}.set>("O0").set>("O1"))); + + EXPECT_CALL(*mock, Open).WillOnce([&](AsyncConnection::OpenParams const& p) { + EXPECT_THAT(p.options.get>(), "O0"); + EXPECT_THAT(p.options.get>(), "O1-function"); + EXPECT_THAT(p.options.get>(), "O2-function"); + auto expected = google::storage::v2::BidiReadObjectSpec{}; + EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); + EXPECT_THAT(p.read_spec, IsProtoEqual(expected)); + auto descriptor = std::make_shared(); + EXPECT_CALL(*descriptor, Read).WillOnce([] { + auto reader = std::make_unique(); + EXPECT_CALL(*reader, Read).WillOnce([] { + return make_ready_future(AsyncReaderConnection::ReadResponse{Status{}}); + }); + return reader; + }); + return make_ready_future(make_status_or( + std::shared_ptr(std::move(descriptor)))); + }); + + auto client = AsyncClient(mock); + auto read_spec = google::storage::v2::BidiReadObjectSpec{}; + read_spec.set_bucket("test-only-invalid"); + read_spec.set_object("test-object"); + auto descriptor = + client + .Open(std::move(read_spec), Options{} + .set>("O1-function") + .set>("O2-function")) + .get(); + ASSERT_STATUS_OK(descriptor); + + AsyncReader r; + AsyncToken t; + std::tie(r, t) = descriptor->Read(100, 200); + EXPECT_TRUE(t.valid()); + + auto pt = r.Read(std::move(t)).get(); + AsyncReaderConnection::ReadResponse p; + AsyncToken t2; + ASSERT_STATUS_OK(pt); + std::tie(p, t2) = *std::move(pt); + EXPECT_FALSE(t2.valid()); + EXPECT_THAT( + p, VariantWith(ResultOf( + "empty response", [](auto const& p) { return p.size(); }, 0))); +} + TEST(AsyncClient, ReadObject1) { auto constexpr kExpectedRequest = R"pb( bucket: "projects/_/buckets/test-bucket" @@ -266,6 +379,145 @@ TEST(AsyncClient, ReadObject2) { "empty response", [](auto const& p) { return p.size(); }, 0))); } +TEST(AsyncClient, StartAppendableObjectUpload1) { + auto constexpr kExpectedRequest = R"pb( + write_object_spec { + resource { bucket: "projects/_/buckets/test-bucket" name: "test-object" } + appendable: true + } + )pb"; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, options) + .WillRepeatedly( + Return(Options{}.set>("O0").set>("O1"))); + + EXPECT_CALL(*mock, StartAppendableObjectUpload) + .WillOnce([&](AsyncConnection::AppendableUploadParams const& p) { + EXPECT_THAT(p.options.get>(), "O0"); + EXPECT_THAT(p.options.get>(), "O1-function"); + EXPECT_THAT(p.options.get>(), "O2-function"); + auto expected = google::storage::v2::BidiWriteObjectRequest{}; + EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); + EXPECT_THAT(p.request, IsProtoEqual(expected)); + auto writer = std::make_unique(); + EXPECT_CALL(*writer, PersistedState).WillOnce(Return(0)); + EXPECT_CALL(*writer, Finalize).WillRepeatedly([] { + return make_ready_future(make_status_or(TestProtoObject())); + }); + return make_ready_future(make_status_or( + std::unique_ptr(std::move(writer)))); + }); + + auto client = AsyncClient(mock); + auto wt = + client + .StartAppendableObjectUpload(BucketName("test-bucket"), "test-object", + Options{} + .set>("O1-function") + .set>("O2-function")) + .get(); + ASSERT_STATUS_OK(wt); + AsyncWriter w; + AsyncToken t; + std::tie(w, t) = *std::move(wt); + EXPECT_TRUE(t.valid()); + auto object = w.Finalize(std::move(t)).get(); + EXPECT_THAT(object, IsOkAndHolds(IsProtoEqual(TestProtoObject()))); +} + +TEST(AsyncClient, StartAppendableObjectUpload2) { + auto constexpr kExpectedRequest = R"pb( + write_object_spec { + resource { bucket: "projects/_/buckets/test-bucket", name: "test-object" } + appendable: true + } + )pb"; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, options) + .WillRepeatedly( + Return(Options{}.set>("O0").set>("O1"))); + + EXPECT_CALL(*mock, StartAppendableObjectUpload) + .WillOnce([&](AsyncConnection::AppendableUploadParams const& p) { + EXPECT_THAT(p.options.get>(), "O0"); + EXPECT_THAT(p.options.get>(), "O1-function"); + EXPECT_THAT(p.options.get>(), "O2-function"); + auto expected = google::storage::v2::BidiWriteObjectRequest{}; + EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); + EXPECT_THAT(p.request, IsProtoEqual(expected)); + auto writer = std::make_unique(); + EXPECT_CALL(*writer, PersistedState).WillOnce(Return(0)); + EXPECT_CALL(*writer, Finalize).WillRepeatedly([] { + return make_ready_future(make_status_or(TestProtoObject())); + }); + return make_ready_future(make_status_or( + std::unique_ptr(std::move(writer)))); + }); + + auto client = AsyncClient(mock); + auto request = google::storage::v2::BidiWriteObjectRequest{}; + ASSERT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &request)); + auto wt = client + .StartAppendableObjectUpload( + std::move(request), Options{} + .set>("O1-function") + .set>("O2-function")) + .get(); + ASSERT_STATUS_OK(wt); + AsyncWriter w; + AsyncToken t; + std::tie(w, t) = *std::move(wt); + EXPECT_TRUE(t.valid()); + auto object = w.Finalize(std::move(t)).get(); + EXPECT_THAT(object, IsOkAndHolds(IsProtoEqual(TestProtoObject()))); +} + +TEST(AsyncClient, ResumeAppendableObjectUpload1) { + auto constexpr kExpectedRequest = R"pb( + append_object_spec { + bucket: "projects/_/buckets/test-bucket", + object: "test-object", + generation: 42 + } + )pb"; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, options) + .WillRepeatedly( + Return(Options{}.set>("O0").set>("O1"))); + + EXPECT_CALL(*mock, ResumeAppendableObjectUpload) + .WillOnce([&](AsyncConnection::AppendableUploadParams const& p) { + EXPECT_THAT(p.options.get>(), "O0"); + EXPECT_THAT(p.options.get>(), "O1-function"); + EXPECT_THAT(p.options.get>(), "O2-function"); + auto expected = google::storage::v2::BidiWriteObjectRequest{}; + EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); + EXPECT_THAT(p.request, IsProtoEqual(expected)); + auto writer = std::make_unique(); + EXPECT_CALL(*writer, PersistedState) + .WillRepeatedly(Return(TestProtoObject())); + + return make_ready_future(make_status_or( + std::unique_ptr(std::move(writer)))); + }); + + auto client = AsyncClient(mock); + auto wt = client + .ResumeAppendableObjectUpload( + BucketName("test-bucket"), "test-object", 42, + Options{} + .set>("O1-function") + .set>("O2-function")) + .get(); + ASSERT_STATUS_OK(wt); + AsyncWriter w; + AsyncToken t; + std::tie(w, t) = *std::move(wt); + EXPECT_FALSE(t.valid()); + EXPECT_THAT(w.PersistedState(), VariantWith( + IsProtoEqual(TestProtoObject()))); +} + TEST(AsyncClient, StartBufferedUpload1) { auto constexpr kExpectedRequest = R"pb( write_object_spec { diff --git a/google/cloud/storage/async/connection.h b/google/cloud/storage/async/connection.h index 77fdbfa6ec442..27cdd9541efd0 100644 --- a/google/cloud/storage/async/connection.h +++ b/google/cloud/storage/async/connection.h @@ -15,6 +15,7 @@ #ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_CONNECTION_H #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_CONNECTION_H +#include "google/cloud/storage/async/object_descriptor_connection.h" #include "google/cloud/storage/async/object_responses.h" #include "google/cloud/storage/async/write_payload.h" #include "google/cloud/storage/internal/object_requests.h" @@ -75,6 +76,22 @@ class AsyncConnection { virtual future> InsertObject( InsertObjectParams p) = 0; + /** + * A thin wrapper around the `Open()` parameters. + * + * We use a single struct as the input parameter for this function to + * prevent breaking any mocks when additional parameters are needed. + */ + struct OpenParams { + google::storage::v2::BidiReadObjectSpec read_spec; + Options options; + }; + + /// Open an object to perform multiple reads. + virtual future>> + Open(OpenParams p) = 0; + /** * A thin wrapper around the `ReadObject()` parameters. * @@ -98,6 +115,27 @@ class AsyncConnection { /// Read a range from an object returning all the contents. virtual future> ReadObjectRange(ReadObjectParams p) = 0; + /** + * A thin wrapper around the `WriteObject()` parameters for appendable object + */ + struct AppendableUploadParams { + /// The bucket name and object name for the new object. + google::storage::v2::BidiWriteObjectRequest request; + /// Any options modifying the RPC behavior, including per-client and + /// per-connection options. + Options options; + }; + + /// Start an appendable upload configured for persistent sources. + virtual future< + StatusOr>> + StartAppendableObjectUpload(AppendableUploadParams p) = 0; + + /// Resume an appendable upload configured for persistent sources. + virtual future< + StatusOr>> + ResumeAppendableObjectUpload(AppendableUploadParams p) = 0; + /** * A thin wrapper around the `WriteObject()` parameters. * diff --git a/google/cloud/storage/async/object_descriptor.cc b/google/cloud/storage/async/object_descriptor.cc new file mode 100644 index 0000000000000..71c92eb6c526a --- /dev/null +++ b/google/cloud/storage/async/object_descriptor.cc @@ -0,0 +1,51 @@ +// Copyright 2024 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/storage/async/object_descriptor.h" +#include "google/cloud/internal/make_status.h" + +namespace google { +namespace cloud { +namespace storage_experimental { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +absl::optional ObjectDescriptor::metadata() const { + return impl_->metadata(); +} + +std::pair ObjectDescriptor::Read(std::int64_t offset, + std::int64_t limit) { + auto reader = impl_->Read({offset, limit}); + auto token = storage_internal::MakeAsyncToken(reader.get()); + return {AsyncReader(std::move(reader)), std::move(token)}; +} + +std::pair ObjectDescriptor::ReadFromOffset( + std::int64_t offset) { + auto reader = impl_->Read({offset, 0}); + auto token = storage_internal::MakeAsyncToken(reader.get()); + return {AsyncReader(std::move(reader)), std::move(token)}; +} + +std::pair ObjectDescriptor::ReadLast( + std::int64_t limit) { + auto reader = impl_->Read({-limit, 0}); + auto token = storage_internal::MakeAsyncToken(reader.get()); + return {AsyncReader(std::move(reader)), std::move(token)}; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_experimental +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/async/object_descriptor.h b/google/cloud/storage/async/object_descriptor.h new file mode 100644 index 0000000000000..dace25c5b7812 --- /dev/null +++ b/google/cloud/storage/async/object_descriptor.h @@ -0,0 +1,88 @@ +// Copyright 2024 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_STORAGE_ASYNC_OBJECT_DESCRIPTOR_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_OBJECT_DESCRIPTOR_H + +#include "google/cloud/storage/async/object_descriptor_connection.h" +#include "google/cloud/storage/async/reader.h" +#include "google/cloud/storage/async/token.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include "absl/types/optional.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_experimental { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * `ObjectDescriptor` is analogous to a file descriptor. + * + * Applications use an `ObjectDescriptor` to perform multiple reads on the same + * Google Cloud Storage object. + */ +class ObjectDescriptor { + public: + /// Creates an uninitialized descriptor. + /// + /// It is UB (undefined behavior) to use any functions on this descriptor. + ObjectDescriptor() = default; + + /// Initialize a descriptor from its implementation class. + explicit ObjectDescriptor(std::shared_ptr impl) + : impl_(std::move(impl)) {} + + ///@{ + /// @name Move-only. + ObjectDescriptor(ObjectDescriptor&&) noexcept = default; + ObjectDescriptor& operator=(ObjectDescriptor&&) noexcept = default; + ObjectDescriptor(ObjectDescriptor const&) = delete; + ObjectDescriptor& operator=(ObjectDescriptor const&) = delete; + ///@} + + /// Returns, if available, the object metadata associated with this + /// descriptor. + absl::optional metadata() const; + + /** + * Starts a new range read in the current descriptor. + */ + std::pair Read(std::int64_t offset, + std::int64_t limit); + + /** + * Starts a new read beginning at the supplied offset and continuing + * until the end. + */ + std::pair ReadFromOffset(std::int64_t offset); + + /** + * Reads the last number of bytes from the end. + */ + std::pair ReadLast(std::int64_t limit); + + private: + std::shared_ptr impl_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_experimental +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_OBJECT_DESCRIPTOR_H diff --git a/google/cloud/storage/async/object_descriptor_connection.h b/google/cloud/storage/async/object_descriptor_connection.h new file mode 100644 index 0000000000000..834be752c22c8 --- /dev/null +++ b/google/cloud/storage/async/object_descriptor_connection.h @@ -0,0 +1,69 @@ +// Copyright 2024 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_STORAGE_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H + +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/options.h" +#include "google/cloud/version.h" +#include "absl/types/optional.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_experimental { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * `ObjectDescriptor` is analogous to a file descriptor. + * + * Applications use an `ObjectDescriptor` to perform multiple reads on the same + * Google Cloud Storage object. + */ +class ObjectDescriptorConnection { + public: + virtual ~ObjectDescriptorConnection() = default; + + virtual Options options() const = 0; + + /// Returns, if available, the object metadata associated with this + /// descriptor. + virtual absl::optional metadata() const = 0; + + /** + * A thin wrapper around the `Read()` parameters. + * + * We use a single struct as the input parameter for this function to + * prevent breaking any mocks when additional parameters are needed. + */ + struct ReadParams { + std::int64_t start; + std::int64_t length; + }; + + /** + * Starts a new range read in the current descriptor. + */ + virtual std::unique_ptr Read(ReadParams p) = 0; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_experimental +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H diff --git a/google/cloud/storage/async/object_descriptor_test.cc b/google/cloud/storage/async/object_descriptor_test.cc new file mode 100644 index 0000000000000..afd43f31e4c70 --- /dev/null +++ b/google/cloud/storage/async/object_descriptor_test.cc @@ -0,0 +1,154 @@ +// Copyright 2024 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/storage/async/object_descriptor.h" +#include "google/cloud/storage/mocks/mock_async_object_descriptor_connection.h" +#include "google/cloud/storage/mocks/mock_async_reader_connection.h" +#include "google/cloud/testing_util/status_matchers.h" +#include + +namespace google { +namespace cloud { +namespace storage_experimental { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage_mocks::MockAsyncObjectDescriptorConnection; +using ::google::cloud::storage_mocks::MockAsyncReaderConnection; +using ::testing::ElementsAre; +using ::testing::Return; +using ReadParams = ::google::cloud::storage_experimental:: + ObjectDescriptorConnection::ReadParams; +using ReadResponse = + ::google::cloud::storage_experimental::AsyncReaderConnection::ReadResponse; + +TEST(ObjectDescriptor, Basic) { + auto mock = std::make_shared(); + EXPECT_CALL(*mock, metadata).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(*mock, Read) + .WillOnce([](ReadParams p) -> std::unique_ptr { + EXPECT_EQ(p.start, 100); + EXPECT_EQ(p.length, 200); + auto reader = std::make_unique(); + EXPECT_CALL(*reader, Read) + .WillOnce([] { + return make_ready_future(ReadResponse(ReadPayload( + std::string("The quick brown fox jumps over the lazy dog")))); + }) + .WillOnce([] { return make_ready_future(ReadResponse(Status{})); }); + return reader; + }); + + auto tested = ObjectDescriptor(mock); + EXPECT_FALSE(tested.metadata().has_value()); + AsyncReader reader; + AsyncToken token; + std::tie(reader, token) = tested.Read(100, 200); + ASSERT_TRUE(token.valid()); + + auto r1 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r1); + ReadPayload payload; + std::tie(payload, token) = *std::move(r1); + EXPECT_THAT(payload.contents(), + ElementsAre("The quick brown fox jumps over the lazy dog")); + + auto r2 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r2); + std::tie(payload, token) = *std::move(r2); + EXPECT_FALSE(token.valid()); +} + +TEST(ObjectDescriptor, ReadFromOffset) { + auto mock = std::make_shared(); + EXPECT_CALL(*mock, metadata).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(*mock, Read) + .WillOnce([](ReadParams p) -> std::unique_ptr { + EXPECT_EQ(p.start, 10); + EXPECT_EQ(p.length, 0); + auto reader = std::make_unique(); + EXPECT_CALL(*reader, Read) + .WillOnce([] { + auto response = + std::string("The quick brown fox jumps over the lazy dog") + .substr(10); + return make_ready_future(ReadResponse(ReadPayload(response))); + }) + .WillOnce([] { return make_ready_future(ReadResponse(Status{})); }); + return reader; + }); + + auto tested = ObjectDescriptor(mock); + EXPECT_FALSE(tested.metadata().has_value()); + AsyncReader reader; + AsyncToken token; + std::tie(reader, token) = tested.ReadFromOffset(10); + ASSERT_TRUE(token.valid()); + + auto r1 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r1); + ReadPayload payload; + std::tie(payload, token) = *std::move(r1); + EXPECT_THAT(payload.contents(), + ElementsAre("brown fox jumps over the lazy dog")); + + auto r2 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r2); + std::tie(payload, token) = *std::move(r2); + EXPECT_FALSE(token.valid()); +} + +TEST(ObjectDescriptor, ReadLast) { + auto mock = std::make_shared(); + EXPECT_CALL(*mock, metadata).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(*mock, Read) + .WillOnce([](ReadParams p) -> std::unique_ptr { + EXPECT_EQ(p.start, -15); + EXPECT_EQ(p.length, 0); + auto reader = std::make_unique(); + EXPECT_CALL(*reader, Read) + .WillOnce([] { + std::string full_response( + "The quick brown fox jumps over the lazy dog"); + auto response = full_response.substr(full_response.size() - 15); + return make_ready_future(ReadResponse(ReadPayload(response))); + }) + .WillOnce([] { return make_ready_future(ReadResponse(Status{})); }); + return reader; + }); + + auto tested = ObjectDescriptor(mock); + EXPECT_FALSE(tested.metadata().has_value()); + AsyncReader reader; + AsyncToken token; + std::tie(reader, token) = tested.ReadLast(15); + ASSERT_TRUE(token.valid()); + + auto r1 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r1); + ReadPayload payload; + std::tie(payload, token) = *std::move(r1); + EXPECT_THAT(payload.contents(), ElementsAre("er the lazy dog")); + + auto r2 = reader.Read(std::move(token)).get(); + ASSERT_STATUS_OK(r2); + std::tie(payload, token) = *std::move(r2); + EXPECT_FALSE(token.valid()); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_experimental +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/async/options.h b/google/cloud/storage/async/options.h index f4d5dfe268afe..31f63dc083ced 100644 --- a/google/cloud/storage/async/options.h +++ b/google/cloud/storage/async/options.h @@ -73,6 +73,19 @@ struct UseMD5ValueOption { using Type = std::string; }; +/** + * Use this option to limit the size of `ObjectDescriptor::Read()` requests. + * + * @par Example + * @code + * auto client = gcs::AsyncClient(gcs::MakeAsyncConnection( + * Options{}.set(128L * 1024 * 1024))); + * @endcode + */ +struct MaximumRangeSizeOption { + using Type = std::uint64_t; +}; + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_experimental } // namespace cloud diff --git a/google/cloud/storage/async/writer.cc b/google/cloud/storage/async/writer.cc index 532c4111f7c91..18f825e6f6670 100644 --- a/google/cloud/storage/async/writer.cc +++ b/google/cloud/storage/async/writer.cc @@ -86,6 +86,12 @@ future> AsyncWriter::Finalize( return Finalize(std::move(token), WritePayload{}); } +future AsyncWriter::Close() { + return impl_->Flush(WritePayload{}).then([impl = impl_](auto f) { + return f.get(); + }); +} + RpcMetadata AsyncWriter::GetRequestMetadata() const { return impl_->GetRequestMetadata(); } diff --git a/google/cloud/storage/async/writer.h b/google/cloud/storage/async/writer.h index 216ef9bc15f5c..9bf5740a12777 100644 --- a/google/cloud/storage/async/writer.h +++ b/google/cloud/storage/async/writer.h @@ -120,6 +120,11 @@ class AsyncWriter { future> Finalize(AsyncToken token, WritePayload payload); + /** + * Close the upload by flushing the remaining data in buffer. + */ + future Close(); + /** * The headers (if any) returned by the service. For debugging only. * diff --git a/google/cloud/storage/examples/storage_async_samples.cc b/google/cloud/storage/examples/storage_async_samples.cc index 4be1ec4744e4e..bc174717e0789 100644 --- a/google/cloud/storage/examples/storage_async_samples.cc +++ b/google/cloud/storage/examples/storage_async_samples.cc @@ -156,6 +156,48 @@ void InsertObjectVectorVectors( } #if GOOGLE_CLOUD_CPP_HAVE_COROUTINES +void OpenObject(google::cloud::storage_experimental::AsyncClient& client, + std::vector const& argv) { + //! [open-object] + namespace gcs_ex = google::cloud::storage_experimental; + // Helper coroutine, count lines returned by a AsyncReader + auto count_newlines = + [](gcs_ex::AsyncReader reader, + gcs_ex::AsyncToken token) -> google::cloud::future { + auto count = std::uint64_t{0}; + while (token.valid()) { + auto [payload, t] = (co_await reader.Read(std::move(token))).value(); + token = std::move(t); + for (auto const& buffer : payload.contents()) { + count += std::count(buffer.begin(), buffer.end(), '\n'); + } + } + co_return count; + }; + + auto coro = + [&count_newlines]( + gcs_ex::AsyncClient& client, std::string bucket_name, + std::string object_name) -> google::cloud::future { + auto descriptor = + (co_await client.Open(gcs_ex::BucketName(std::move(bucket_name)), + std::move(object_name))) + .value(); + + auto [r1, t1] = descriptor.Read(1000, 1000); + auto [r2, t2] = descriptor.Read(2000, 1000); + + auto c1 = count_newlines(std::move(r1), std::move(t1)); + auto c2 = count_newlines(std::move(r2), std::move(t2)); + co_return (co_await std::move(c1)) + (co_await std::move(c2)); + }; + //! [open-object] + // The example is easier to test and run if we call the coroutine and block + // until it completes. + auto const count = coro(client, argv.at(0), argv.at(1)).get(); + std::cout << "The ranges contain " << count << " newlines\n"; +} + void ReadObject(google::cloud::storage_experimental::AsyncClient& client, std::vector const& argv) { //! [read-object] @@ -494,6 +536,34 @@ void ResumeUnbufferedUpload( std::cout << "Object successfully uploaded " << object.DebugString() << "\n"; } +void StartAppendableObjectUpload( + google::cloud::storage_experimental::AsyncClient& client, + std::vector const& argv) { + //! [start-appendable-object-upload] + namespace gcs = google::cloud::storage; + namespace gcs_ex = google::cloud::storage_experimental; + auto coro = [](gcs_ex::AsyncClient& client, std::string bucket_name, + std::string object_name) + -> google::cloud::future { + auto [writer, token] = (co_await client.StartAppendableObjectUpload( + gcs_ex::BucketName(std::move(bucket_name)), + std::move(object_name))) + .value(); + for (int i = 0; i != 1000; ++i) { + auto line = gcs_ex::WritePayload(std::vector{ + std::string("line number "), std::to_string(i), std::string("\n")}); + token = + (co_await writer.Write(std::move(token), std::move(line))).value(); + } + co_return (co_await writer.Finalize(std::move(token))).value(); + }; + //! [start-appendable-object-upload] + // The example is easier to test and run if we call the coroutine and block + // until it completes.. + auto const object = coro(client, argv.at(0), argv.at(1)).get(); + std::cout << "File successfully uploaded " << object.DebugString() << "\n"; +} + void RewriteObject(google::cloud::storage_experimental::AsyncClient& client, std::vector const& argv) { //! [rewrite-object] @@ -583,6 +653,11 @@ void ResumeRewrite(google::cloud::storage_experimental::AsyncClient& client, } #else +void OpenObject(google::cloud::storage_experimental::AsyncClient&, + std::vector const&) { + std::cerr << "AsyncClient::Open() example requires coroutines\n"; +} + void ReadObject(google::cloud::storage_experimental::AsyncClient&, std::vector const&) { std::cerr << "AsyncClient::ReadObject() example requires coroutines\n"; @@ -644,6 +719,13 @@ void ResumeUnbufferedUpload(google::cloud::storage_experimental::AsyncClient&, << "AsyncClient::ResumeUnbufferedUpload() example requires coroutines\n"; } +void StartAppendableObjectUpload( + google::cloud::storage_experimental::AsyncClient&, + std::vector const&) { + std::cerr << "AsyncClient::StartAppendableObjectUpload() example requires " + "coroutines\n"; +} + void RewriteObject(google::cloud::storage_experimental::AsyncClient&, std::vector const&) { std::cerr << "AsyncClient::RewriteObject() example requires coroutines\n"; @@ -819,6 +901,9 @@ void AutoRun(std::vector const& argv) { scheduled_for_delete.push_back(std::move(object_name)); object_name = examples::MakeRandomObjectName(generator, "object-"); + std::cout << "Running the OpenObject() example" << std::endl; + OpenObject(client, {bucket_name, composed_name}); + std::cout << "Running the ReadObject() example" << std::endl; ReadObject(client, {bucket_name, composed_name}); @@ -979,6 +1064,7 @@ int main(int argc, char* argv[]) try { make_entry("insert-object-vector", {}, InsertObjectVector), make_entry("insert-object-vector-strings", {}, InsertObjectVectorStrings), make_entry("insert-object-vector-vectors", {}, InsertObjectVectorVectors), + make_entry("open-object", {}, OpenObject), make_entry("read-object", {}, ReadObject), make_entry("read-all", {}, ReadAll), make_entry("read-object-range", {}, ReadObjectRange), @@ -998,6 +1084,9 @@ int main(int argc, char* argv[]) try { make_resume_entry("resume-unbuffered-upload", {""}, ResumeUnbufferedUpload), + make_entry("start-appendable-object-upload", {}, + StartAppendableObjectUpload), + make_entry("rewrite-object", {""}, RewriteObject), make_entry("resume-rewrite-object", {""}, ResumeRewrite), {"auto", AutoRun}, diff --git a/google/cloud/storage/examples/storage_object_samples.cc b/google/cloud/storage/examples/storage_object_samples.cc index a7ee4e756860c..ff2a241cc6cc4 100644 --- a/google/cloud/storage/examples/storage_object_samples.cc +++ b/google/cloud/storage/examples/storage_object_samples.cc @@ -322,8 +322,8 @@ void ReadObjectIntoMemory(google::cloud::storage::Client client, [](gcs::Client client, std::string const& bucket_name, std::string const& object_name) { gcs::ObjectReadStream stream = client.ReadObject(bucket_name, object_name); - std::string buffer{std::istream_iterator(stream), - std::istream_iterator()}; + std::string buffer{std::istreambuf_iterator(stream), + std::istreambuf_iterator()}; if (stream.bad()) throw google::cloud::Status(stream.status()); std::cout << "The object has " << buffer.size() << " characters\n"; @@ -340,7 +340,7 @@ void ReadObjectGzip(google::cloud::storage::Client client, std::string const& object_name) { auto is = client.ReadObject(bucket_name, object_name, gcs::AcceptEncodingGzip()); - auto const contents = std::string{std::istream_iterator(is), {}}; + auto const contents = std::string{std::istreambuf_iterator(is), {}}; if (is.bad()) throw google::cloud::Status(is.status()); std::cout << "The object has " << contents.size() << " characters\n"; } diff --git a/google/cloud/storage/google_cloud_cpp_storage_grpc.bzl b/google/cloud/storage/google_cloud_cpp_storage_grpc.bzl index 5b6397a0a1e1a..2ceccbd9b2b57 100644 --- a/google/cloud/storage/google_cloud_cpp_storage_grpc.bzl +++ b/google/cloud/storage/google_cloud_cpp_storage_grpc.bzl @@ -21,6 +21,8 @@ google_cloud_cpp_storage_grpc_hdrs = [ "async/client.h", "async/connection.h", "async/idempotency_policy.h", + "async/object_descriptor.h", + "async/object_descriptor_connection.h", "async/object_responses.h", "async/options.h", "async/read_all.h", @@ -38,10 +40,18 @@ google_cloud_cpp_storage_grpc_hdrs = [ "internal/async/connection_impl.h", "internal/async/connection_tracing.h", "internal/async/default_options.h", + "internal/async/handle_redirect_error.h", "internal/async/insert_object.h", + "internal/async/object_descriptor_connection_tracing.h", + "internal/async/object_descriptor_impl.h", + "internal/async/object_descriptor_reader.h", + "internal/async/object_descriptor_reader_tracing.h", + "internal/async/open_object.h", + "internal/async/open_stream.h", "internal/async/partial_upload.h", "internal/async/read_payload_fwd.h", "internal/async/read_payload_impl.h", + "internal/async/read_range.h", "internal/async/reader_connection_factory.h", "internal/async/reader_connection_impl.h", "internal/async/reader_connection_resume.h", @@ -49,11 +59,13 @@ google_cloud_cpp_storage_grpc_hdrs = [ "internal/async/rewriter_connection_impl.h", "internal/async/rewriter_connection_tracing.h", "internal/async/token_impl.h", + "internal/async/write_object.h", "internal/async/write_payload_fwd.h", "internal/async/write_payload_impl.h", "internal/async/writer_connection_buffered.h", "internal/async/writer_connection_finalized.h", "internal/async/writer_connection_impl.h", + "internal/async/writer_connection_resumed.h", "internal/async/writer_connection_tracing.h", "internal/grpc/bucket_access_control_parser.h", "internal/grpc/bucket_metadata_parser.h", @@ -94,6 +106,7 @@ google_cloud_cpp_storage_grpc_srcs = [ "async/bucket_name.cc", "async/client.cc", "async/idempotency_policy.cc", + "async/object_descriptor.cc", "async/object_responses.cc", "async/read_all.cc", "async/reader.cc", @@ -104,8 +117,16 @@ google_cloud_cpp_storage_grpc_srcs = [ "internal/async/connection_impl.cc", "internal/async/connection_tracing.cc", "internal/async/default_options.cc", + "internal/async/handle_redirect_error.cc", "internal/async/insert_object.cc", + "internal/async/object_descriptor_connection_tracing.cc", + "internal/async/object_descriptor_impl.cc", + "internal/async/object_descriptor_reader.cc", + "internal/async/object_descriptor_reader_tracing.cc", + "internal/async/open_object.cc", + "internal/async/open_stream.cc", "internal/async/partial_upload.cc", + "internal/async/read_range.cc", "internal/async/reader_connection_factory.cc", "internal/async/reader_connection_impl.cc", "internal/async/reader_connection_resume.cc", @@ -113,9 +134,11 @@ google_cloud_cpp_storage_grpc_srcs = [ "internal/async/rewriter_connection_impl.cc", "internal/async/rewriter_connection_tracing.cc", "internal/async/token_impl.cc", + "internal/async/write_object.cc", "internal/async/writer_connection_buffered.cc", "internal/async/writer_connection_finalized.cc", "internal/async/writer_connection_impl.cc", + "internal/async/writer_connection_resumed.cc", "internal/async/writer_connection_tracing.cc", "internal/grpc/bucket_access_control_parser.cc", "internal/grpc/bucket_metadata_parser.cc", diff --git a/google/cloud/storage/google_cloud_cpp_storage_grpc.cmake b/google/cloud/storage/google_cloud_cpp_storage_grpc.cmake index f1f1b62806a79..be93ae5821e72 100644 --- a/google/cloud/storage/google_cloud_cpp_storage_grpc.cmake +++ b/google/cloud/storage/google_cloud_cpp_storage_grpc.cmake @@ -75,6 +75,9 @@ add_library( async/connection.h async/idempotency_policy.cc async/idempotency_policy.h + async/object_descriptor.cc + async/object_descriptor.h + async/object_descriptor_connection.h async/object_responses.cc async/object_responses.h async/options.h @@ -102,12 +105,28 @@ add_library( internal/async/connection_tracing.h internal/async/default_options.cc internal/async/default_options.h + internal/async/handle_redirect_error.cc + internal/async/handle_redirect_error.h internal/async/insert_object.cc internal/async/insert_object.h + internal/async/object_descriptor_connection_tracing.cc + internal/async/object_descriptor_connection_tracing.h + internal/async/object_descriptor_impl.cc + internal/async/object_descriptor_impl.h + internal/async/object_descriptor_reader.cc + internal/async/object_descriptor_reader.h + internal/async/object_descriptor_reader_tracing.cc + internal/async/object_descriptor_reader_tracing.h + internal/async/open_object.cc + internal/async/open_object.h + internal/async/open_stream.cc + internal/async/open_stream.h internal/async/partial_upload.cc internal/async/partial_upload.h internal/async/read_payload_fwd.h internal/async/read_payload_impl.h + internal/async/read_range.cc + internal/async/read_range.h internal/async/reader_connection_factory.cc internal/async/reader_connection_factory.h internal/async/reader_connection_impl.cc @@ -122,6 +141,8 @@ add_library( internal/async/rewriter_connection_tracing.h internal/async/token_impl.cc internal/async/token_impl.h + internal/async/write_object.cc + internal/async/write_object.h internal/async/write_payload_fwd.h internal/async/write_payload_impl.h internal/async/writer_connection_buffered.cc @@ -130,6 +151,8 @@ add_library( internal/async/writer_connection_finalized.h internal/async/writer_connection_impl.cc internal/async/writer_connection_impl.h + internal/async/writer_connection_resumed.cc + internal/async/writer_connection_resumed.h internal/async/writer_connection_tracing.cc internal/async/writer_connection_tracing.h internal/grpc/bucket_access_control_parser.cc @@ -315,6 +338,7 @@ if (GOOGLE_CLOUD_CPP_WITH_MOCKS) set(google_cloud_cpp_storage_grpc_mocks_hdrs # cmake-format: sort mocks/mock_async_connection.h + mocks/mock_async_object_descriptor_connection.h mocks/mock_async_reader_connection.h mocks/mock_async_rewriter_connection.h mocks/mock_async_writer_connection.h) @@ -396,6 +420,7 @@ set(storage_client_grpc_unit_tests async/bucket_name_test.cc async/client_test.cc async/idempotency_policy_test.cc + async/object_descriptor_test.cc async/read_all_test.cc async/reader_test.cc async/resume_policy_test.cc @@ -404,6 +429,7 @@ set(storage_client_grpc_unit_tests async/writer_test.cc grpc_plugin_test.cc internal/async/connection_impl_insert_test.cc + internal/async/connection_impl_open_test.cc internal/async/connection_impl_read_hash_test.cc internal/async/connection_impl_read_test.cc internal/async/connection_impl_test.cc @@ -412,18 +438,27 @@ set(storage_client_grpc_unit_tests internal/async/connection_tracing_test.cc internal/async/default_options_test.cc internal/async/insert_object_test.cc + internal/async/object_descriptor_connection_tracing_test.cc + internal/async/object_descriptor_impl_test.cc + internal/async/object_descriptor_reader_test.cc + internal/async/object_descriptor_reader_tracing_test.cc + internal/async/open_object_test.cc + internal/async/open_stream_test.cc internal/async/partial_upload_test.cc internal/async/read_payload_impl_test.cc + internal/async/read_range_test.cc internal/async/reader_connection_factory_test.cc internal/async/reader_connection_impl_test.cc internal/async/reader_connection_resume_test.cc internal/async/reader_connection_tracing_test.cc internal/async/rewriter_connection_impl_test.cc internal/async/rewriter_connection_tracing_test.cc + internal/async/write_object_test.cc internal/async/write_payload_impl_test.cc internal/async/writer_connection_buffered_test.cc internal/async/writer_connection_finalized_test.cc internal/async/writer_connection_impl_test.cc + internal/async/writer_connection_resumed_test.cc internal/async/writer_connection_tracing_test.cc internal/grpc/bucket_access_control_parser_test.cc internal/grpc/bucket_metadata_parser_test.cc diff --git a/google/cloud/storage/google_cloud_cpp_storage_grpc_mocks.bzl b/google/cloud/storage/google_cloud_cpp_storage_grpc_mocks.bzl index 74c68da336f44..80ee8b14323b5 100644 --- a/google/cloud/storage/google_cloud_cpp_storage_grpc_mocks.bzl +++ b/google/cloud/storage/google_cloud_cpp_storage_grpc_mocks.bzl @@ -18,6 +18,7 @@ google_cloud_cpp_storage_grpc_mocks_hdrs = [ "mocks/mock_async_connection.h", + "mocks/mock_async_object_descriptor_connection.h", "mocks/mock_async_reader_connection.h", "mocks/mock_async_rewriter_connection.h", "mocks/mock_async_writer_connection.h", diff --git a/google/cloud/storage/internal/async/connection_impl.cc b/google/cloud/storage/internal/async/connection_impl.cc index d71b4a5e3e621..37b607c50933c 100644 --- a/google/cloud/storage/internal/async/connection_impl.cc +++ b/google/cloud/storage/internal/async/connection_impl.cc @@ -19,15 +19,21 @@ #include "google/cloud/storage/async/reader.h" #include "google/cloud/storage/async/resume_policy.h" #include "google/cloud/storage/internal/async/default_options.h" +#include "google/cloud/storage/internal/async/handle_redirect_error.h" #include "google/cloud/storage/internal/async/insert_object.h" +#include "google/cloud/storage/internal/async/object_descriptor_impl.h" +#include "google/cloud/storage/internal/async/open_object.h" +#include "google/cloud/storage/internal/async/open_stream.h" #include "google/cloud/storage/internal/async/read_payload_impl.h" #include "google/cloud/storage/internal/async/reader_connection_impl.h" #include "google/cloud/storage/internal/async/reader_connection_resume.h" #include "google/cloud/storage/internal/async/rewriter_connection_impl.h" +#include "google/cloud/storage/internal/async/write_object.h" #include "google/cloud/storage/internal/async/write_payload_impl.h" #include "google/cloud/storage/internal/async/writer_connection_buffered.h" #include "google/cloud/storage/internal/async/writer_connection_finalized.h" #include "google/cloud/storage/internal/async/writer_connection_impl.h" +#include "google/cloud/storage/internal/async/writer_connection_resumed.h" #include "google/cloud/storage/internal/crc32c.h" #include "google/cloud/storage/internal/grpc/channel_refresh.h" #include "google/cloud/storage/internal/grpc/configure_client_context.h" @@ -49,6 +55,7 @@ #include "google/cloud/internal/async_streaming_write_rpc_timeout.h" #include "google/cloud/internal/make_status.h" #include +#include namespace google { namespace cloud { @@ -183,6 +190,67 @@ future> AsyncConnectionImpl::InsertObject( std::move(current), std::move(request), __func__); } +future< + StatusOr>> +AsyncConnectionImpl::Open(OpenParams p) { + auto initial_request = google::storage::v2::BidiReadObjectRequest{}; + *initial_request.mutable_read_object_spec() = p.read_spec; + auto current = internal::MakeImmutableOptions(p.options); + // Get the policy factory and immediately create a policy. + auto resume_policy = + current->get()(); + + auto retry = std::shared_ptr(retry_policy(*current)); + auto backoff = + std::shared_ptr(backoff_policy(*current)); + auto const* function_name = __func__; + auto factory = OpenStreamFactory( + [stub = stub_, cq = cq_, retry = std::move(retry), + backoff = std::move(backoff), current = std::move(current), + function_name](google::storage::v2::BidiReadObjectRequest request) { + struct DummyRequest {}; + + auto call = [stub, request = std::move(request)]( + CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + DummyRequest const&) mutable { + auto open = std::make_shared( + *stub, cq, std::move(context), std::move(options), request); + // Extend the lifetime of the coroutine until it finishes. + return open->Call().then([open, &request](auto f) mutable { + open.reset(); + auto response = f.get(); + if (response) return response; + ApplyRedirectErrors(*request.mutable_read_object_spec(), + ExtractGrpcStatus(response.status())); + return response; + }); + }; + + return google::cloud::internal::AsyncRetryLoop( + retry->clone(), backoff->clone(), Idempotency::kIdempotent, cq, + std::move(call), current, DummyRequest{}, function_name); + }); + + auto pending = factory(std::move(initial_request)); + using ReturnType = + std::shared_ptr; + return pending.then( + [rp = std::move(resume_policy), fa = std::move(factory), + rs = std::move(p.read_spec), + options = std::move(p.options)](auto f) mutable -> StatusOr { + auto result = f.get(); + if (!result) return std::move(result).status(); + + auto impl = std::make_shared( + std::move(rp), std::move(fa), std::move(rs), + std::move(result->stream), std::move(options)); + impl->Start(std::move(result->first_response)); + return ReturnType(impl); + }); +} + future>> AsyncConnectionImpl::ReadObject(ReadObjectParams p) { using ReturnType = @@ -224,6 +292,101 @@ AsyncConnectionImpl::ReadObjectRange(ReadObjectParams p) { }); } +future>> +AsyncConnectionImpl::StartAppendableObjectUpload(AppendableUploadParams p) { + return AppendableObjectUploadImpl(std::move(p)); +} + +future>> +AsyncConnectionImpl::ResumeAppendableObjectUpload(AppendableUploadParams p) { + return AppendableObjectUploadImpl(std::move(p), true); +} + +future>> +AsyncConnectionImpl::AppendableObjectUploadImpl(AppendableUploadParams p, + bool takeover) { + auto current = internal::MakeImmutableOptions(std::move(p.options)); + auto request = p.request; + std::int64_t persisted_size = 0; + std::shared_ptr hash_function = + CreateHashFunction(*current); + auto retry = std::shared_ptr(retry_policy(*current)); + auto backoff = + std::shared_ptr(backoff_policy(*current)); + using StreamingRpcTimeout = + google::cloud::internal::AsyncStreamingReadWriteRpcTimeout< + google::storage::v2::BidiWriteObjectRequest, + google::storage::v2::BidiWriteObjectResponse>; + struct RequestPlaceholder {}; + + using WriteResultFactory = + std::function>( + google::storage::v2::BidiWriteObjectRequest)>; + + auto factory = WriteResultFactory( + [stub = stub_, cq = cq_, retry = std::move(retry), + // NOLINTNEXTLINE(bugprone-lambda-function-name) + backoff = std::move(backoff), current, function_name = __func__, + takeover](google::storage::v2::BidiWriteObjectRequest req) { + auto call = [stub, request = std::move(req), takeover]( + google::cloud::CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + RequestPlaceholder const&) mutable + -> future> { + auto timeout = ScaleStallTimeout( + options->get(), + options->get(), + google::storage::v2::ServiceConstants::MAX_WRITE_CHUNK_BYTES); + + // Apply the routing header + if (takeover) + ApplyRoutingHeaders(*context, request.append_object_spec()); + else + ApplyRoutingHeaders(*context, request.write_object_spec()); + + auto rpc = stub->AsyncBidiWriteObject(cq, std::move(context), + std::move(options)); + rpc = std::make_unique(cq, timeout, timeout, + timeout, std::move(rpc)); + request.set_state_lookup(true); + auto open = std::make_shared(std::move(rpc), request); + return open->Call().then([open, &request](auto f) mutable { + open.reset(); + auto response = f.get(); + if (!response) { + EnsureFirstMessageAppendObjectSpec(request); + ApplyWriteRedirectErrors(*request.mutable_append_object_spec(), + ExtractGrpcStatus(response.status())); + } + return response; + }); + }; + + return google::cloud::internal::AsyncRetryLoop( + retry->clone(), backoff->clone(), Idempotency::kIdempotent, cq, + std::move(call), std::move(current), RequestPlaceholder{}, + function_name); + }); + + auto pending = factory(std::move(request)); + return pending.then( + [current, request = std::move(p.request), persisted_size, + hash = std::move(hash_function), fa = std::move(factory)](auto f) mutable + -> StatusOr< + std::unique_ptr> { + auto rpc = f.get(); + if (!rpc) return std::move(rpc).status(); + persisted_size = rpc->first_response.resource().size(); + auto impl = std::make_unique( + current, request, std::move(rpc->stream), hash, persisted_size, + false); + return MakeWriterConnectionResumed(std::move(fa), std::move(impl), + std::move(request), std::move(hash), + *current); + }); +} + future>> AsyncConnectionImpl::StartUnbufferedUpload(UploadParams p) { auto current = internal::MakeImmutableOptions(std::move(p.options)); @@ -603,7 +766,7 @@ AsyncConnectionImpl::UnbufferedUploadImpl( return std::unique_ptr( std::make_unique( current, std::move(request), *std::move(rpc), std::move(hash), - persisted_size)); + persisted_size, true)); }; auto retry = retry_policy(*current); diff --git a/google/cloud/storage/internal/async/connection_impl.h b/google/cloud/storage/internal/async/connection_impl.h index b7a8967b7427e..61b2e6b928042 100644 --- a/google/cloud/storage/internal/async/connection_impl.h +++ b/google/cloud/storage/internal/async/connection_impl.h @@ -16,6 +16,7 @@ #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_CONNECTION_IMPL_H #include "google/cloud/storage/async/connection.h" +#include "google/cloud/storage/async/object_descriptor_connection.h" #include "google/cloud/storage/async/reader_connection.h" #include "google/cloud/storage/async/rewriter_connection.h" #include "google/cloud/storage/idempotency_policy.h" @@ -23,6 +24,7 @@ #include "google/cloud/storage/internal/hash_function.h" #include "google/cloud/storage/options.h" #include "google/cloud/storage/retry_policy.h" +#include "google/cloud/async_streaming_read_write_rpc.h" #include "google/cloud/completion_queue.h" #include "google/cloud/future.h" #include "google/cloud/internal/invocation_id_generator.h" @@ -58,12 +60,22 @@ class AsyncConnectionImpl future> InsertObject( InsertObjectParams p) override; + future>> + Open(OpenParams p) override; + future>> ReadObject(ReadObjectParams p) override; future> ReadObjectRange( ReadObjectParams p) override; + future>> + StartAppendableObjectUpload(AppendableUploadParams p) override; + + future>> + ResumeAppendableObjectUpload(AppendableUploadParams p) override; + future>> StartUnbufferedUpload(UploadParams p) override; @@ -128,6 +140,9 @@ class AsyncConnectionImpl std::shared_ptr hash_function, std::int64_t persisted_size); + future>> + AppendableObjectUploadImpl(AppendableUploadParams p, bool takeover = false); + CompletionQueue cq_; std::shared_ptr refresh_; std::shared_ptr stub_; diff --git a/google/cloud/storage/internal/async/connection_impl_open_test.cc b/google/cloud/storage/internal/async/connection_impl_open_test.cc new file mode 100644 index 0000000000000..9e2e4c49d182b --- /dev/null +++ b/google/cloud/storage/internal/async/connection_impl_open_test.cc @@ -0,0 +1,389 @@ +// Copyright 2024 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/mocks/mock_async_streaming_read_write_rpc.h" +#include "google/cloud/storage/internal/async/connection_impl.h" +#include "google/cloud/storage/internal/async/default_options.h" +#include "google/cloud/storage/internal/grpc/ctype_cord_workaround.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_resume_policy.h" +#include "google/cloud/storage/testing/mock_storage_stub.h" +#include "google/cloud/common_options.h" +#include "google/cloud/grpc_options.h" +#include "google/cloud/internal/background_threads_impl.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/mock_completion_queue_impl.h" +#include "google/cloud/testing_util/status_matchers.h" +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::storage::testing::canonical_errors::TransientError; +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::IsOkAndHolds; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::MockCompletionQueueImpl; +using ::google::cloud::testing_util::StatusIs; +using ::google::protobuf::TextFormat; +using ::testing::NotNull; +using ::testing::Optional; + +using BidiReadStream = google::cloud::AsyncStreamingReadWriteRpc< + google::storage::v2::BidiReadObjectRequest, + google::storage::v2::BidiReadObjectResponse>; +using MockStream = google::cloud::mocks::MockAsyncStreamingReadWriteRpc< + google::storage::v2::BidiReadObjectRequest, + google::storage::v2::BidiReadObjectResponse>; + +auto constexpr kAuthority = "storage.googleapis.com"; +auto constexpr kRetryAttempts = 2; + +auto TestOptions(Options options = {}) { + using ms = std::chrono::milliseconds; + options = internal::MergeOptions( + std::move(options), + Options{} + .set(1) + // By default, disable timeouts, most tests are simpler without them. + .set(std::chrono::seconds(0)) + // By default, disable resumes, most tests are simpler without them. + .set( + storage_experimental::LimitedErrorCountResumePolicy(0)) + .set( + storage::LimitedErrorCountRetryPolicy(kRetryAttempts).clone()) + .set( + storage::ExponentialBackoffPolicy(ms(1), ms(2), 2.0).clone())); + return DefaultOptionsAsync(std::move(options)); +} + +std::unique_ptr MakeErrorStream(AsyncSequencer& sequencer, + Status const& status) { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&] { + return sequencer.PushBack("Start").then([](auto) { return false; }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&, status] { + return sequencer.PushBack("Finish").then([status](auto) { return status; }); + }); + EXPECT_CALL(*stream, Cancel).WillRepeatedly([] {}); + return std::unique_ptr(std::move(stream)); +} + +Status RedirectError(absl::string_view handle, absl::string_view token) { + auto make_details = [&] { + auto redirected = google::storage::v2::BidiReadObjectRedirectedError{}; + redirected.mutable_read_handle()->set_handle(std::string(handle)); + redirected.set_routing_token(std::string(token)); + auto details_proto = google::rpc::Status{}; + details_proto.set_code(grpc::UNAVAILABLE); + details_proto.set_message("redirect"); + details_proto.add_details()->PackFrom(redirected); + + std::string details; + details_proto.SerializeToString(&details); + return details; + }; + + return google::cloud::MakeStatusFromRpcError( + grpc::Status(grpc::UNAVAILABLE, "redirect", make_details())); +} + +// Verify we can open a stream, without retries, timeouts, or any other +// difficulties. This test does not read any data. +TEST(AsyncConnectionImplTest, OpenSimple) { + auto constexpr kExpectedRequest = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 42 + if_metageneration_match: 7 + )pb"; + auto constexpr kMetadataText = R"pb( + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + )pb"; + + AsyncSequencer sequencer; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, AsyncBidiReadObject) + .WillOnce([&](CompletionQueue const&, + std::shared_ptr const&, + google::cloud::internal::ImmutableOptions const& options) { + // Verify at least one option is initialized with the correct value. + EXPECT_EQ(options->get(), kAuthority); + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write) + .WillOnce( + [=, &sequencer]( + google::storage::v2::BidiReadObjectRequest const& request, + grpc::WriteOptions) { + auto expected = google::storage::v2::BidiReadObjectRequest{}; + EXPECT_TRUE(TextFormat::ParseFromString( + kExpectedRequest, expected.mutable_read_object_spec())); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Read) + .WillOnce([&]() { + return sequencer.PushBack("Read").then( + [=](auto f) -> absl::optional< + google::storage::v2::BidiReadObjectResponse> { + if (!f.get()) return absl::nullopt; + auto constexpr kHandleText = R"pb( + handle: "handle-12345" + )pb"; + auto response = + google::storage::v2::BidiReadObjectResponse{}; + EXPECT_TRUE(TextFormat::ParseFromString( + kMetadataText, response.mutable_metadata())); + EXPECT_TRUE(TextFormat::ParseFromString( + kHandleText, response.mutable_read_handle())); + return response; + }); + }) + .WillOnce([&sequencer]() { + return sequencer.PushBack("Read[N]").then( + [](auto f) -> absl::optional< + google::storage::v2::BidiReadObjectResponse> { + if (!f.get()) return absl::nullopt; + return google::storage::v2::BidiReadObjectResponse{}; + }); + }); + EXPECT_CALL(*stream, Cancel).WillOnce([&sequencer]() { + (void)sequencer.PushBack("Cancel"); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return Status{}; }); + }); + + return std::unique_ptr(std::move(stream)); + }); + + auto mock_cq = std::make_shared(); + auto connection = std::make_shared( + CompletionQueue(mock_cq), std::shared_ptr(), mock, + TestOptions()); + + auto request = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &request)); + auto pending = connection->Open({std::move(request), connection->options()}); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(true); + + auto p = pending.get(); + ASSERT_THAT(p, IsOkAndHolds(NotNull())); + auto descriptor = *std::move(p); + + auto expected_metadata = google::storage::v2::Object{}; + EXPECT_TRUE(TextFormat::ParseFromString(kMetadataText, &expected_metadata)); + EXPECT_THAT(descriptor->metadata(), + Optional(IsProtoEqual(expected_metadata))); + + // Deleting the descriptor should cancel the stream, and start the background + // operations to call `Finish()`. + descriptor.reset(); + + auto last_read = sequencer.PopFrontWithName(); + EXPECT_EQ(last_read.second, "Read[N]"); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Cancel"); + next.first.set_value(true); + last_read.first.set_value(false); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); +} + +TEST(AsyncConnectionImplTest, HandleRedirectErrors) { + auto constexpr kExpectedRequest0 = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + auto constexpr kExpectedRequestN = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + read_handle { handle: "test-read-handle" } + routing_token: "test-routing-token" + )pb"; + + AsyncSequencer sequencer; + auto make_redirect_stream = [&sequencer](char const* expected_text) { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&] { + return sequencer.PushBack("Start").then([](auto) { return true; }); + }); + EXPECT_CALL(*stream, Write) + .WillOnce([&sequencer, expected_text]( + google::storage::v2::BidiReadObjectRequest const& r, + grpc::WriteOptions) { + auto expected = google::storage::v2::BidiReadObjectSpec{}; + EXPECT_TRUE(TextFormat::ParseFromString(expected_text, &expected)); + EXPECT_THAT(r.read_object_spec(), IsProtoEqual(expected)); + return sequencer.PushBack("Write").then([](auto) { return true; }); + }); + EXPECT_CALL(*stream, Read).WillOnce([&] { + return sequencer.PushBack("Read").then([](auto) { + return absl::optional(); + }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&] { + return sequencer.PushBack("Finish").then([](auto) { + return RedirectError("test-read-handle", "test-routing-token"); + }); + }); + EXPECT_CALL(*stream, Cancel).WillRepeatedly([] {}); + return std::unique_ptr(std::move(stream)); + }; + + auto mock = std::make_shared(); + EXPECT_CALL(*mock, AsyncBidiReadObject) + .WillOnce([&] { return make_redirect_stream(kExpectedRequest0); }) + .WillOnce([&] { return make_redirect_stream(kExpectedRequestN); }) + .WillOnce([&] { return make_redirect_stream(kExpectedRequestN); }); + + // Easier to just use a real CQ vs. mock its behavior. + internal::AutomaticallyCreatedBackgroundThreads pool(1); + auto connection = std::make_shared( + pool.cq(), std::shared_ptr(), mock, TestOptions()); + + auto request = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kExpectedRequest0, &request)); + auto pending = connection->Open({std::move(request), connection->options()}); + + for (int i = 0; i != kRetryAttempts + 1; ++i) { + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(); + } + + ASSERT_THAT(pending.get(), StatusIs(StatusCode::kUnavailable)); +} + +TEST(AsyncConnectionImplTest, StopOnPermanentError) { + auto constexpr kExpectedRequest = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + + AsyncSequencer sequencer; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, AsyncBidiReadObject).WillOnce([&]() { + return MakeErrorStream(sequencer, PermanentError()); + }); + + auto mock_cq = std::make_shared(); + auto connection = std::make_shared( + CompletionQueue(mock_cq), std::shared_ptr(), mock, + TestOptions()); + + auto request = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &request)); + auto pending = connection->Open({std::move(request), connection->options()}); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(); + + auto response = pending.get(); + ASSERT_THAT(response, StatusIs(PermanentError().code())); +} + +TEST(AsyncConnectionImplTest, TooManyTransienErrors) { + auto constexpr kExpectedRequest = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + + AsyncSequencer sequencer; + auto mock = std::make_shared(); + EXPECT_CALL(*mock, AsyncBidiReadObject) + .Times(kRetryAttempts + 1) + .WillRepeatedly( + [&]() { return MakeErrorStream(sequencer, TransientError()); }); + + // Easier to just use a real CQ vs. mock its behavior. + internal::AutomaticallyCreatedBackgroundThreads pool(1); + auto connection = std::make_shared( + pool.cq(), std::shared_ptr(), mock, TestOptions()); + + auto request = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &request)); + auto pending = connection->Open({std::move(request), connection->options()}); + + for (int i = 0; i != kRetryAttempts + 1; ++i) { + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(); + } + + ASSERT_THAT(pending.get(), StatusIs(TransientError().code())); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/connection_tracing.cc b/google/cloud/storage/internal/async/connection_tracing.cc index a5a3762ce269d..66b89607f4031 100644 --- a/google/cloud/storage/internal/async/connection_tracing.cc +++ b/google/cloud/storage/internal/async/connection_tracing.cc @@ -14,6 +14,7 @@ #include "google/cloud/storage/internal/async/connection_tracing.h" #include "google/cloud/storage/async/writer_connection.h" +#include "google/cloud/storage/internal/async/object_descriptor_connection_tracing.h" #include "google/cloud/storage/internal/async/reader_connection_tracing.h" #include "google/cloud/storage/internal/async/rewriter_connection_tracing.h" #include "google/cloud/storage/internal/async/writer_connection_tracing.h" @@ -46,6 +47,26 @@ class AsyncConnectionTracing : public storage_experimental::AsyncConnection { impl_->InsertObject(std::move(p))); } + future>> + Open(OpenParams p) override { + auto span = internal::MakeSpan("storage::AsyncConnection::Open"); + internal::OTelScope scope(span); + return impl_->Open(std::move(p)) + .then([oc = opentelemetry::context::RuntimeContext::GetCurrent(), + span = std::move(span)](auto f) + -> StatusOr> { + auto result = f.get(); + internal::DetachOTelContext(oc); + if (!result) { + return internal::EndSpan(*span, std::move(result).status()); + } + return MakeTracingObjectDescriptorConnection(std::move(span), + *std::move(result)); + }); + } + future>> ReadObject(ReadObjectParams p) override { auto span = internal::MakeSpan("storage::AsyncConnection::ReadObject"); @@ -75,6 +96,40 @@ class AsyncConnectionTracing : public storage_experimental::AsyncConnection { }); } + future>> + StartAppendableObjectUpload(AppendableUploadParams p) override { + auto span = internal::MakeSpan( + "storage::AsyncConnection::StartAppendableObjectUpload"); + internal::OTelScope scope(span); + return impl_->StartAppendableObjectUpload(std::move(p)) + .then([oc = opentelemetry::context::RuntimeContext::GetCurrent(), + span = std::move(span)](auto f) + -> StatusOr> { + auto w = f.get(); + internal::DetachOTelContext(oc); + if (!w) return internal::EndSpan(*span, std::move(w).status()); + return MakeTracingWriterConnection(span, *std::move(w)); + }); + } + + future>> + ResumeAppendableObjectUpload(AppendableUploadParams p) override { + auto span = internal::MakeSpan( + "storage::AsyncConnection::ResumeAppendableObjectUpload"); + internal::OTelScope scope(span); + return impl_->ResumeAppendableObjectUpload(std::move(p)) + .then([oc = opentelemetry::context::RuntimeContext::GetCurrent(), + span = std::move(span)](auto f) + -> StatusOr> { + auto w = f.get(); + internal::DetachOTelContext(oc); + if (!w) return internal::EndSpan(*span, std::move(w).status()); + return MakeTracingWriterConnection(span, *std::move(w)); + }); + } + future>> StartUnbufferedUpload(UploadParams p) override { auto span = diff --git a/google/cloud/storage/internal/async/connection_tracing_test.cc b/google/cloud/storage/internal/async/connection_tracing_test.cc index c2d37c656f6f0..72bbfaaddf676 100644 --- a/google/cloud/storage/internal/async/connection_tracing_test.cc +++ b/google/cloud/storage/internal/async/connection_tracing_test.cc @@ -15,8 +15,10 @@ #ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY #include "google/cloud/storage/internal/async/connection_tracing.h" +#include "google/cloud/storage/async/object_descriptor_connection.h" #include "google/cloud/storage/async/reader_connection.h" #include "google/cloud/storage/mocks/mock_async_connection.h" +#include "google/cloud/storage/mocks/mock_async_object_descriptor_connection.h" #include "google/cloud/storage/mocks/mock_async_reader_connection.h" #include "google/cloud/storage/mocks/mock_async_rewriter_connection.h" #include "google/cloud/storage/mocks/mock_async_writer_connection.h" @@ -34,7 +36,9 @@ namespace { using ::google::cloud::storage::testing::canonical_errors::PermanentError; using ::google::cloud::storage_experimental::AsyncConnection; +using ::google::cloud::storage_experimental::ObjectDescriptorConnection; using ::google::cloud::storage_mocks::MockAsyncConnection; +using ::google::cloud::storage_mocks::MockAsyncObjectDescriptorConnection; using ::google::cloud::storage_mocks::MockAsyncReaderConnection; using ::google::cloud::storage_mocks::MockAsyncRewriterConnection; using ::google::cloud::storage_mocks::MockAsyncWriterConnection; @@ -544,6 +548,197 @@ TEST(ConnectionTracing, RewriteObject) { SpanHasEvents(EventNamed("gl-cpp.storage.rewrite.iterate"))))); } +TEST(ConnectionTracing, OpenError) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext>> + p; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + EXPECT_CALL(*mock, Open).WillOnce(expect_context(p)); + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto result = + actual->Open(AsyncConnection::OpenParams{}).then(expect_no_context); + + p.set_value( + StatusOr< + std::shared_ptr>( + PermanentError())); + EXPECT_THAT(result.get(), StatusIs(PermanentError().code())); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, ElementsAre( + AllOf(SpanNamed("storage::AsyncConnection::Open"), + SpanWithStatus(opentelemetry::trace::StatusCode::kError), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + +TEST(ConnectionTracing, OpenSuccess) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext>> + p; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + EXPECT_CALL(*mock, Open).WillOnce(expect_context(p)); + + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto f = actual->Open(AsyncConnection::OpenParams{}).then(expect_no_context); + + auto mock_descriptor = + std::make_shared(); + p.set_value( + StatusOr< + std::shared_ptr>( + std::move(mock_descriptor))); + auto result = f.get(); + ASSERT_STATUS_OK(result); + auto descriptor = *std::move(result); + descriptor.reset(); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT(spans, ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::Open"), + SpanWithStatus(opentelemetry::trace::StatusCode::kOk), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + +TEST(ConnectionTracing, StartAppendableObjectUploadSuccess) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext< + StatusOr>> + p; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + + EXPECT_CALL(*mock, StartAppendableObjectUpload).WillOnce(expect_context(p)); + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto f = actual + ->StartAppendableObjectUpload( + AsyncConnection::AppendableUploadParams{}) + .then(expect_no_context); + + auto mock_header = std::make_unique(); + EXPECT_CALL(*mock_header, Finalize) + .WillOnce(Return(ByMove( + make_ready_future(make_status_or(google::storage::v2::Object{}))))); + p.set_value( + StatusOr>( + std::move(mock_header))); + + auto result = f.get(); + ASSERT_STATUS_OK(result); + auto writer = *std::move(result); + auto w = writer->Finalize(storage_experimental::WritePayload{}).get(); + EXPECT_STATUS_OK(w); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, + ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::StartAppendableObjectUpload"), + SpanWithStatus(opentelemetry::trace::StatusCode::kOk), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + +TEST(ConnectionTracing, StartAppendableObjectUploadError) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext< + StatusOr>> + p; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + EXPECT_CALL(*mock, StartAppendableObjectUpload).WillOnce(expect_context(p)); + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto result = actual->StartAppendableObjectUpload( + AsyncConnection::AppendableUploadParams{}); + + p.set_value( + StatusOr>( + PermanentError())); + EXPECT_THAT(result.get(), StatusIs(PermanentError().code())); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, + ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::StartAppendableObjectUpload"), + SpanWithStatus(opentelemetry::trace::StatusCode::kError), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + +TEST(ConnectionTracing, ResumeAppendableObjectUploadSuccess) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext< + StatusOr>> + p; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + + EXPECT_CALL(*mock, ResumeAppendableObjectUpload).WillOnce(expect_context(p)); + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto f = actual + ->ResumeAppendableObjectUpload( + AsyncConnection::AppendableUploadParams{}) + .then(expect_no_context); + + auto mock_writer = std::make_unique(); + EXPECT_CALL(*mock_writer, Finalize) + .WillOnce(Return(ByMove( + make_ready_future(make_status_or(google::storage::v2::Object{}))))); + p.set_value( + StatusOr>( + std::move(mock_writer))); + + auto result = f.get(); + ASSERT_STATUS_OK(result); + auto writer = *std::move(result); + auto w = writer->Finalize(storage_experimental::WritePayload{}).get(); + EXPECT_STATUS_OK(w); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, + ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::ResumeAppendableObjectUpload"), + SpanWithStatus(opentelemetry::trace::StatusCode::kOk), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + +TEST(ConnectionTracing, ResumeAppendableObjectUploadError) { + auto span_catcher = InstallSpanCatcher(); + PromiseWithOTelContext< + StatusOr>> + p; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options).WillOnce(Return(TracingEnabled())); + EXPECT_CALL(*mock, ResumeAppendableObjectUpload).WillOnce(expect_context(p)); + auto actual = MakeTracingAsyncConnection(std::move(mock)); + auto result = actual + ->ResumeAppendableObjectUpload( + AsyncConnection::AppendableUploadParams{}) + .then(expect_no_context); + + p.set_value( + StatusOr>( + PermanentError())); + EXPECT_THAT(result.get(), StatusIs(PermanentError().code())); + + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, + ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::ResumeAppendableObjectUpload"), + SpanWithStatus(opentelemetry::trace::StatusCode::kError), + SpanHasInstrumentationScope(), SpanKindIsClient()))); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_internal diff --git a/google/cloud/storage/internal/async/default_options.cc b/google/cloud/storage/internal/async/default_options.cc index 75a74ab7795d7..f48fe279e34bb 100644 --- a/google/cloud/storage/internal/async/default_options.cc +++ b/google/cloud/storage/internal/async/default_options.cc @@ -68,7 +68,9 @@ Options DefaultOptionsAsync(Options opts) { storage_experimental::StopOnConsecutiveErrorsResumePolicy()) .set( storage_experimental::MakeStrictIdempotencyPolicy) - .set(true)); + .set(true) + .set(128 * 1024 * + 1024L)); return Adjust(DefaultOptionsGrpc(std::move(opts))); } diff --git a/google/cloud/storage/internal/async/default_options_test.cc b/google/cloud/storage/internal/async/default_options_test.cc index 996018d7464f2..940b3c9bdaa3e 100644 --- a/google/cloud/storage/internal/async/default_options_test.cc +++ b/google/cloud/storage/internal/async/default_options_test.cc @@ -85,6 +85,13 @@ TEST(DefaultOptionsAsync, Adjust) { EXPECT_LT(lwm, hwm); } +TEST(DefaultOptionsAsync, MaximumRangeSizeOption) { + auto const options = DefaultOptionsAsync({}); + auto const max_range_size_option = + options.get(); + EXPECT_EQ(max_range_size_option, 128 * 1024 * 1024L); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_internal diff --git a/google/cloud/storage/internal/async/handle_redirect_error.cc b/google/cloud/storage/internal/async/handle_redirect_error.cc new file mode 100644 index 0000000000000..1e70ff6c3bf39 --- /dev/null +++ b/google/cloud/storage/internal/async/handle_redirect_error.cc @@ -0,0 +1,65 @@ +// Copyright 2024 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/storage/internal/async/handle_redirect_error.h" +#include "google/cloud/internal/status_payload_keys.h" + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +void EnsureFirstMessageAppendObjectSpec( + google::storage::v2::BidiWriteObjectRequest& request) { + if (request.has_write_object_spec()) { + auto spec = request.write_object_spec(); + auto& append_object_spec = *request.mutable_append_object_spec(); + append_object_spec.set_bucket(spec.resource().bucket()); + append_object_spec.set_object(spec.resource().name()); + } +} + +google::rpc::Status ExtractGrpcStatus(Status const& status) { + auto proto_status = google::rpc::Status{}; + auto payload = google::cloud::internal::GetPayload( + status, google::cloud::internal::StatusPayloadGrpcProto()); + if (payload) proto_status.ParseFromString(*payload); + return proto_status; +} + +void ApplyRedirectErrors(google::storage::v2::BidiReadObjectSpec& spec, + google::rpc::Status const& rpc_status) { + for (auto const& any : rpc_status.details()) { + auto error = google::storage::v2::BidiReadObjectRedirectedError{}; + if (!any.UnpackTo(&error)) continue; + *spec.mutable_read_handle() = std::move(*error.mutable_read_handle()); + *spec.mutable_routing_token() = std::move(*error.mutable_routing_token()); + } +} + +void ApplyWriteRedirectErrors(google::storage::v2::AppendObjectSpec& spec, + google::rpc::Status const& rpc_status) { + for (auto const& any : rpc_status.details()) { + auto error = google::storage::v2::BidiWriteObjectRedirectedError{}; + if (!any.UnpackTo(&error)) continue; + *spec.mutable_write_handle() = std::move(*error.mutable_write_handle()); + *spec.mutable_routing_token() = std::move(*error.mutable_routing_token()); + if (error.has_generation()) spec.set_generation(error.generation()); + } +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/handle_redirect_error.h b/google/cloud/storage/internal/async/handle_redirect_error.h new file mode 100644 index 0000000000000..94adb1679cf15 --- /dev/null +++ b/google/cloud/storage/internal/async/handle_redirect_error.h @@ -0,0 +1,44 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_HANDLE_REDIRECT_ERROR_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_HANDLE_REDIRECT_ERROR_H + +#include "google/cloud/status.h" +#include "google/cloud/version.h" +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +void EnsureFirstMessageAppendObjectSpec( + google::storage::v2::BidiWriteObjectRequest& request); + +google::rpc::Status ExtractGrpcStatus(Status const& status); + +void ApplyRedirectErrors(google::storage::v2::BidiReadObjectSpec& spec, + google::rpc::Status const& rpc_status); + +void ApplyWriteRedirectErrors(google::storage::v2::AppendObjectSpec& spec, + google::rpc::Status const& rpc_status); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_HANDLE_REDIRECT_ERROR_H diff --git a/google/cloud/storage/internal/async/object_descriptor_connection_tracing.cc b/google/cloud/storage/internal/async/object_descriptor_connection_tracing.cc new file mode 100644 index 0000000000000..8f5e71fcddfb5 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_connection_tracing.cc @@ -0,0 +1,84 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_connection_tracing.h" +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/internal/opentelemetry.h" +#include "google/cloud/version.h" +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY +#include +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +namespace { + +namespace sc = ::opentelemetry::trace::SemanticConventions; + +class AsyncObjectDescriptorConnectionTracing + : public storage_experimental::ObjectDescriptorConnection { + public: + explicit AsyncObjectDescriptorConnectionTracing( + opentelemetry::nostd::shared_ptr span, + std::shared_ptr impl) + : span_(std::move(span)), impl_(std::move(impl)) {} + + ~AsyncObjectDescriptorConnectionTracing() override { + internal::EndSpan(*span_); + } + + Options options() const override { return impl_->options(); } + + absl::optional metadata() const override { + return impl_->metadata(); + } + + std::unique_ptr Read( + ReadParams p) override { + internal::OTelScope scope(span_); + auto result = impl_->Read(p); + span_->AddEvent("gl-cpp.open.read", + {{sc::kThreadId, internal::CurrentThreadId()}, + {"read-start", p.start}, + {"read-length", p.length}}); + return result; + } + + private: + opentelemetry::nostd::shared_ptr span_; + std::shared_ptr impl_; +}; + +} // namespace + +std::shared_ptr +MakeTracingObjectDescriptorConnection( + opentelemetry::nostd::shared_ptr span, + std::shared_ptr impl) { + return std::make_unique( + std::move(span), std::move(impl)); +} + +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_connection_tracing.h b/google/cloud/storage/internal/async/object_descriptor_connection_tracing.h new file mode 100644 index 0000000000000..cff3325993113 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_connection_tracing.h @@ -0,0 +1,42 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_TRACING_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_TRACING_H + +#include "google/cloud/storage/async/object_descriptor_connection.h" +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY +#include "google/cloud/internal/opentelemetry.h" +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +std::shared_ptr +MakeTracingObjectDescriptorConnection( + opentelemetry::nostd::shared_ptr span, + std::shared_ptr impl); + +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_TRACING_H diff --git a/google/cloud/storage/internal/async/object_descriptor_connection_tracing_test.cc b/google/cloud/storage/internal/async/object_descriptor_connection_tracing_test.cc new file mode 100644 index 0000000000000..fbecb948f7dc3 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_connection_tracing_test.cc @@ -0,0 +1,85 @@ +// Copyright 2024 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. + +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +#include "google/cloud/storage/internal/async/object_descriptor_connection_tracing.h" +#include "google/cloud/storage/async/object_descriptor_connection.h" +#include "google/cloud/storage/mocks/mock_async_object_descriptor_connection.h" +#include "google/cloud/storage/mocks/mock_async_reader_connection.h" +#include "google/cloud/opentelemetry_options.h" +#include "google/cloud/options.h" +#include "google/cloud/testing_util/opentelemetry_matchers.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ReadResponse = + ::google::cloud::storage_experimental::AsyncReaderConnection::ReadResponse; +using ::google::cloud::storage_experimental::ObjectDescriptorConnection; +using ::google::cloud::storage_mocks::MockAsyncObjectDescriptorConnection; +using ::google::cloud::storage_mocks::MockAsyncReaderConnection; +using ::google::cloud::testing_util::EventNamed; +using ::google::cloud::testing_util::InstallSpanCatcher; +using ::google::cloud::testing_util::OTelAttribute; +using ::google::cloud::testing_util::SpanEventAttributesAre; +using ::google::cloud::testing_util::SpanHasInstrumentationScope; +using ::google::cloud::testing_util::SpanKindIsClient; +using ::google::cloud::testing_util::SpanNamed; +using ::google::cloud::testing_util::SpanWithStatus; +using ::testing::_; + +TEST(ObjectDescriptorConnectionTracing, Read) { + namespace sc = ::opentelemetry::trace::SemanticConventions; + auto span_catcher = InstallSpanCatcher(); + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Read) + .WillOnce([](ObjectDescriptorConnection::ReadParams p) { + EXPECT_EQ(p.start, 100); + EXPECT_EQ(p.length, 200); + return std::make_unique(); + }); + auto actual = MakeTracingObjectDescriptorConnection( + internal::MakeSpan("test-span-name"), std::move(mock)); + auto f1 = actual->Read(ObjectDescriptorConnection::ReadParams{100, 200}); + + actual.reset(); + auto spans = span_catcher->GetSpans(); + EXPECT_THAT(spans, + ElementsAre(AllOf( + SpanNamed("test-span-name"), + SpanWithStatus(opentelemetry::trace::StatusCode::kOk), + SpanHasInstrumentationScope(), SpanKindIsClient(), + SpanHasEvents(AllOf( + EventNamed("gl-cpp.open.read"), + SpanEventAttributesAre( + OTelAttribute("read-length", 200), + OTelAttribute("read-start", 100), + OTelAttribute(sc::kThreadId, _))))))); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY diff --git a/google/cloud/storage/internal/async/object_descriptor_impl.cc b/google/cloud/storage/internal/async/object_descriptor_impl.cc new file mode 100644 index 0000000000000..dafec1720665f --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_impl.cc @@ -0,0 +1,232 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_impl.h" +#include "google/cloud/storage/async/options.h" +#include "google/cloud/storage/internal/async/handle_redirect_error.h" +#include "google/cloud/storage/internal/async/object_descriptor_reader_tracing.h" +#include "google/cloud/storage/internal/hash_function.h" +#include "google/cloud/storage/internal/hash_function_impl.h" +#include "google/cloud/grpc_error_delegate.h" +#include "google/cloud/internal/opentelemetry.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +ObjectDescriptorImpl::ObjectDescriptorImpl( + std::unique_ptr resume_policy, + OpenStreamFactory make_stream, + google::storage::v2::BidiReadObjectSpec read_object_spec, + std::shared_ptr stream, Options options) + : resume_policy_(std::move(resume_policy)), + make_stream_(std::move(make_stream)), + read_object_spec_(std::move(read_object_spec)), + stream_(std::move(stream)), + options_(std::move(options)) {} + +ObjectDescriptorImpl::~ObjectDescriptorImpl() { stream_->Cancel(); } + +void ObjectDescriptorImpl::Start( + google::storage::v2::BidiReadObjectResponse first_response) { + OnRead(std::move(first_response)); +} + +void ObjectDescriptorImpl::Cancel() { return stream_->Cancel(); } + +absl::optional ObjectDescriptorImpl::metadata() + const { + std::unique_lock lk(mu_); + return metadata_; +} + +std::unique_ptr +ObjectDescriptorImpl::Read(ReadParams p) { + std::shared_ptr hash_function = + std::shared_ptr( + storage::internal::CreateNullHashFunction()); + if (options_.has()) { + hash_function = + std::make_shared( + storage::internal::CreateNullHashFunction()); + } + auto range = std::make_shared(p.start, p.length, hash_function); + + std::unique_lock lk(mu_); + auto const id = ++read_id_generator_; + active_ranges_.emplace(id, range); + auto& read_range = *next_request_.add_read_ranges(); + read_range.set_read_id(id); + read_range.set_read_offset(p.start); + read_range.set_read_length(p.length); + Flush(std::move(lk)); + + if (!internal::TracingEnabled(options_)) { + return std::unique_ptr( + std::make_unique(std::move(range))); + } + + return MakeTracingObjectDescriptorReader(std::move(range)); +} + +void ObjectDescriptorImpl::Flush(std::unique_lock lk) { + if (write_pending_ || next_request_.read_ranges().empty()) return; + write_pending_ = true; + google::storage::v2::BidiReadObjectRequest request; + request.Swap(&next_request_); + + // Assign CurrentStream to a temporary variable to prevent + // lifetime extension which can cause the lock to be held until the + // end of the block. + auto current_stream = CurrentStream(std::move(lk)); + current_stream->Write(std::move(request)).then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnWrite(f.get()); + }); +} + +void ObjectDescriptorImpl::OnWrite(bool ok) { + std::unique_lock lk(mu_); + if (!ok) return DoFinish(std::move(lk)); + write_pending_ = false; + Flush(std::move(lk)); +} + +void ObjectDescriptorImpl::DoRead(std::unique_lock lk) { + // Assign CurrentStream to a temporary variable to prevent + // lifetime extension which can cause the lock to be held until the + // end of the block. + auto current_stream = CurrentStream(std::move(lk)); + current_stream->Read().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnRead(f.get()); + }); +} + +void ObjectDescriptorImpl::OnRead( + absl::optional response) { + std::unique_lock lk(mu_); + if (!response) return DoFinish(std::move(lk)); + if (response->has_metadata()) { + metadata_ = std::move(*response->mutable_metadata()); + } + if (response->has_read_handle()) { + *read_object_spec_.mutable_read_handle() = + std::move(*response->mutable_read_handle()); + } + auto copy = CopyActiveRanges(lk); + // Release the lock while notifying the ranges. The notifications may trigger + // application code, and that code may callback on this class. + lk.unlock(); + for (auto& range_data : *response->mutable_object_data_ranges()) { + auto id = range_data.read_range().read_id(); + auto const l = copy.find(id); + if (l == copy.end()) continue; + // TODO(#34) - Consider returning if the range is done, and then + // skipping CleanupDoneRanges(). + l->second->OnRead(std::move(range_data)); + } + lk.lock(); + CleanupDoneRanges(lk); + DoRead(std::move(lk)); +} + +void ObjectDescriptorImpl::CleanupDoneRanges( + std::unique_lock const&) { + for (auto i = active_ranges_.begin(); i != active_ranges_.end();) { + if (i->second->IsDone()) { + i = active_ranges_.erase(i); + } else { + ++i; + } + } +} + +void ObjectDescriptorImpl::DoFinish(std::unique_lock lk) { + // Assign CurrentStream to a temporary variable to prevent + // lifetime extension which can cause the lock to be held until the + // end of the block. + auto current_stream = CurrentStream(std::move(lk)); + auto pending = current_stream->Finish(); + if (!pending.valid()) return; + pending.then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnFinish(f.get()); + }); +} + +void ObjectDescriptorImpl::OnFinish(Status const& status) { + auto proto_status = ExtractGrpcStatus(status); + + if (IsResumable(status, proto_status)) return Resume(proto_status); + std::unique_lock lk(mu_); + auto copy = CopyActiveRanges(std::move(lk)); + for (auto const& kv : copy) { + kv.second->OnFinish(status); + } +} + +void ObjectDescriptorImpl::Resume(google::rpc::Status const& proto_status) { + std::unique_lock lk(mu_); + // This call needs to happen inside the lock, as it may modify + // `read_object_spec_`. + ApplyRedirectErrors(read_object_spec_, proto_status); + auto request = google::storage::v2::BidiReadObjectRequest{}; + *request.mutable_read_object_spec() = read_object_spec_; + for (auto const& kv : active_ranges_) { + auto range = kv.second->RangeForResume(kv.first); + if (!range) continue; + *request.add_read_ranges() = *std::move(range); + } + write_pending_ = true; + lk.unlock(); + make_stream_(std::move(request)).then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnResume(f.get()); + }); +} + +void ObjectDescriptorImpl::OnResume(StatusOr result) { + if (!result) return OnFinish(std::move(result).status()); + std::unique_lock lk(mu_); + stream_ = std::move(result->stream); + // TODO(#36) - this should be done without release the lock. + Flush(std::move(lk)); + OnRead(std::move(result->first_response)); +} + +bool ObjectDescriptorImpl::IsResumable( + Status const& status, google::rpc::Status const& proto_status) { + for (auto const& any : proto_status.details()) { + auto error = google::storage::v2::BidiReadObjectError{}; + if (!any.UnpackTo(&error)) continue; + auto ranges = CopyActiveRanges(); + for (auto const& range : CopyActiveRanges()) { + for (auto const& range_error : error.read_range_errors()) { + if (range.first != range_error.read_id()) continue; + range.second->OnFinish(MakeStatusFromRpcError(range_error.status())); + } + } + CleanupDoneRanges(std::unique_lock(mu_)); + return true; + } + + return resume_policy_->OnFinish(status) == + storage_experimental::ResumePolicy::kContinue; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_impl.h b/google/cloud/storage/internal/async/object_descriptor_impl.h new file mode 100644 index 0000000000000..55d6d292c7485 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_impl.h @@ -0,0 +1,115 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_IMPL_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_IMPL_H + +#include "google/cloud/storage/async/object_descriptor_connection.h" +#include "google/cloud/storage/async/resume_policy.h" +#include "google/cloud/storage/internal/async/object_descriptor_reader.h" +#include "google/cloud/storage/internal/async/open_stream.h" +#include "google/cloud/storage/internal/async/read_range.h" +#include "google/cloud/storage/options.h" +#include "google/cloud/status.h" +#include "google/cloud/version.h" +#include "absl/types/optional.h" +#include +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +class ObjectDescriptorImpl + : public storage_experimental::ObjectDescriptorConnection, + public std::enable_shared_from_this { + public: + ObjectDescriptorImpl( + std::unique_ptr resume_policy, + OpenStreamFactory make_stream, + google::storage::v2::BidiReadObjectSpec read_object_spec, + std::shared_ptr stream, Options options = {}); + ~ObjectDescriptorImpl() override; + + // Start the read loop. + void Start(google::storage::v2::BidiReadObjectResponse first_response); + + // Cancel the underlying RPC and stop the resume loop. + void Cancel(); + + Options options() const override { return options_; } + + // Return the object metadata. This is only available after the first `Read()` + // returns. + absl::optional metadata() const override; + + // Start a new ranged read. + std::unique_ptr Read( + ReadParams p) override; + + private: + std::weak_ptr WeakFromThis() { + return shared_from_this(); + } + + // This may seem expensive, but it is less bug-prone than iterating over + // the map with the lock held. + auto CopyActiveRanges(std::unique_lock const&) const { + return active_ranges_; + } + + auto CopyActiveRanges() const { + return CopyActiveRanges(std::unique_lock(mu_)); + } + + auto CurrentStream(std::unique_lock) const { return stream_; } + + void Flush(std::unique_lock lk); + void OnWrite(bool ok); + void DoRead(std::unique_lock); + void OnRead( + absl::optional response); + void CleanupDoneRanges(std::unique_lock const&); + void DoFinish(std::unique_lock); + void OnFinish(Status const& status); + void Resume(google::rpc::Status const& proto_status); + void OnResume(StatusOr result); + bool IsResumable(Status const& status, + google::rpc::Status const& proto_status); + + std::unique_ptr resume_policy_; + OpenStreamFactory make_stream_; + + mutable std::mutex mu_; + google::storage::v2::BidiReadObjectSpec read_object_spec_; + std::shared_ptr stream_; + absl::optional metadata_; + std::int64_t read_id_generator_ = 0; + bool write_pending_ = false; + google::storage::v2::BidiReadObjectRequest next_request_; + + std::unordered_map> active_ranges_; + Options options_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_IMPL_H diff --git a/google/cloud/storage/internal/async/object_descriptor_impl_test.cc b/google/cloud/storage/internal/async/object_descriptor_impl_test.cc new file mode 100644 index 0000000000000..e321e96820b0c --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_impl_test.cc @@ -0,0 +1,1197 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_impl.h" +#include "google/cloud/mocks/mock_async_streaming_read_write_rpc.h" +#include "google/cloud/storage/async/resume_policy.h" +#include "google/cloud/storage/internal/async/default_options.h" +#include "google/cloud/storage/options.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_storage_stub.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "absl/strings/string_view.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::storage::testing::canonical_errors::TransientError; +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::IsOk; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::StatusIs; +using ::google::protobuf::TextFormat; +using ::testing::ElementsAre; +using ::testing::NotNull; +using ::testing::Optional; +using ::testing::ResultOf; +using ::testing::VariantWith; + +using Request = google::storage::v2::BidiReadObjectRequest; +using Response = google::storage::v2::BidiReadObjectResponse; +using MockStream = + google::cloud::mocks::MockAsyncStreamingReadWriteRpc; +using MockFactory = + ::testing::MockFunction>(Request)>; + +auto constexpr kMetadataText = R"pb( + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 +)pb"; + +auto NoResume() { + return storage_experimental::LimitedErrorCountResumePolicy(0)(); +} + +MATCHER_P(IsProtoEqualModuloRepeatedFieldOrdering, value, + "Checks whether protos are equal, ignoring repeated field ordering") { + std::string delta; + google::protobuf::util::MessageDifferencer differencer; + differencer.set_repeated_field_comparison( + google::protobuf::util::MessageDifferencer::AS_SET); + differencer.ReportDifferencesToString(&delta); + if (differencer.Compare(arg, value)) return true; + *result_listener << "\n" << delta; + return false; +} + +/// @test Verify opening a stream and closing it produces the expected results. +TEST(ObjectDescriptorImpl, LifecycleNoRead) { + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Read).WillOnce([&sequencer]() { + return sequencer.PushBack("Read[1]").then( + [](auto) { return absl::optional{}; }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + EXPECT_CALL(*stream, Cancel).WillOnce([&sequencer]() { + sequencer.PushBack("Cancel"); + }); + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), + google::storage::v2::BidiReadObjectSpec{}, + std::make_shared(std::move(stream))); + auto response = Response{}; + EXPECT_TRUE( + TextFormat::ParseFromString(kMetadataText, response.mutable_metadata())); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto expected_metadata = google::storage::v2::Object{}; + EXPECT_TRUE(TextFormat::ParseFromString(kMetadataText, &expected_metadata)); + EXPECT_THAT(tested->metadata(), Optional(IsProtoEqual(expected_metadata))); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + read1.first.set_value(true); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + tested.reset(); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Cancel"); + next.first.set_value(true); +} + +/// @test Read a single stream and then close. +TEST(ObjectDescriptorImpl, ReadSingleRange) { + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kResponse1 = R"pb( + read_handle { handle: "handle-23456" } + object_data_ranges { + range_end: true + read_range { read_id: 1 read_offset: 20000 } + checksummed_data { + content: "The quick brown fox jumps over the lazy dog" + } + } + )pb"; + + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Write) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }); + EXPECT_CALL(*stream, Read) + .WillOnce([=, &sequencer]() { + return sequencer.PushBack("Read[1]").then([&](auto) { + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse1, &response)); + return absl::make_optional(response); + }); + }) + .WillOnce([&sequencer]() { + return sequencer.PushBack("Read[2]").then( + [](auto) { return absl::optional{}; }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + EXPECT_CALL(*stream, Cancel).Times(1); + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), + google::storage::v2::BidiReadObjectSpec{}, + std::make_shared(std::move(stream))); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + + auto expected_metadata = google::storage::v2::Object{}; + EXPECT_TRUE(TextFormat::ParseFromString(kMetadataText, &expected_metadata)); + EXPECT_THAT(tested->metadata(), Optional(IsProtoEqual(expected_metadata))); + + auto s1 = tested->Read({20000, 100}); + auto s1r1 = s1->Read(); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + next.first.set_value(true); + read1.first.set_value(true); + + // The future returned by `Read()` should become satisfied at this point. + // We expect it to contain the right data. + EXPECT_THAT(s1r1.get(), + VariantWith(ResultOf( + "contents are", + [](storage_experimental::ReadPayload const& p) { + return p.contents(); + }, + ElementsAre(absl::string_view{ + "The quick brown fox jumps over the lazy dog"})))); + // Since the `range_end()` flag is set, we expect the stream to finish with + // success. + EXPECT_THAT(s1->Read().get(), VariantWith(IsOk())); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read[2]"); + next.first.set_value(true); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); +} + +/// @test Reading multiple ranges creates a single request. +TEST(ObjectDescriptorImpl, ReadMultipleRanges) { + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kRequest2 = R"pb( + read_ranges { read_id: 2 read_offset: 40000 read_length: 100 } + read_ranges { read_id: 3 read_offset: 60000 read_length: 100 } + )pb"; + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + auto constexpr kResponse1 = R"pb( + object_data_ranges { + range_end: true + read_range { read_id: 1 read_offset: 20000 } + checksummed_data { + content: "The quick brown fox jumps over the lazy dog" + } + } + )pb"; + + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Write) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest2, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[2]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read) + .WillOnce([&]() { + return sequencer.PushBack("Read[1]").then([&](auto) { + // Simulate a response with 3 out of order messages. + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse1, &response)); + return absl::make_optional(response); + }); + }) + .WillRepeatedly([&]() { + return sequencer.PushBack("Read[2]").then( + [&](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + EXPECT_CALL(*stream, Cancel).Times(1); + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), + google::storage::v2::BidiReadObjectSpec{}, + std::make_shared(std::move(stream))); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + + // Additional ranges are queued until the first `Write()` call is completed. + auto s2 = tested->Read({2 * kOffset, kLength}); + ASSERT_THAT(s2, NotNull()); + + auto s3 = tested->Read({3 * kOffset, kLength}); + ASSERT_THAT(s3, NotNull()); + + // Complete the first `Write()` call, that should result in a second + // `Write()` call with the two additional ranges. + next.first.set_value(true); + + // And then the follow up `Write()` message with the queued information. + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[2]"); + next.first.set_value(true); + + auto s1r1 = s1->Read(); + auto s2r1 = s2->Read(); + auto s3r1 = s3->Read(); + EXPECT_FALSE(s1r1.is_ready()); + EXPECT_FALSE(s2r1.is_ready()); + EXPECT_FALSE(s3r1.is_ready()); + + read1.first.set_value(true); + + // The future returned by `Read()` should become satisfied at this point. + // We expect it to contain the right data. + EXPECT_THAT(s1r1.get(), + VariantWith(ResultOf( + "contents are", + [](storage_experimental::ReadPayload const& p) { + return p.contents(); + }, + ElementsAre(absl::string_view{ + "The quick brown fox jumps over the lazy dog"})))); + // Since the `range_end()` flag is set, we expect the stream to finish with + // success. + EXPECT_THAT(s1->Read().get(), VariantWith(IsOk())); + + // Simulate a clean shutdown with an unrecoverable error. + auto last_read = sequencer.PopFrontWithName(); + EXPECT_EQ(last_read.second, "Read[2]"); + last_read.first.set_value(false); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + EXPECT_TRUE(s2r1.is_ready()); + EXPECT_TRUE(s3r1.is_ready()); + EXPECT_THAT(s2r1.get(), VariantWith(PermanentError())); + EXPECT_THAT(s3r1.get(), VariantWith(PermanentError())); +} + +/// @test Reading a range may require many messages. +TEST(ObjectDescriptorImpl, ReadSingleRangeManyMessages) { + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + auto constexpr kResponse1 = R"pb( + object_data_ranges { + range_end: false + read_range { read_id: 1 read_offset: 20000 } + checksummed_data { + content: "The quick brown fox jumps over the lazy dog" + } + } + )pb"; + auto constexpr kResponse2 = R"pb( + object_data_ranges { + range_end: true + read_range { read_id: 1 read_offset: 20026 } + checksummed_data { + content: "The quick brown fox jumps over the lazy dog" + } + } + )pb"; + auto constexpr kOffset = 20000; + auto constexpr kLength = 100; + + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Write) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read) + .WillOnce([&]() { + return sequencer.PushBack("Read[1]").then([&](auto) { + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse1, &response)); + return absl::make_optional(response); + }); + }) + .WillOnce([&]() { + return sequencer.PushBack("Read[2]").then([&](auto) { + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse2, &response)); + return absl::make_optional(response); + }); + }) + .WillRepeatedly([&]() { + return sequencer.PushBack("Read[3]").then( + [&](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + EXPECT_CALL(*stream, Cancel).Times(1); + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), + google::storage::v2::BidiReadObjectSpec{}, + std::make_shared(std::move(stream))); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read = sequencer.PopFrontWithName(); + EXPECT_EQ(read.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + next.first.set_value(true); + + auto s1r1 = s1->Read(); + EXPECT_FALSE(s1r1.is_ready()); + + read.first.set_value(true); + + // The future returned by `Read()` should become satisfied at this point. + // We expect it to contain the right data. + EXPECT_THAT(s1r1.get(), + VariantWith(ResultOf( + "contents are", + [](storage_experimental::ReadPayload const& p) { + return p.contents(); + }, + ElementsAre(absl::string_view{ + "The quick brown fox jumps over the lazy dog"})))); + + auto s1r2 = s1->Read(); + EXPECT_FALSE(s1r2.is_ready()); + + read = sequencer.PopFrontWithName(); + EXPECT_EQ(read.second, "Read[2]"); + read.first.set_value(true); + + // The future returned by `Read()` should become satisfied at this point. + // We expect it to contain the right data. + EXPECT_THAT(s1r2.get(), + VariantWith(ResultOf( + "contents are", + [](storage_experimental::ReadPayload const& p) { + return p.contents(); + }, + ElementsAre(absl::string_view{ + "The quick brown fox jumps over the lazy dog"})))); + + // Since the `range_end()` flag is set, we expect the stream to finish with + // success. + EXPECT_THAT(s1->Read().get(), VariantWith(IsOk())); + + auto last_read = sequencer.PopFrontWithName(); + EXPECT_EQ(last_read.second, "Read[3]"); + last_read.first.set_value(false); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); +} + +/// @test When the underlying stream fails with unrecoverable errors all ranges +/// fail. +TEST(ObjectDescriptorImpl, AllRangesFailOnUnrecoverableError) { + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kRequest2 = R"pb( + read_ranges { read_id: 2 read_offset: 40000 read_length: 100 } + read_ranges { read_id: 3 read_offset: 60000 read_length: 100 } + )pb"; + + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Write) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest2, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[2]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read).WillOnce([&]() { + return sequencer.PushBack("Read[1]").then( + [](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + EXPECT_CALL(*stream, Cancel).Times(1); + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), + google::storage::v2::BidiReadObjectSpec{}, + std::make_shared(std::move(stream))); + tested->Start(Response{}); + EXPECT_FALSE(tested->metadata().has_value()); + + auto read = sequencer.PopFrontWithName(); + EXPECT_EQ(read.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + + // Additional ranges are queued until the first `Write()` call is completed. + auto s2 = tested->Read({2 * kOffset, kLength}); + ASSERT_THAT(s2, NotNull()); + + auto s3 = tested->Read({3 * kOffset, kLength}); + ASSERT_THAT(s3, NotNull()); + + // Complete the first `Write()` call, that should result in a second + // `Write()` call with the two additional ranges. + next.first.set_value(true); + + // And then the follow up `Write()` message with the queued information. + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[2]"); + next.first.set_value(true); + + auto s1r1 = s1->Read(); + auto s2r1 = s2->Read(); + auto s3r1 = s3->Read(); + EXPECT_FALSE(s1r1.is_ready()); + EXPECT_FALSE(s2r1.is_ready()); + EXPECT_FALSE(s3r1.is_ready()); + + // Simulate a failure. + read.first.set_value(false); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + // All the ranges fail with the same error. + EXPECT_THAT(s1r1.get(), VariantWith(PermanentError())); + EXPECT_THAT(s2r1.get(), VariantWith(PermanentError())); + EXPECT_THAT(s3r1.get(), VariantWith(PermanentError())); +} + +auto InitialStream(AsyncSequencer& sequencer) { + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kRequest2 = R"pb( + read_ranges { read_id: 2 read_offset: 40000 read_length: 100 } + read_ranges { read_id: 3 read_offset: 60000 read_length: 100 } + )pb"; + + auto constexpr kResponse1 = R"pb( + object_data_ranges { + read_range { read_id: 1 read_offset: 20000 } + checksummed_data { content: "0123456789" crc32c: 0x280c069e } + } + object_data_ranges { + read_range { read_id: 2 read_offset: 40000 } + checksummed_data { content: "0123456789" crc32c: 0x280c069e } + } + object_data_ranges { + range_end: true + read_range { read_id: 3 read_offset: 40000 } + checksummed_data { content: "0123456789" crc32c: 0x280c069e } + } + )pb"; + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Cancel).Times(1); // Always called by OpenStream + EXPECT_CALL(*stream, Write) + .WillOnce([=, &sequencer](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest2, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[2]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read) + .WillOnce([=, &sequencer]() { + return sequencer.PushBack("Read[1]").then([&](auto) { + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse1, &response)); + return absl::make_optional(response); + }); + }) + .WillOnce([&]() { + return sequencer.PushBack("Read[2]").then( + [](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return TransientError(); }); + }); + + return stream; +} + +/// @test Verify that resuming a stream adjusts all offsets. +TEST(ObjectDescriptorImpl, ResumeRangesOnRecoverableError) { +// There is a problem with this test and certain versions of MSVC (19.43.34808) +// skipping it on WIN32 for now. +#ifdef _WIN32 + GTEST_SKIP(); +#endif + + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kReadSpecText = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + // The resume request should include all the remaining ranges, starting from + // the remaining offset (10 bytes after the start). + auto constexpr kResumeRequest = R"pb( + read_object_spec { + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + read_handle { handle: "handle-12345" } + } + read_ranges { read_id: 1 read_offset: 20010 read_length: 90 } + read_ranges { read_id: 2 read_offset: 40010 read_length: 90 } + )pb"; + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + + AsyncSequencer sequencer; + + MockFactory factory; + EXPECT_CALL(factory, Call).WillOnce([=, &sequencer](Request const& request) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResumeRequest, &expected)); + EXPECT_THAT(request, IsProtoEqualModuloRepeatedFieldOrdering(expected)); + // Resume with an unrecoverable failure to simplify the test. + return sequencer.PushBack("Factory").then( + [&](auto) { return StatusOr(PermanentError()); }); + }); + + auto spec = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kReadSpecText, &spec)); + auto tested = std::make_shared( + storage_experimental::LimitedErrorCountResumePolicy(1)(), + factory.AsStdFunction(), spec, + std::make_shared(InitialStream(sequencer))); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + + // Additional ranges are queued until the first `Write()` call is completed. + auto s2 = tested->Read({2 * kOffset, kLength}); + ASSERT_THAT(s2, NotNull()); + + auto s3 = tested->Read({3 * kOffset, kLength}); + ASSERT_THAT(s3, NotNull()); + + // Complete the first `Write()` call, that should result in a second + // `Write()` call with the two additional ranges. + next.first.set_value(true); + + // And then the follow up `Write()` message with the queued information. + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[2]"); + next.first.set_value(true); + + auto s1r1 = s1->Read(); + auto s2r1 = s2->Read(); + auto s3r1 = s3->Read(); + EXPECT_FALSE(s1r1.is_ready()); + EXPECT_FALSE(s2r1.is_ready()); + EXPECT_FALSE(s3r1.is_ready()); + + // Simulate a partial read. + read1.first.set_value(true); + // The ranges should have some data. + EXPECT_TRUE(s1r1.is_ready()); + EXPECT_TRUE(s2r1.is_ready()); + EXPECT_TRUE(s3r1.is_ready()); + + auto expected_r1 = VariantWith(ResultOf( + "contents are", + [](storage_experimental::ReadPayload const& p) { return p.contents(); }, + ElementsAre(absl::string_view{"0123456789"}))); + + EXPECT_THAT(s1r1.get(), expected_r1); + EXPECT_THAT(s2r1.get(), expected_r1); + EXPECT_THAT(s3r1.get(), expected_r1); + + auto s1r2 = s1->Read(); + auto s2r2 = s2->Read(); + auto s3r2 = s3->Read(); + EXPECT_FALSE(s1r2.is_ready()); + EXPECT_FALSE(s2r2.is_ready()); + // The third range should be fully done. + EXPECT_TRUE(s3r2.is_ready()); + EXPECT_THAT(s3r2.get(), VariantWith(IsOk())); + + // Simulate the recoverable failure. + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read[2]"); + next.first.set_value(false); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Factory"); + next.first.set_value(true); + + // All the ranges fail with the same error. + EXPECT_THAT(s1r2.get(), VariantWith(PermanentError())); + EXPECT_THAT(s2r2.get(), VariantWith(PermanentError())); +} + +Status RedirectError(absl::string_view handle, absl::string_view token) { + auto details = [&] { + auto redirected = google::storage::v2::BidiReadObjectRedirectedError{}; + redirected.mutable_read_handle()->set_handle(std::string(handle)); + redirected.set_routing_token(std::string(token)); + auto details_proto = google::rpc::Status{}; + details_proto.set_code(grpc::UNAVAILABLE); + details_proto.set_message("redirect"); + details_proto.add_details()->PackFrom(redirected); + + std::string details; + details_proto.SerializeToString(&details); + return details; + }; + + return google::cloud::MakeStatusFromRpcError( + grpc::Status(grpc::UNAVAILABLE, "redirect", details())); +} + +/// @test Verify that resuming a stream uses a handle and routing token. +TEST(ObjectDescriptorImpl, PendingFinish) { + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + + AsyncSequencer sequencer; + + auto initial_stream = [&sequencer]() { + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Cancel).Times(1); // Always called by OpenStream + EXPECT_CALL(*stream, Write) + .WillOnce([=, &sequencer](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto) { + return false; + }); + }); + + EXPECT_CALL(*stream, Read).WillOnce([&]() { + return sequencer.PushBack("Read[1]").then( + [](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return TransientError(); }); + }); + + return stream; + }; + + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kReadSpecText = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + + MockFactory factory; + EXPECT_CALL(factory, Call).Times(0); + + auto spec = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kReadSpecText, &spec)); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), spec, + std::make_shared(initial_stream())); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto write1 = sequencer.PopFrontWithName(); + EXPECT_EQ(write1.second, "Write[1]"); + + // Simulate a (nearly) simultaneous error in the `Write()` and `Read()` calls. + read1.first.set_value(false); + write1.first.set_value(false); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + // The ranges fails with the same error. + EXPECT_THAT(s1->Read().get(), VariantWith(TransientError())); +} + +/// @test Verify that resuming a stream uses a handle and routing token. +TEST(ObjectDescriptorImpl, ResumeUsesRouting) { + auto constexpr kResponse0 = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-12345" } + )pb"; + + AsyncSequencer sequencer; + + auto initial_stream = [&sequencer]() { + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Cancel).Times(1); // Always called by OpenStream + EXPECT_CALL(*stream, Write) + .WillOnce([=, &sequencer](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read).WillOnce([&]() { + return sequencer.PushBack("Read[1]").then( + [](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then([](auto) { + return RedirectError("handle-redirect-3456", "token-redirect-3456"); + }); + }); + + return stream; + }; + + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kReadSpecText = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + // The resume request should include all the remaining ranges, starting from + // the remaining offset (10 bytes after the start). + auto constexpr kResumeRequest = R"pb( + read_object_spec { + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + read_handle { handle: "handle-redirect-3456" } + routing_token: "token-redirect-3456" + } + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + + MockFactory factory; + EXPECT_CALL(factory, Call).WillOnce([=, &sequencer](Request const& request) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResumeRequest, &expected)); + EXPECT_THAT(request, IsProtoEqualModuloRepeatedFieldOrdering(expected)); + // Resume with an unrecoverable failure to simplify the test. + return sequencer.PushBack("Factory").then( + [&](auto) { return StatusOr(PermanentError()); }); + }); + + auto spec = google::storage::v2::BidiReadObjectSpec{}; + ASSERT_TRUE(TextFormat::ParseFromString(kReadSpecText, &spec)); + auto tested = std::make_shared( + storage_experimental::LimitedErrorCountResumePolicy(1)(), + factory.AsStdFunction(), spec, + std::make_shared(initial_stream())); + auto response = Response{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResponse0, &response)); + tested->Start(std::move(response)); + EXPECT_TRUE(tested->metadata().has_value()); + + auto read1 = sequencer.PopFrontWithName(); + EXPECT_EQ(read1.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto write1 = sequencer.PopFrontWithName(); + EXPECT_EQ(write1.second, "Write[1]"); + + // Simulate the recoverable failure. + read1.first.set_value(false); + write1.first.set_value(false); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Factory"); + next.first.set_value(true); + + // The ranges fails with the same error. + EXPECT_THAT(s1->Read().get(), VariantWith(PermanentError())); +} + +Status PartialFailure(std::int64_t read_id) { + auto details = [&] { + auto error = google::storage::v2::BidiReadObjectError{}; + auto& range_error = *error.add_read_range_errors(); + range_error.set_read_id(read_id); + range_error.mutable_status()->set_code(grpc::INVALID_ARGUMENT); + range_error.mutable_status()->set_message("out of range read"); + + auto details_proto = google::rpc::Status{}; + details_proto.set_code(grpc::INVALID_ARGUMENT); + details_proto.set_message("some reads are out of range"); + details_proto.add_details()->PackFrom(error); + + std::string details; + details_proto.SerializeToString(&details); + return details; + }; + + return google::cloud::MakeStatusFromRpcError( + grpc::Status(grpc::INVALID_ARGUMENT, "redirect", details())); +} + +/// @test When the underlying stream fails with unrecoverable errors all ranges +/// fail. +TEST(ObjectDescriptorImpl, RecoverFromPartialFailure) { + auto constexpr kLength = 100; + auto constexpr kOffset = 20000; + auto constexpr kReadSpecText = R"pb( + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + )pb"; + auto constexpr kRequest1 = R"pb( + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + )pb"; + auto constexpr kRequest2 = R"pb( + read_ranges { read_id: 2 read_offset: 4000000 read_length: 100 } + read_ranges { read_id: 3 read_offset: 60000 read_length: 100 } + )pb"; + + // The resume request should include all the remaining ranges. + auto constexpr kResumeRequest = R"pb( + read_object_spec { + bucket: "test-only-invalid" + object: "test-object" + generation: 24 + if_generation_match: 42 + } + read_ranges { read_id: 1 read_offset: 20000 read_length: 100 } + read_ranges { read_id: 3 read_offset: 60000 read_length: 100 } + )pb"; + + AsyncSequencer sequencer; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Write) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest1, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[1]").then([](auto f) { + return f.get(); + }); + }) + .WillOnce([&](Request const& request, grpc::WriteOptions) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kRequest2, &expected)); + EXPECT_THAT(request, IsProtoEqual(expected)); + return sequencer.PushBack("Write[2]").then([](auto f) { + return f.get(); + }); + }); + + EXPECT_CALL(*stream, Read).WillOnce([&]() { + return sequencer.PushBack("Read[1]").then( + [](auto) { return absl::optional{}; }); + }); + + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then([](auto) { + // Return an error, indicating that range #2 is invalid. It should + // resume with the new ranges. + return PartialFailure(2); + }); + }); + EXPECT_CALL(*stream, Cancel).Times(1); + + MockFactory factory; + EXPECT_CALL(factory, Call).WillOnce([=, &sequencer](Request const& request) { + auto expected = Request{}; + EXPECT_TRUE(TextFormat::ParseFromString(kResumeRequest, &expected)); + EXPECT_THAT(request, IsProtoEqualModuloRepeatedFieldOrdering(expected)); + // Resume with an unrecoverable failure to simplify the test. + return sequencer.PushBack("Factory").then( + [&](auto) { return StatusOr(PermanentError()); }); + }); + + auto spec = google::storage::v2::BidiReadObjectSpec{}; + EXPECT_TRUE(TextFormat::ParseFromString(kReadSpecText, &spec)); + auto tested = std::make_shared( + NoResume(), factory.AsStdFunction(), spec, + std::make_shared(std::move(stream))); + tested->Start(Response{}); + EXPECT_FALSE(tested->metadata().has_value()); + + auto read = sequencer.PopFrontWithName(); + EXPECT_EQ(read.second, "Read[1]"); + + auto s1 = tested->Read({kOffset, kLength}); + ASSERT_THAT(s1, NotNull()); + + // Asking for data should result in an immediate `Write()` message with the + // first range. + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[1]"); + + // Additional ranges are queued until the first `Write()` call is completed. + auto s2 = tested->Read({2 * kOffset * 100, kLength}); + ASSERT_THAT(s2, NotNull()); + + auto s3 = tested->Read({3 * kOffset, kLength}); + ASSERT_THAT(s3, NotNull()); + + // Complete the first `Write()` call, that should result in a second + // `Write()` call with the two additional ranges. + next.first.set_value(true); + + // And then the follow up `Write()` message with the queued information. + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write[2]"); + next.first.set_value(true); + + auto s1r1 = s1->Read(); + auto s2r1 = s2->Read(); + auto s3r1 = s3->Read(); + EXPECT_FALSE(s1r1.is_ready()); + EXPECT_FALSE(s2r1.is_ready()); + EXPECT_FALSE(s3r1.is_ready()); + + // Simulate a failure. + read.first.set_value(false); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + // Range 2 should fail with the invalid argument error. + EXPECT_THAT(s2r1.get(), + VariantWith(StatusIs(StatusCode::kInvalidArgument))); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Factory"); + next.first.set_value(true); + + // All the other ranges fail with the same error. + EXPECT_THAT(s1r1.get(), VariantWith(PermanentError())); + EXPECT_THAT(s3r1.get(), VariantWith(PermanentError())); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_reader.cc b/google/cloud/storage/internal/async/object_descriptor_reader.cc new file mode 100644 index 0000000000000..e982b936e4aed --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader.cc @@ -0,0 +1,38 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_reader.h" + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +ObjectDescriptorReader::ObjectDescriptorReader(std::shared_ptr impl) + : impl_(std::move(impl)) {} + +void ObjectDescriptorReader::Cancel() { /* noop, cannot cancel a single range + read */ +} + +future ObjectDescriptorReader::Read() { + return impl_->Read(); +} + +RpcMetadata ObjectDescriptorReader::GetRequestMetadata() { return {}; } + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_reader.h b/google/cloud/storage/internal/async/object_descriptor_reader.h new file mode 100644 index 0000000000000..657dce0fa9917 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader.h @@ -0,0 +1,54 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_H + +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/storage/internal/async/read_range.h" +#include "google/cloud/version.h" +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * Adapts `ReadRange` to meet the `AsyncReaderConnection` interface. + * + * We want to return `AsyncReader` objects from `ObjectDescriptor`. To do so, we + * need to implement the `AsyncReaderConnection` interface, using `ReadRange` as + * the underlying implementation. + */ +class ObjectDescriptorReader + : public storage_experimental::AsyncReaderConnection { + public: + explicit ObjectDescriptorReader(std::shared_ptr impl); + ~ObjectDescriptorReader() override = default; + + void Cancel() override; + future Read() override; + RpcMetadata GetRequestMetadata() override; + + private: + std::shared_ptr impl_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_H diff --git a/google/cloud/storage/internal/async/object_descriptor_reader_test.cc b/google/cloud/storage/internal/async/object_descriptor_reader_test.cc new file mode 100644 index 0000000000000..1fc6c86c3b93c --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader_test.cc @@ -0,0 +1,55 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_reader.h" +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage_experimental::ReadPayload; +using ::google::protobuf::TextFormat; +using ::testing::ElementsAre; +using ::testing::ResultOf; +using ::testing::VariantWith; + +TEST(ObjectDescriptorReader, Basic) { + auto impl = std::make_shared(10000, 30); + auto tested = ObjectDescriptorReader(impl); + + auto data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData0 = R"pb( + checksummed_data { content: "0123456789" } + read_range { read_offset: 10000 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData0, &data)); + impl->OnRead(std::move(data)); + + auto actual = tested.Read().get(); + EXPECT_THAT(actual, + VariantWith(ResultOf( + "contents", [](ReadPayload const& p) { return p.contents(); }, + ElementsAre("0123456789")))); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_reader_tracing.cc b/google/cloud/storage/internal/async/object_descriptor_reader_tracing.cc new file mode 100644 index 0000000000000..725ce5347dff6 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader_tracing.cc @@ -0,0 +1,93 @@ +// Copyright 2024 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/storage/internal/async/object_descriptor_reader_tracing.h" +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/storage/internal/async/object_descriptor_reader.h" +#include "google/cloud/internal/opentelemetry.h" +#include "google/cloud/version.h" +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY +#include +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +namespace { + +namespace sc = ::opentelemetry::trace::SemanticConventions; + +class ObjectDescriptorReaderTracing : public ObjectDescriptorReader { + public: + explicit ObjectDescriptorReaderTracing(std::shared_ptr impl) + : ObjectDescriptorReader(std::move(impl)) {} + + ~ObjectDescriptorReaderTracing() override = default; + + future Read() override { + auto span = internal::MakeSpan("storage::AsyncConnection::ReadObjectRange"); + internal::OTelScope scope(span); + return ObjectDescriptorReader::Read().then( + [span = std::move(span), + oc = opentelemetry::context::RuntimeContext::GetCurrent()]( + auto f) -> ReadResponse { + auto result = f.get(); + internal::DetachOTelContext(oc); + if (!absl::holds_alternative(result)) { + auto const& payload = + absl::get(result); + + span->AddEvent( + "gl-cpp.read-range", + {{/*sc::kRpcMessageType=*/"rpc.message.type", "RECEIVED"}, + {sc::kThreadId, internal::CurrentThreadId()}, + {"message.size", payload.size()}}); + } else { + span->AddEvent( + "gl-cpp.read-range", + {{/*sc::kRpcMessageType=*/"rpc.message.type", "RECEIVED"}, + {sc::kThreadId, internal::CurrentThreadId()}}); + return internal::EndSpan(*span, + absl::get(std::move(result))); + } + return result; + }); + } +}; + +} // namespace + +std::unique_ptr +MakeTracingObjectDescriptorReader(std::shared_ptr impl) { + return std::make_unique(std::move(impl)); +} + +#else // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +std::unique_ptr +MakeTracingObjectDescriptorReader(std::shared_ptr impl) { + return std::make_unique(std::move(impl)); +} + +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/object_descriptor_reader_tracing.h b/google/cloud/storage/internal/async/object_descriptor_reader_tracing.h new file mode 100644 index 0000000000000..039dd01be9e9d --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader_tracing.h @@ -0,0 +1,35 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_TRACING_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_TRACING_H + +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/storage/internal/async/read_range.h" +#include "google/cloud/internal/opentelemetry.h" + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +std::unique_ptr +MakeTracingObjectDescriptorReader(std::shared_ptr impl); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OBJECT_DESCRIPTOR_READER_TRACING_H diff --git a/google/cloud/storage/internal/async/object_descriptor_reader_tracing_test.cc b/google/cloud/storage/internal/async/object_descriptor_reader_tracing_test.cc new file mode 100644 index 0000000000000..fb7c71a03dd05 --- /dev/null +++ b/google/cloud/storage/internal/async/object_descriptor_reader_tracing_test.cc @@ -0,0 +1,102 @@ +// Copyright 2024 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. + +#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY + +#include "google/cloud/storage/internal/async/object_descriptor_reader_tracing.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/opentelemetry_options.h" +#include "google/cloud/options.h" +#include "google/cloud/testing_util/opentelemetry_matchers.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::testing_util::EventNamed; +using ::google::cloud::testing_util::InstallSpanCatcher; +using ::google::cloud::testing_util::OTelAttribute; +using ::google::cloud::testing_util::SpanEventAttributesAre; +using ::google::cloud::testing_util::SpanHasAttributes; +using ::google::cloud::testing_util::SpanHasEvents; +using ::google::cloud::testing_util::SpanNamed; +using ::google::protobuf::TextFormat; +using ::testing::_; + +namespace sc = ::opentelemetry::trace::SemanticConventions; + +TEST(ObjectDescriptorReaderTracing, Read) { + auto span_catcher = InstallSpanCatcher(); + + auto impl = std::make_shared(10000, 30); + auto reader = MakeTracingObjectDescriptorReader(impl); + + auto data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData0 = R"pb( + checksummed_data { content: "0123456789" } + read_range { read_offset: 10000 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData0, &data)); + impl->OnRead(std::move(data)); + + auto actual = reader->Read().get(); + auto spans = span_catcher->GetSpans(); + EXPECT_THAT(spans, ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::ReadObjectRange"), + SpanHasEvents(AllOf( + EventNamed("gl-cpp.read-range"), + SpanEventAttributesAre( + OTelAttribute("message.size", 10), + OTelAttribute(sc::kThreadId, _), + OTelAttribute("rpc.message.type", + "RECEIVED"))))))); +} + +TEST(ObjectDescriptorReaderTracing, ReadError) { + auto span_catcher = InstallSpanCatcher(); + auto impl = std::make_shared(10000, 30); + auto reader = MakeTracingObjectDescriptorReader(impl); + + impl->OnFinish(PermanentError()); + + auto actual = reader->Read().get(); + auto spans = span_catcher->GetSpans(); + EXPECT_THAT( + spans, + ElementsAre(AllOf( + SpanNamed("storage::AsyncConnection::ReadObjectRange"), + SpanHasAttributes( + OTelAttribute("gl-cpp.status_code", "NOT_FOUND")), + SpanHasEvents(AllOf(EventNamed("gl-cpp.read-range"), + SpanEventAttributesAre( + OTelAttribute(sc::kThreadId, _), + OTelAttribute("rpc.message.type", + "RECEIVED"))))))); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY diff --git a/google/cloud/storage/internal/async/open_object.cc b/google/cloud/storage/internal/async/open_object.cc new file mode 100644 index 0000000000000..e112d2376b1ed --- /dev/null +++ b/google/cloud/storage/internal/async/open_object.cc @@ -0,0 +1,103 @@ +// Copyright 2024 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/storage/internal/async/open_object.h" +#include "google/cloud/internal/absl_str_cat_quiet.h" +#include "google/cloud/internal/make_status.h" +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +std::string RequestParams( + google::storage::v2::BidiReadObjectRequest const& request) { + auto const& read_spec = request.read_object_spec(); + if (read_spec.has_routing_token()) { + return absl::StrCat("bucket=", read_spec.bucket(), + "&routing_token=", read_spec.routing_token()); + } + return absl::StrCat("bucket=", read_spec.bucket()); +} + +OpenObject::OpenObject(storage_internal::StorageStub& stub, CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::storage::v2::BidiReadObjectRequest request) + : rpc_(std::make_shared(CreateRpc( + stub, cq, std::move(context), std::move(options), request))), + initial_request_(std::move(request)) {} + +future> OpenObject::Call() { + auto future = promise_.get_future(); + rpc_->Start().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnStart(f.get()); + }); + return future; +} + +std::weak_ptr OpenObject::WeakFromThis() { + return shared_from_this(); +} + +std::unique_ptr OpenObject::CreateRpc( + storage_internal::StorageStub& stub, CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::storage::v2::BidiReadObjectRequest const& request) { + auto p = RequestParams(request); + if (!p.empty()) context->AddMetadata("x-goog-request-params", std::move(p)); + return stub.AsyncBidiReadObject(cq, std::move(context), std::move(options)); +} + +void OpenObject::OnStart(bool ok) { + if (!ok) return DoFinish(); + rpc_->Write(initial_request_).then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnWrite(f.get()); + }); +} + +void OpenObject::OnWrite(bool ok) { + if (!ok) return DoFinish(); + rpc_->Read().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnRead(f.get()); + }); +} + +void OpenObject::OnRead( + absl::optional response) { + if (!response) return DoFinish(); + promise_.set_value(OpenStreamResult{std::move(rpc_), std::move(*response)}); +} + +void OpenObject::DoFinish() { + rpc_->Finish().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnFinish(f.get()); + }); +} + +void OpenObject::OnFinish(Status status) { + if (!status.ok()) return promise_.set_value(std::move(status)); + // This should not happen, it indicates an EOF on the stream, but we + // did not ask to close it. + promise_.set_value(google::cloud::internal::InternalError( + "could not open stream, but the stream closed successfully", + GCP_ERROR_INFO())); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/open_object.h b/google/cloud/storage/internal/async/open_object.h new file mode 100644 index 0000000000000..a3e4c4b6972b6 --- /dev/null +++ b/google/cloud/storage/internal/async/open_object.h @@ -0,0 +1,117 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OPEN_OBJECT_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OPEN_OBJECT_H + +#include "google/cloud/storage/internal/async/open_stream.h" +#include "google/cloud/storage/internal/storage_stub.h" +#include "google/cloud/completion_queue.h" +#include "google/cloud/future.h" +#include "google/cloud/options.h" +#include "google/cloud/status.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/// Computes the `x-goog-request-params` for a bidi streaming read. +std::string RequestParams( + google::storage::v2::BidiReadObjectRequest const& request); + +/** + * Performs a single attempt to open a bidi-streaming read RPC. + * + * Before we can use a bidi-streaming read RPC we must call `Start()`, and + * send the initial message via `Write()`. + * + * Using C++20 coroutines we would write this as: + * + * @code + * using StreamingRpc = google::cloud::AsyncStreamingReadWriteRpc< + * google::storage::v2::BidiReadObjectRequest, + * google::storage::v2::BidiReadObjectResponse>; + * + * struct Result { + * std::unique_ptr rpc; + * google::storage::v2::BidiReadObjectResponse response; + * }; + * + * future> Open( + * storage_internal::StorageStub& stub, + * CompletionQueue& cq, + * std::shared_ptr context, + * google::cloud::internal::ImmutableOptions options, + * google::storage::v2::BidiReadObjectRequest request) { + * context->AddMetadata("x-goog-request-params", RequestParams(request)); + * auto rpc = stub->AsyncBidiReadObject( + * cq, std::move(context), std::move(options)); + * auto start = co_await rpc->Start(); + * if (!start) co_return co_await rpc->Finish(); + * auto write = co_await rpc->Write(request, grpc::WriteOptions{}); + * if (!write) co_return co_await rpc->Finish(); + * auto read = co_await rpc->Read(); + * if (!read) co_return co_await rpc->Finish(); + * co_return Result{std::move(rpc), std::move(*read)}; + * } + * @endcode + * + * As usual, all `co_await` calls because a callback. And all `co_return` calls + * must set the value in an explicit `google::cloud::promise<>` object. + */ +class OpenObject : public std::enable_shared_from_this { + public: + /// Create a coroutine to create an open a bidi streaming read RPC. + OpenObject(storage_internal::StorageStub& stub, CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::storage::v2::BidiReadObjectRequest request); + + /// Start the coroutine. + future> Call(); + + private: + std::weak_ptr WeakFromThis(); + + static std::unique_ptr CreateRpc( + storage_internal::StorageStub& stub, CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::storage::v2::BidiReadObjectRequest const& request); + + void OnStart(bool ok); + void OnWrite(bool ok); + void OnRead( + absl::optional response); + void DoFinish(); + void OnFinish(Status status); + + std::shared_ptr rpc_; + promise> promise_; + google::storage::v2::BidiReadObjectRequest initial_request_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OPEN_OBJECT_H diff --git a/google/cloud/storage/internal/async/open_object_test.cc b/google/cloud/storage/internal/async/open_object_test.cc new file mode 100644 index 0000000000000..5017b5ebf4c2e --- /dev/null +++ b/google/cloud/storage/internal/async/open_object_test.cc @@ -0,0 +1,397 @@ +// Copyright 2024 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/storage/internal/async/open_object.h" +#include "google/cloud/mocks/mock_async_streaming_read_write_rpc.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_storage_stub.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "google/cloud/testing_util/validate_metadata.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::MockStorageStub; +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::IsOkAndHolds; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::StatusIs; +using ::google::protobuf::TextFormat; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::Field; +using ::testing::NotNull; +using ::testing::Pair; + +using MockStream = google::cloud::mocks::MockAsyncStreamingReadWriteRpc< + google::storage::v2::BidiReadObjectRequest, + google::storage::v2::BidiReadObjectResponse>; + +TEST(OpenImpl, RequestParams) { + auto constexpr kPlain = R"pb( + read_object_spec { + bucket: "projects/_/buckets/test-bucket-name" + object: "test-object-unused" + generation: 42 + read_handle { handle: "unused" } + } + )pb"; + auto constexpr kWithRoutingToken = R"pb( + read_object_spec { + bucket: "projects/_/buckets/test-bucket-name" + object: "test-object-unused" + generation: 42 + read_handle { handle: "unused" } + routing_token: "test-routing-token" + } + )pb"; + + auto params = [](auto text) { + auto r = google::storage::v2::BidiReadObjectRequest{}; + TextFormat::ParseFromString(text, &r); + return RequestParams(r); + }; + EXPECT_EQ(params(kPlain), "bucket=projects/_/buckets/test-bucket-name"); + EXPECT_EQ(params(kWithRoutingToken), + "bucket=projects/_/buckets/test-bucket-name" + "&routing_token=test-routing-token"); +} + +TEST(OpenImpl, Basic) { + auto constexpr kText = R"pb( + bucket: "projects/_/buckets/test-bucket" + object: "test-object" + generation: 42 + )pb"; + auto constexpr kReadResponse = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-123" } + )pb"; + + auto request = google::storage::v2::BidiReadObjectRequest{}; + ASSERT_TRUE( + TextFormat::ParseFromString(kText, request.mutable_read_object_spec())); + auto expected_response = google::storage::v2::BidiReadObjectResponse{}; + ASSERT_TRUE(TextFormat::ParseFromString(kReadResponse, &expected_response)); + + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject) + .WillOnce([&](CompletionQueue const&, + std::shared_ptr const& context, + google::cloud::internal::ImmutableOptions const&) { + google::cloud::testing_util::ValidateMetadataFixture md; + auto metadata = md.GetMetadata(*context); + EXPECT_THAT(metadata, + Contains(Pair("x-goog-request-params", + "bucket=projects/_/buckets/test-bucket"))); + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write) + .WillOnce( + [&sequencer, request]( + google::storage::v2::BidiReadObjectRequest const& actual, + grpc::WriteOptions) { + EXPECT_THAT(actual, IsProtoEqual(request)); + return sequencer.PushBack("Write").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Read).WillOnce([&sequencer, expected_response]() { + return sequencer.PushBack("Read").then([expected_response](auto) { + return absl::make_optional(expected_response); + }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), request); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(true); + + auto response = pending.get(); + auto expected_result = [expected_response]() { + return AllOf(Field(&OpenStreamResult::stream, NotNull()), + Field(&OpenStreamResult::first_response, + IsProtoEqual(expected_response))); + }; + ASSERT_THAT(response, IsOkAndHolds(expected_result())); +} + +TEST(OpenImpl, BasicReadHandle) { + auto constexpr kText = R"pb( + bucket: "projects/_/buckets/test-bucket" + object: "test-object" + generation: 42 + read_handle { handle: "test-handle-1234" } + routing_token: "test-token" + )pb"; + auto constexpr kReadResponse = R"pb( + metadata { + bucket: "projects/_/buckets/test-bucket" + name: "test-object" + generation: 42 + } + read_handle { handle: "handle-123" } + )pb"; + + auto request = google::storage::v2::BidiReadObjectRequest{}; + ASSERT_TRUE( + TextFormat::ParseFromString(kText, request.mutable_read_object_spec())); + auto expected_response = google::storage::v2::BidiReadObjectResponse{}; + ASSERT_TRUE(TextFormat::ParseFromString(kReadResponse, &expected_response)); + + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject) + .WillOnce([&](CompletionQueue const&, + std::shared_ptr const& context, + google::cloud::internal::ImmutableOptions const&) { + google::cloud::testing_util::ValidateMetadataFixture md; + auto metadata = md.GetMetadata(*context); + EXPECT_THAT(metadata, + Contains(Pair("x-goog-request-params", + "bucket=projects/_/buckets/test-bucket" + "&routing_token=test-token"))); + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write) + .WillOnce( + [&sequencer, expected_request = request]( + google::storage::v2::BidiReadObjectRequest const& request, + grpc::WriteOptions) { + EXPECT_THAT(request, IsProtoEqual(expected_request)); + return sequencer.PushBack("Write").then( + [](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Read).WillOnce([&sequencer, expected_response]() { + return sequencer.PushBack("Read").then([expected_response](auto) { + return absl::make_optional(expected_response); + }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), request); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(true); + + auto response = pending.get(); + auto expected_result = [expected_response]() { + return AllOf(Field(&OpenStreamResult::stream, NotNull()), + Field(&OpenStreamResult::first_response, + IsProtoEqual(expected_response))); + }; + ASSERT_THAT(response, IsOkAndHolds(expected_result())); +} + +TEST(OpenImpl, StartError) { + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject).WillOnce([&sequencer]() { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), + google::storage::v2::BidiReadObjectRequest{}); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(false); // simulate an error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto response = pending.get(); + EXPECT_THAT(response, StatusIs(PermanentError().code())); +} + +TEST(OpenImpl, WriteError) { + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject).WillOnce([&sequencer]() { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), + google::storage::v2::BidiReadObjectRequest{}); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(false); // simulate an error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto response = pending.get(); + EXPECT_THAT(response, StatusIs(PermanentError().code())); +} + +TEST(OpenImpl, ReadError) { + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject).WillOnce([&sequencer]() { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Read).WillOnce([&sequencer]() { + return sequencer.PushBack("Read").then([](auto) { + return absl::optional(); + }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), + google::storage::v2::BidiReadObjectRequest{}); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(false); // simulate an error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto response = pending.get(); + EXPECT_THAT(response, StatusIs(PermanentError().code())); +} + +TEST(OpenImpl, UnexpectedFinish) { + AsyncSequencer sequencer; + MockStorageStub mock; + EXPECT_CALL(mock, AsyncBidiReadObject).WillOnce([&sequencer]() { + auto stream = std::make_unique(); + EXPECT_CALL(*stream, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*stream, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then([](auto) { return Status{}; }); + }); + return std::unique_ptr(std::move(stream)); + }); + + CompletionQueue cq; + auto coro = std::make_shared( + mock, cq, std::make_shared(), + internal::MakeImmutableOptions({}), + google::storage::v2::BidiReadObjectRequest{}); + auto pending = coro->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(false); // simulate an error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto response = pending.get(); + EXPECT_THAT(response, StatusIs(StatusCode::kInternal)); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/open_stream.cc b/google/cloud/storage/internal/async/open_stream.cc new file mode 100644 index 0000000000000..e613b266c19b5 --- /dev/null +++ b/google/cloud/storage/internal/async/open_stream.cc @@ -0,0 +1,88 @@ +// Copyright 2024 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/storage/internal/async/open_stream.h" +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +OpenStream::OpenStream(std::unique_ptr rpc) + : rpc_(std::move(rpc)) {} + +void OpenStream::Cancel() { + cancel_.store(true); + rpc_->Cancel(); + MaybeFinish(); +} + +future OpenStream::Start() { + if (cancel_) return make_ready_future(false); + return rpc_->Start(); +} + +future OpenStream::Write( + google::storage::v2::BidiReadObjectRequest const& request) { + if (cancel_) return make_ready_future(false); + pending_write_.store(true); + return rpc_->Write(request, grpc::WriteOptions{}) + .then([s = shared_from_this()](auto f) mutable { + auto self = std::move(s); + self->OnWrite(); + return f.get(); + }); +} + +future OpenStream::Read() { + if (cancel_) return make_ready_future(ReadType(absl::nullopt)); + pending_read_.store(true); + return rpc_->Read().then([s = shared_from_this()](auto f) mutable { + auto self = std::move(s); + self->OnRead(); + return f.get(); + }); +} + +future OpenStream::Finish() { + if (finish_issued_.exchange(true)) return {}; + return rpc_->Finish().then([s = shared_from_this()](auto f) mutable { + auto result = f.get(); + s.reset(); + return result; + }); +} + +void OpenStream::OnWrite() { + pending_write_.store(false); + return MaybeFinish(); +} + +void OpenStream::OnRead() { + pending_read_.store(false); + return MaybeFinish(); +} + +void OpenStream::MaybeFinish() { + if (!cancel_) return; + // Once `cancel_` is true these can only become false. + if (pending_read_ || pending_write_) return; + Finish(); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/open_stream.h b/google/cloud/storage/internal/async/open_stream.h new file mode 100644 index 0000000000000..bea42960f1336 --- /dev/null +++ b/google/cloud/storage/internal/async/open_stream.h @@ -0,0 +1,96 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_OPEN_STREAM_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OPEN_STREAM_H + +#include "google/cloud/async_streaming_read_write_rpc.h" +#include "google/cloud/future.h" +#include "google/cloud/status.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include "absl/types/optional.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * An open bidi streaming read RPC. + * + * gRPC imposes a number of restrictions on how to close bidi streaming RPCs. + * This class automates most of these restrictions. In particular, it waits + * (using background threads) until all pending `Read()` and `Write()` calls + * complete before trying to `Finish()` and then delete the stream. + * + * gRPC will assert if one deletes a streaming read-write RPC before waiting for + * the result of `Finish()` calls. It will also assert if one calls `Finish()` + * while there are pending `Read()` or `Write()` calls. + * + * This class tracks what operations, if any, are pending. On cancel, it waits + * until all pending operations complete, then calls `Finish()` and then deletes + * the streaming RPC (and itself). + * + * One may wonder if this class should be a reusable feature in the common + * libraries. That may be, but there are not enough use-cases to determine how + * a generalized version would look like. And in other cases (e.g. + * `AsyncWriterConnectionImpl`) the usage was restricted in ways that did not + * require this helper class. + */ +class OpenStream : public std::enable_shared_from_this { + public: + using StreamingRpc = google::cloud::AsyncStreamingReadWriteRpc< + google::storage::v2::BidiReadObjectRequest, + google::storage::v2::BidiReadObjectResponse>; + using ReadType = absl::optional; + + explicit OpenStream(std::unique_ptr rpc); + + void Cancel(); + future Start(); + future Write(google::storage::v2::BidiReadObjectRequest const& request); + future Read(); + future Finish(); + + private: + void OnWrite(); + void OnRead(); + void MaybeFinish(); + + std::atomic cancel_{false}; + std::atomic pending_read_{false}; + std::atomic pending_write_{false}; + std::atomic finish_issued_{false}; + std::unique_ptr rpc_; +}; + +struct OpenStreamResult { + std::shared_ptr stream; + google::storage::v2::BidiReadObjectResponse first_response; +}; + +using OpenStreamFactory = std::function>( + google::storage::v2::BidiReadObjectRequest)>; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_OPEN_STREAM_H diff --git a/google/cloud/storage/internal/async/open_stream_test.cc b/google/cloud/storage/internal/async/open_stream_test.cc new file mode 100644 index 0000000000000..eade037781006 --- /dev/null +++ b/google/cloud/storage/internal/async/open_stream_test.cc @@ -0,0 +1,126 @@ +// Copyright 2024 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/storage/internal/async/open_stream.h" +#include "google/cloud/mocks/mock_async_streaming_read_write_rpc.h" +#include "google/cloud/internal/make_status.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/status_matchers.h" +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::StatusIs; +using ReadType = ::google::cloud::storage_internal::OpenStream::ReadType; + +using MockStream = google::cloud::mocks::MockAsyncStreamingReadWriteRpc< + google::storage::v2::BidiReadObjectRequest, + google::storage::v2::BidiReadObjectResponse>; + +TEST(OpenStream, CancelBlocksAllRequest) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Start).Times(0); + EXPECT_CALL(*mock, Write).Times(0); + EXPECT_CALL(*mock, Read).Times(0); + EXPECT_CALL(*mock, Cancel).Times(1); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer] { + return sequencer.PushBack("Finish").then([](auto) { + return internal::CancelledError("test-only", GCP_ERROR_INFO()); + }); + }); + + auto actual = std::make_shared(std::move(mock)); + actual->Cancel(); + + EXPECT_FALSE(actual->Start().get()); + EXPECT_FALSE(actual->Write({}).get()); + EXPECT_FALSE(actual->Read().get().has_value()); + + actual.reset(); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); +} + +TEST(OpenStream, DuplicateFinish) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Cancel).Times(1); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer] { + return sequencer.PushBack("Finish").then([](auto) { + return internal::CancelledError("test-only", GCP_ERROR_INFO()); + }); + }); + + auto actual = std::make_shared(std::move(mock)); + + auto f1 = actual->Finish(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + EXPECT_THAT(f1.get(), StatusIs(StatusCode::kCancelled)); + + actual->Cancel(); + actual.reset(); +} + +TEST(OpenStream, CleanShutdown) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Write).WillOnce([&sequencer] { + return sequencer.PushBack("Write").then([](auto) { return false; }); + }); + EXPECT_CALL(*mock, Read).WillOnce([&sequencer] { + return sequencer.PushBack("Read").then( + [](auto) { return ReadType(absl::nullopt); }); + }); + EXPECT_CALL(*mock, Cancel).Times(1); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer] { + return sequencer.PushBack("Finish").then([](auto) { + return internal::CancelledError("test-only", GCP_ERROR_INFO()); + }); + }); + + auto actual = std::make_shared(std::move(mock)); + auto write = actual->Write(google::storage::v2::BidiReadObjectRequest{}); + auto ws = sequencer.PopFrontWithName(); + EXPECT_EQ(ws.second, "Write"); + auto read = actual->Read(); + auto rs = sequencer.PopFrontWithName(); + EXPECT_EQ(rs.second, "Read"); + + actual->Cancel(); + actual.reset(); + + ws.first.set_value(true); + rs.first.set_value(true); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/partial_upload.cc b/google/cloud/storage/internal/async/partial_upload.cc index 1a45b2ac85b67..801c1f52fcd0b 100644 --- a/google/cloud/storage/internal/async/partial_upload.cc +++ b/google/cloud/storage/internal/async/partial_upload.cc @@ -51,9 +51,12 @@ void PartialUpload::Write() { auto wopt = grpc::WriteOptions{}; auto const last_message = data_.empty(); if (last_message) { - if (action_ == LastMessageAction::kFinalize) { + if (action_ == LastMessageAction::kFinalizeWithChecksum) { auto status = Finalize(request_, wopt, *hash_function_); if (!status.ok()) return WriteError(std::move(status)); + } else if (action_ == LastMessageAction::kFinalize) { + request_.set_finish_write(true); + wopt.set_last_message(); } else if (action_ == LastMessageAction::kFlush) { request_.set_flush(true); request_.set_state_lookup(true); diff --git a/google/cloud/storage/internal/async/partial_upload.h b/google/cloud/storage/internal/async/partial_upload.h index 1d7196a4dbfec..f15ae1bb83526 100644 --- a/google/cloud/storage/internal/async/partial_upload.h +++ b/google/cloud/storage/internal/async/partial_upload.h @@ -90,7 +90,7 @@ class PartialUpload : public std::enable_shared_from_this { google::storage::v2::BidiWriteObjectRequest, google::storage::v2::BidiWriteObjectResponse>; - enum LastMessageAction { kNone, kFlush, kFinalize }; + enum LastMessageAction { kNone, kFlush, kFinalize, kFinalizeWithChecksum }; static std::shared_ptr Call( std::shared_ptr rpc, diff --git a/google/cloud/storage/internal/async/partial_upload_test.cc b/google/cloud/storage/internal/async/partial_upload_test.cc index e9c42049f7564..35b8a980f99bd 100644 --- a/google/cloud/storage/internal/async/partial_upload_test.cc +++ b/google/cloud/storage/internal/async/partial_upload_test.cc @@ -47,6 +47,39 @@ std::string RandomData(google::cloud::internal::DefaultPRNG& generator, "abcdefghijklmnopqrstuvwxyz0123456789"); } +TEST(PartialUpload, FinalizeEmptyWithoutChecksum) { + AsyncSequencer sequencer; + + auto rpc = std::make_unique(); + EXPECT_CALL(*rpc, Write) + .WillOnce([&sequencer](Request const& request, grpc::WriteOptions wopt) { + EXPECT_EQ(request.write_offset(), 0); + EXPECT_FALSE(request.has_write_object_spec()); + EXPECT_FALSE(request.has_append_object_spec()); + EXPECT_FALSE(request.has_upload_id()); + EXPECT_TRUE(request.finish_write()); + EXPECT_FALSE(request.has_object_checksums()); + EXPECT_EQ(request.checksummed_data().crc32c(), 0); + EXPECT_EQ(request.object_checksums().crc32c(), 0); + EXPECT_TRUE(wopt.is_last_message()); + return sequencer.PushBack("Write"); + }); + + auto hash = std::make_unique(); + Request request; + auto call = PartialUpload::Call(std::move(rpc), std::move(hash), request, + absl::Cord(), PartialUpload::kFinalize); + auto result = call->Start(); + + auto next = sequencer.PopFrontWithName(); + EXPECT_THAT(next.second, "Write"); + next.first.set_value(true); + + ASSERT_TRUE(result.is_ready()); + auto success = result.get(); + EXPECT_THAT(success, IsOkAndHolds(true)); +} + TEST(PartialUpload, FinalizeEmpty) { AsyncSequencer sequencer; @@ -68,8 +101,9 @@ TEST(PartialUpload, FinalizeEmpty) { auto hash = std::make_unique(); Request request; request.set_upload_id("test-upload-id"); - auto call = PartialUpload::Call(std::move(rpc), std::move(hash), request, - absl::Cord(), PartialUpload::kFinalize); + auto call = + PartialUpload::Call(std::move(rpc), std::move(hash), request, + absl::Cord(), PartialUpload::kFinalizeWithChecksum); auto result = call->Start(); @@ -119,7 +153,8 @@ TEST(PartialUpload, FinalizeChunkAligned) { Request request; request.set_upload_id("test-upload-id"); auto call = PartialUpload::Call(std::move(rpc), std::move(hash), request, - absl::Cord(buffer), PartialUpload::kFinalize); + absl::Cord(buffer), + PartialUpload::kFinalizeWithChecksum); auto result = call->Start(); @@ -184,7 +219,8 @@ TEST(PartialUpload, FinalizeChunkPartial) { Request request; request.set_upload_id("test-upload-id"); auto call = PartialUpload::Call(std::move(rpc), std::move(hash), request, - absl::Cord(buffer), PartialUpload::kFinalize); + absl::Cord(buffer), + PartialUpload::kFinalizeWithChecksum); auto result = call->Start(); @@ -464,8 +500,9 @@ TEST(PartialUpload, ErrorOnChecksums) { storage::internal::HashValues{"invalid", ""}); Request request; request.set_upload_id("test-upload-id"); - auto call = PartialUpload::Call(std::move(rpc), std::move(hash), request, - absl::Cord(), PartialUpload::kFinalize); + auto call = + PartialUpload::Call(std::move(rpc), std::move(hash), request, + absl::Cord(), PartialUpload::kFinalizeWithChecksum); auto result = call->Start(); diff --git a/google/cloud/storage/internal/async/read_payload_impl.h b/google/cloud/storage/internal/async/read_payload_impl.h index 634193b2bfbcc..c8c5bada6016f 100644 --- a/google/cloud/storage/internal/async/read_payload_impl.h +++ b/google/cloud/storage/internal/async/read_payload_impl.h @@ -70,6 +70,11 @@ struct ReadPayloadImpl { storage::internal::HashValues hashes) { payload.object_hash_values_ = std::move(hashes); } + + static void Append(storage_experimental::ReadPayload& payload, + storage_experimental::ReadPayload new_data) { + payload.impl_.Append(std::move(new_data.impl_)); + } }; GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/storage/internal/async/read_range.cc b/google/cloud/storage/internal/async/read_range.cc new file mode 100644 index 0000000000000..516688067e533 --- /dev/null +++ b/google/cloud/storage/internal/async/read_range.cc @@ -0,0 +1,106 @@ +// Copyright 2024 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/storage/internal/async/read_range.h" +#include "google/cloud/storage/internal/async/read_payload_impl.h" +#include "google/cloud/storage/internal/grpc/ctype_cord_workaround.h" +#include "google/cloud/log.h" + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +bool ReadRange::IsDone() const { + std::lock_guard lk(mu_); + return status_.has_value(); +} + +absl::optional ReadRange::RangeForResume( + std::int64_t read_id) const { + auto range = google::storage::v2::ReadRange{}; + range.set_read_id(read_id); + std::lock_guard lk(mu_); + if (status_.has_value()) return absl::nullopt; + range.set_read_offset(offset_); + range.set_read_length(length_); + return range; +} + +future ReadRange::Read() { + std::unique_lock lk(mu_); + if (!payload_ && !status_) { + auto p = promise{}; + auto f = p.get_future(); + wait_.emplace(std::move(p)); + return f; + } + if (payload_) { + auto p = std::move(*payload_); + payload_.reset(); + return make_ready_future(ReadResponse(std::move(p))); + } + return make_ready_future(ReadResponse(*status_)); +} + +void ReadRange::OnFinish(Status status) { + std::unique_lock lk(mu_); + if (status_) return; + status_ = std::move(status); + if (!wait_) return; + auto p = std::move(*wait_); + wait_.reset(); + lk.unlock(); + p.set_value(*status_); +} + +void ReadRange::OnRead(google::storage::v2::ObjectRangeData data) { + std::unique_lock lk(mu_); + if (status_) return; + if (data.range_end()) status_ = Status{}; + + auto* check_summed_data = data.mutable_checksummed_data(); + auto content = StealMutableContent(*data.mutable_checksummed_data()); + auto status = + hash_function_->Update(offset_, content, check_summed_data->crc32c()); + if (!status.ok()) { + status_ = std::move(status); + return Notify(std::move(lk), ReadPayloadImpl::Make(std::move(content))); + } + + offset_ += content.size(); + if (length_ != 0) length_ -= std::min(content.size(), length_); + auto p = ReadPayloadImpl::Make(std::move(content)); + if (wait_) { + if (!payload_) return Notify(std::move(lk), std::move(p)); + GCP_LOG(FATAL) << "broken class invariant, `payload_` set when there is an" + << " active `wait_`."; + } + if (payload_) return ReadPayloadImpl::Append(*payload_, std::move(p)); + payload_ = std::move(p); +} + +void ReadRange::Notify(std::unique_lock lk, + storage_experimental::ReadPayload p) { + auto wait = std::move(*wait_); + wait_.reset(); + payload_.reset(); + lk.unlock(); + wait.set_value(std::move(p)); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/read_range.h b/google/cloud/storage/internal/async/read_range.h new file mode 100644 index 0000000000000..f955e15291c72 --- /dev/null +++ b/google/cloud/storage/internal/async/read_range.h @@ -0,0 +1,83 @@ +// Copyright 2024 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_STORAGE_INTERNAL_ASYNC_READ_RANGE_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_READ_RANGE_H + +#include "google/cloud/storage/async/reader_connection.h" +#include "google/cloud/storage/internal/hash_function.h" +#include "google/cloud/future.h" +#include "google/cloud/status.h" +#include "google/cloud/version.h" +#include "absl/types/optional.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * A read range represents a partially completed range download via a + * `ObjectDescriptor`. + * + * An `ObjectDescriptor` may have many active ranges at a time. The data for + * them may be interleaved, that is, data for ranges requested first may arrive + * second. The object descriptor implementation will demux these messages to an + * instance of this class. + */ +class ReadRange { + public: + using ReadResponse = + storage_experimental::AsyncReaderConnection::ReadResponse; + + ReadRange(std::int64_t offset, std::int64_t length, + std::shared_ptr hash_function = + storage::internal::CreateNullHashFunction()) + : offset_(offset), + length_(length), + hash_function_(std::move(hash_function)) {} + + bool IsDone() const; + + absl::optional RangeForResume( + std::int64_t read_id) const; + + future Read(); + void OnFinish(Status status); + + void OnRead(google::storage::v2::ObjectRangeData data); + + private: + void Notify(std::unique_lock lk, + storage_experimental::ReadPayload p); + + mutable std::mutex mu_; + std::int64_t offset_; + std::int64_t length_; + absl::optional payload_; + absl::optional status_; + absl::optional> wait_; + std::shared_ptr hash_function_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_READ_RANGE_H diff --git a/google/cloud/storage/internal/async/read_range_test.cc b/google/cloud/storage/internal/async/read_range_test.cc new file mode 100644 index 0000000000000..3a4cd28a140ba --- /dev/null +++ b/google/cloud/storage/internal/async/read_range_test.cc @@ -0,0 +1,198 @@ +// Copyright 2024 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/storage/internal/async/read_range.h" +#include "google/cloud/storage/internal/grpc/ctype_cord_workaround.h" +#include "google/cloud/storage/internal/hash_function.h" +#include "google/cloud/storage/internal/hash_values.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_hash_function.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "absl/strings/cord.h" +#include "absl/strings/string_view.h" +#include +#include +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::MockHashFunction; +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::storage_experimental::ReadPayload; +using ::google::cloud::testing_util::IsOk; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::StatusIs; +using ::google::protobuf::TextFormat; +using ::testing::_; +using ::testing::An; +using ::testing::AtLeast; +using ::testing::ElementsAre; +using ::testing::Optional; +using ::testing::ResultOf; +using ::testing::VariantWith; + +using HashUpdateType = + std::conditional_t::value, + absl::Cord const&, absl::string_view>; + +TEST(ReadRange, BasicLifecycle) { + ReadRange actual(10000, 40); + EXPECT_FALSE(actual.IsDone()); + auto range = google::storage::v2::ReadRange{}; + auto constexpr kRange0 = R"pb( + read_id: 7 read_offset: 10000 read_length: 40 + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kRange0, &range)); + EXPECT_THAT(actual.RangeForResume(7), Optional(IsProtoEqual(range))); + + auto pending = actual.Read(); + EXPECT_FALSE(pending.is_ready()); + + auto data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData0 = R"pb( + checksummed_data { content: "0123456789" } + read_range { read_offset: 10000 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData0, &data)); + actual.OnRead(std::move(data)); + + EXPECT_TRUE(pending.is_ready()); + EXPECT_THAT(pending.get(), + VariantWith(ResultOf( + "contents", [](ReadPayload const& p) { return p.contents(); }, + ElementsAre("0123456789")))); + range = google::storage::v2::ReadRange{}; + auto constexpr kRange1 = R"pb( + read_id: 7 read_offset: 10010 read_length: 30 + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kRange1, &range)); + EXPECT_THAT(actual.RangeForResume(7), Optional(IsProtoEqual(range))); + + auto constexpr kData1 = R"pb( + checksummed_data { content: "1234567890" } + read_range { read_offset: 10020 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData1, &data)); + actual.OnRead(std::move(data)); + + pending = actual.Read(); + EXPECT_TRUE(pending.is_ready()); + EXPECT_THAT(pending.get(), + VariantWith(ResultOf( + "contents", [](ReadPayload const& p) { return p.contents(); }, + ElementsAre("1234567890")))); + + data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData2 = R"pb( + checksummed_data { content: "2345678901" } + read_range { read_offset: 10030 read_length: 10 read_id: 7 } + range_end: true + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData2, &data)); + actual.OnRead(std::move(data)); + + EXPECT_TRUE(actual.IsDone()); + EXPECT_FALSE(actual.RangeForResume(7).has_value()); + + pending = actual.Read(); + EXPECT_TRUE(pending.is_ready()); + EXPECT_THAT(pending.get(), + VariantWith(ResultOf( + "contents", [](ReadPayload const& p) { return p.contents(); }, + ElementsAre("2345678901")))); + + actual.OnFinish(Status{}); + EXPECT_THAT(actual.Read().get(), VariantWith(IsOk())); + // A second Read() should be harmless. + EXPECT_THAT(actual.Read().get(), VariantWith(IsOk())); +} + +TEST(ReadRange, Error) { + ReadRange actual(10000, 40); + auto pending = actual.Read(); + EXPECT_FALSE(pending.is_ready()); + actual.OnFinish(PermanentError()); + + EXPECT_THAT(pending.get(), + VariantWith(StatusIs(PermanentError().code()))); +} + +TEST(ReadRange, Queue) { + ReadRange actual(10000, 40); + + auto data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData0 = R"pb( + checksummed_data { content: "0123456789" } + read_range { read_offset: 10000 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData0, &data)); + actual.OnRead(std::move(data)); + + auto constexpr kData1 = R"pb( + checksummed_data { content: "1234567890" } + read_range { read_offset: 10020 read_length: 10 read_id: 7 } + range_end: false + )pb"; + EXPECT_TRUE(TextFormat::ParseFromString(kData1, &data)); + actual.OnRead(std::move(data)); + + auto matcher = ResultOf( + "contents", + [](ReadPayload const& p) { + // For small strings, absl::Cord may choose to merge the strings into a + // single value. Testing with `ElementsAre()` won't work in all + // environments. + std::string m; + for (auto sv : p.contents()) { + m += std::string(sv); + } + return m; + }, + "01234567891234567890"); + EXPECT_THAT(actual.Read().get(), VariantWith(matcher)); +} + +TEST(ReadRange, HashFunctionCalled) { + auto hash_function = std::make_shared(); + EXPECT_CALL(*hash_function, Update(0, An(), _)) + .Times(AtLeast(1)); + + ReadRange actual(0, 10, hash_function); + auto data = google::storage::v2::ObjectRangeData{}; + auto constexpr kData0 = R"pb( + checksummed_data { content: "1234567890" } + read_range { read_offset: 0 read_length: 10 read_id: 7 } + range_end: false + )pb"; + + EXPECT_TRUE(TextFormat::ParseFromString(kData0, &data)); + actual.OnRead(std::move(data)); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/write_object.cc b/google/cloud/storage/internal/async/write_object.cc new file mode 100644 index 0000000000000..065a4384dcd09 --- /dev/null +++ b/google/cloud/storage/internal/async/write_object.cc @@ -0,0 +1,80 @@ +// 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. + +#include "google/cloud/storage/internal/async/write_object.h" +#include "google/cloud/internal/absl_str_cat_quiet.h" +#include "google/cloud/internal/make_status.h" +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +WriteObject::WriteObject(std::unique_ptr rpc, + google::storage::v2::BidiWriteObjectRequest request) + : rpc_(std::move(rpc)), initial_request_(std::move(request)) {} + +future> WriteObject::Call() { + auto future = promise_.get_future(); + rpc_->Start().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnStart(f.get()); + }); + return future; +} + +std::weak_ptr WriteObject::WeakFromThis() { + return shared_from_this(); +} + +void WriteObject::OnStart(bool ok) { + if (!ok) return DoFinish(); + rpc_->Write(initial_request_, grpc::WriteOptions{}) + .then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnWrite(f.get()); + }); +} + +void WriteObject::OnWrite(bool ok) { + if (!ok) return DoFinish(); + rpc_->Read().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnRead(f.get()); + }); +} + +void WriteObject::OnRead( + absl::optional response) { + if (!response) return DoFinish(); + promise_.set_value(WriteResult{std::move(rpc_), std::move(*response)}); +} + +void WriteObject::DoFinish() { + rpc_->Finish().then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) self->OnFinish(f.get()); + }); +} + +void WriteObject::OnFinish(Status status) { + if (!status.ok()) return promise_.set_value(std::move(status)); + // This should not happen, it indicates an EOF on the stream, but we + // did not ask to close it. + promise_.set_value(google::cloud::internal::InternalError( + "could not open stream, but the stream closed successfully", + GCP_ERROR_INFO())); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/write_object.h b/google/cloud/storage/internal/async/write_object.h new file mode 100644 index 0000000000000..e6d6be1fec99c --- /dev/null +++ b/google/cloud/storage/internal/async/write_object.h @@ -0,0 +1,102 @@ +// 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_STORAGE_INTERNAL_ASYNC_WRITE_OBJECT_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_WRITE_OBJECT_H + +#include "google/cloud/storage/internal/storage_stub.h" +#include "google/cloud/completion_queue.h" +#include "google/cloud/future.h" +#include "google/cloud/options.h" +#include "google/cloud/status.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * Performs a single attempt to open a bidi-streaming write RPC. + * + * Before we can use a bidi-streaming write RPC we must call `Start()`, and + * then call `Read()` to check the RPC start was successful. + * + * Using C++20 coroutines we would write this as: + * + * @code + * using StreamingRpc = google::cloud::AsyncStreamingReadWriteRpc< + * google::storage::v2::BidiWriteObjectRequest, + * google::storage::v2::BidiWriteObjectResponse>; + * + * + * future> Call( + * StreamingRpc rpc, + * google::storage::v2::BidiWriteObjectRequest request) { + * auto start = co_await rpc->Start(); + * if (!start) co_return co_await rpc->Finish(); + * auto read = co_await rpc->Read(); + * if (!read) co_return co_await rpc->Finish(); + * co_return std::move(*read); + * } + * @endcode + * + * As usual, all `co_await` calls become a callback. And all `co_return` calls + * must set the value in an explicit `google::cloud::promise<>` object. + */ +class WriteObject : public std::enable_shared_from_this { + public: + using StreamingRpc = google::cloud::AsyncStreamingReadWriteRpc< + google::storage::v2::BidiWriteObjectRequest, + google::storage::v2::BidiWriteObjectResponse>; + + using ReturnType = google::storage::v2::BidiWriteObjectResponse; + /// Create a coroutine to create an open a bidi streaming write RPC. + WriteObject(std::unique_ptr rpc, + google::storage::v2::BidiWriteObjectRequest request); + + struct WriteResult { + std::unique_ptr stream; + google::storage::v2::BidiWriteObjectResponse first_response; + }; + + /// Start the coroutine. + future> Call(); + + private: + std::weak_ptr WeakFromThis(); + + void OnStart(bool ok); + void OnWrite(bool ok); + void OnRead( + absl::optional response); + void DoFinish(); + void OnFinish(Status status); + + std::unique_ptr rpc_; + promise> promise_; + google::storage::v2::BidiWriteObjectRequest initial_request_; +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_WRITE_OBJECT_H diff --git a/google/cloud/storage/internal/async/write_object_test.cc b/google/cloud/storage/internal/async/write_object_test.cc new file mode 100644 index 0000000000000..586ccde802f1e --- /dev/null +++ b/google/cloud/storage/internal/async/write_object_test.cc @@ -0,0 +1,295 @@ +// 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. + +#include "google/cloud/storage/internal/async/write_object.h" +#include "google/cloud/mocks/mock_async_streaming_read_write_rpc.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_storage_stub.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "google/cloud/testing_util/validate_metadata.h" +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::canonical_errors::PermanentError; +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::IsOkAndHolds; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::StatusIs; +using ::google::protobuf::TextFormat; +using ::testing::AllOf; +using ::testing::Field; +using ::testing::NotNull; + +using MockStream = google::cloud::mocks::MockAsyncStreamingReadWriteRpc< + google::storage::v2::BidiWriteObjectRequest, + google::storage::v2::BidiWriteObjectResponse>; + +TEST(WriteObjectTest, Basic) { + auto constexpr kText = R"pb( + resource { bucket: "projects/_/buckets/test-bucket", name: "test-object" } + appendable: true + )pb"; + auto constexpr kWriteResponse = R"pb( + persisted_size: 2048 + write_handle { handle: "handle-123" } + )pb"; + + auto request = google::storage::v2::BidiWriteObjectRequest{}; + ASSERT_TRUE( + TextFormat::ParseFromString(kText, request.mutable_write_object_spec())); + auto expected_response = google::storage::v2::BidiWriteObjectResponse{}; + ASSERT_TRUE(TextFormat::ParseFromString(kWriteResponse, &expected_response)); + + AsyncSequencer sequencer; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + + EXPECT_CALL(*mock, Write) + .WillOnce([&sequencer, request]( + google::storage::v2::BidiWriteObjectRequest const& actual, + grpc::WriteOptions) { + EXPECT_THAT(actual, IsProtoEqual(request)); + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + + EXPECT_CALL(*mock, Read).WillOnce([&sequencer, expected_response]() { + return sequencer.PushBack("Read").then([expected_response](auto) { + return absl::make_optional(expected_response); + }); + }); + + auto write_object = std::make_shared(std::move(mock), request); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(true); + + auto result = pending.get(); + auto expected_result = [expected_response]() { + return AllOf(Field(&WriteObject::WriteResult::stream, NotNull()), + Field(&WriteObject::WriteResult::first_response, + IsProtoEqual(expected_response))); + }; + ASSERT_THAT(result, IsOkAndHolds(expected_result())); +} + +TEST(WriteObject, BasicWriteHandle) { + auto constexpr kText = R"pb( + bucket: "projects/_/buckets/test-bucket" + object: "test-object" + generation: 42 + routing_token: "test-routing-token" + write_handle { handle: "handle-123" } + )pb"; + + auto constexpr kWriteResponse = R"pb( + resource { bucket: "projects/_/buckets/test-bucket" name: "test-object" } + write_handle { handle: "handle-123" } + )pb"; + + auto request = google::storage::v2::BidiWriteObjectRequest{}; + ASSERT_TRUE( + TextFormat::ParseFromString(kText, request.mutable_append_object_spec())); + auto expected_response = google::storage::v2::BidiWriteObjectResponse{}; + ASSERT_TRUE(TextFormat::ParseFromString(kWriteResponse, &expected_response)); + + AsyncSequencer sequencer; + auto mock = std::make_unique(); + + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Write) + .WillOnce([&sequencer, expected_request = request]( + google::storage::v2::BidiWriteObjectRequest const& request, + grpc::WriteOptions) { + EXPECT_THAT(request, IsProtoEqual(expected_request)); + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Read).WillOnce([&sequencer, expected_response]() { + return sequencer.PushBack("Read").then([expected_response](auto) { + return absl::make_optional(expected_response); + }); + }); + + auto write_object = std::make_shared(std::move(mock), request); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(true); + + auto response = pending.get(); + auto expected_result = [expected_response]() { + return AllOf(Field(&WriteObject::WriteResult::stream, NotNull()), + Field(&WriteObject::WriteResult::first_response, + IsProtoEqual(expected_response))); + }; + ASSERT_THAT(response, IsOkAndHolds(expected_result())); +} + +TEST(WriteObject, StartError) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + + auto write_object = std::make_shared( + std::move(mock), google::storage::v2::BidiWriteObjectRequest{}); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(false); // simulate an error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto result = pending.get(); + EXPECT_THAT(result, StatusIs(PermanentError().code())); +} + +TEST(WriteObject, WriteError) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + + auto write_object = std::make_shared( + std::move(mock), google::storage::v2::BidiWriteObjectRequest{}); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(false); // simulate the error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto result = pending.get(); + EXPECT_THAT(result, StatusIs(PermanentError().code())); +} + +TEST(WriteObject, ReadError) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Read).WillOnce([&sequencer]() { + return sequencer.PushBack("Read").then([](auto) { + return absl::optional(); + }); + }); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then( + [](auto) { return PermanentError(); }); + }); + + auto write_object = std::make_shared( + std::move(mock), google::storage::v2::BidiWriteObjectRequest{}); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Read"); + next.first.set_value(false); // simulate the error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto result = pending.get(); + ASSERT_THAT(result, StatusIs(PermanentError().code())); +} + +TEST(WriteObject, UnexpectedFinish) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + + EXPECT_CALL(*mock, Start).WillOnce([&sequencer]() { + return sequencer.PushBack("Start").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Write).WillOnce([&sequencer]() { + return sequencer.PushBack("Write").then([](auto f) { return f.get(); }); + }); + EXPECT_CALL(*mock, Finish).WillOnce([&sequencer]() { + return sequencer.PushBack("Finish").then([](auto) { return Status{}; }); + }); + + auto write_object = std::make_shared( + std::move(mock), google::storage::v2::BidiWriteObjectRequest{}); + auto pending = write_object->Call(); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Start"); + next.first.set_value(true); + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Write"); + next.first.set_value(false); // simulate the error + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finish"); + next.first.set_value(true); + + auto result = pending.get(); + EXPECT_THAT(result, StatusIs(StatusCode::kInternal)); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/writer_connection_impl.cc b/google/cloud/storage/internal/async/writer_connection_impl.cc index ce81d0878dd84..53a0513ae62c0 100644 --- a/google/cloud/storage/internal/async/writer_connection_impl.cc +++ b/google/cloud/storage/internal/async/writer_connection_impl.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "google/cloud/storage/internal/async/writer_connection_impl.h" +#include "google/cloud/storage/internal/async/handle_redirect_error.h" #include "google/cloud/storage/internal/async/partial_upload.h" #include "google/cloud/storage/internal/async/write_payload_impl.h" #include "google/cloud/storage/internal/grpc/ctype_cord_workaround.h" @@ -48,11 +49,11 @@ AsyncWriterConnectionImpl::AsyncWriterConnectionImpl( google::storage::v2::BidiWriteObjectRequest request, std::unique_ptr impl, std::shared_ptr hash_function, - std::int64_t persisted_size) - : AsyncWriterConnectionImpl(std::move(options), std::move(request), - std::move(impl), std::move(hash_function), - PersistedStateType(persisted_size), - /*offset=*/persisted_size) {} + std::int64_t persisted_size, bool first_request) + : AsyncWriterConnectionImpl( + std::move(options), std::move(request), std::move(impl), + std::move(hash_function), PersistedStateType(persisted_size), + /*offset=*/persisted_size, std::move(first_request)) {} AsyncWriterConnectionImpl::AsyncWriterConnectionImpl( google::cloud::internal::ImmutableOptions options, @@ -70,13 +71,14 @@ AsyncWriterConnectionImpl::AsyncWriterConnectionImpl( google::storage::v2::BidiWriteObjectRequest request, std::unique_ptr impl, std::shared_ptr hash_function, - PersistedStateType persisted_state, std::int64_t offset) + PersistedStateType persisted_state, std::int64_t offset, bool first_request) : options_(std::move(options)), impl_(std::move(impl)), request_(std::move(request)), hash_function_(std::move(hash_function)), persisted_state_(std::move(persisted_state)), offset_(offset), + first_request_(std::move(first_request)), finished_(on_finish_.get_future()) { request_.clear_object_checksums(); request_.set_state_lookup(false); @@ -131,8 +133,11 @@ AsyncWriterConnectionImpl::Finalize( auto p = WritePayloadImpl::GetImpl(payload); auto size = p.size(); + auto action = request_.has_append_object_spec() + ? PartialUpload::kFinalize + : PartialUpload::kFinalizeWithChecksum; auto coro = PartialUpload::Call(impl_, hash_function_, std::move(write), - std::move(p), PartialUpload::kFinalize); + std::move(p), std::move(action)); return coro->Start().then([coro, size, this](auto f) mutable { coro.reset(); // breaks the cycle between the completion queue and coro return OnFinalUpload(size, f.get()); @@ -168,6 +173,8 @@ AsyncWriterConnectionImpl::MakeRequest() { first_request_ = false; } else { request.clear_upload_id(); + request.clear_write_object_spec(); + request.clear_append_object_spec(); } request.set_write_offset(offset_); return request; @@ -225,7 +232,13 @@ future> AsyncWriterConnectionImpl::OnQuery( return Finish() .then(HandleFinishAfterError( "Expected error in Finish() after non-ok Read()")) - .then([](auto f) { return StatusOr(f.get()); }); + .then([this](auto g) { + auto result = g.get(); + EnsureFirstMessageAppendObjectSpec(request_); + ApplyWriteRedirectErrors(*request_.mutable_append_object_spec(), + ExtractGrpcStatus(result)); + return StatusOr(std::move(result)); + }); } if (response->has_persisted_size()) { persisted_state_ = response->persisted_size(); diff --git a/google/cloud/storage/internal/async/writer_connection_impl.h b/google/cloud/storage/internal/async/writer_connection_impl.h index 2949a4a8a0f7d..d891761dad20e 100644 --- a/google/cloud/storage/internal/async/writer_connection_impl.h +++ b/google/cloud/storage/internal/async/writer_connection_impl.h @@ -42,7 +42,7 @@ class AsyncWriterConnectionImpl google::storage::v2::BidiWriteObjectRequest request, std::unique_ptr impl, std::shared_ptr hash_function, - std::int64_t persisted_size); + std::int64_t persisted_size, bool first_request = true); explicit AsyncWriterConnectionImpl( google::cloud::internal::ImmutableOptions options, google::storage::v2::BidiWriteObjectRequest request, @@ -72,7 +72,8 @@ class AsyncWriterConnectionImpl google::storage::v2::BidiWriteObjectRequest request, std::unique_ptr impl, std::shared_ptr hash_function, - PersistedStateType persisted_state, std::int64_t offset); + PersistedStateType persisted_state, std::int64_t offset, + bool first_request = true); google::storage::v2::BidiWriteObjectRequest MakeRequest(); @@ -90,7 +91,7 @@ class AsyncWriterConnectionImpl std::shared_ptr hash_function_; PersistedStateType persisted_state_; std::int64_t offset_ = 0; - bool first_request_ = true; + bool first_request_; // `Finish()` must be called exactly once. If it has not been called by the // time the destructor is reached, then the destructor will arrange for a diff --git a/google/cloud/storage/internal/async/writer_connection_resumed.cc b/google/cloud/storage/internal/async/writer_connection_resumed.cc new file mode 100644 index 0000000000000..99f3990b36339 --- /dev/null +++ b/google/cloud/storage/internal/async/writer_connection_resumed.cc @@ -0,0 +1,560 @@ +// 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. + +#include "google/cloud/storage/internal/async/writer_connection_resumed.h" +#include "google/cloud/storage/internal/async/write_payload_impl.h" +#include "google/cloud/future.h" +#include "google/cloud/internal/make_status.h" +#include "google/cloud/status.h" +#include "google/cloud/status_or.h" +#include "absl/strings/cord.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +Status MakeRewindError(std::int64_t resend_offset, std::int64_t persisted_size, + internal::ErrorInfoBuilder eib) { + auto const previous = std::to_string(resend_offset); + auto const returned = std::to_string(persisted_size); + return internal::InternalError( + "server persisted_size rewind. This indicates a bug in the client library" + " or the service.", + std::move(eib) + .WithMetadata("gcloud-cpp.storage.resend_offset", previous) + .WithMetadata("gcloud-cpp.storage.persisted_size", returned)); +} + +Status MakeFastForwardError(std::int64_t resend_offset, + std::int64_t persisted_size, + internal::ErrorInfoBuilder eib) { + auto const previous = std::to_string(resend_offset); + auto const returned = std::to_string(persisted_size); + return internal::InternalError( + "server persisted_size too high. This can be caused by concurrent" + " uploads using the same upload id. Most likely an application bug.", + std::move(eib) + .WithMetadata("gcloud-cpp.storage.resend_offset", previous) + .WithMetadata("gcloud-cpp.storage.persisted_size", returned)); +} + +class AsyncWriterConnectionResumedState + : public std::enable_shared_from_this { + public: + AsyncWriterConnectionResumedState( + WriterResultFactory factory, + std::unique_ptr impl, + google::storage::v2::BidiWriteObjectRequest initial_request, + std::shared_ptr hash_function, + Options const& options, std::size_t buffer_size_lwm, + std::size_t buffer_size_hwm) + : factory_(std::move(factory)), + impl_(std::move(impl)), + initial_request_(std::move(initial_request)), + hash_function_(std::move(hash_function)), + buffer_size_lwm_(buffer_size_lwm), + buffer_size_hwm_(buffer_size_hwm) { + finalized_future_ = finalized_.get_future(); + flushed_future_ = flushed_.get_future(); + options_ = internal::MakeImmutableOptions(options); + auto state = impl_->PersistedState(); + if (absl::holds_alternative(state)) { + SetFinalized(std::unique_lock(mu_), + absl::get(std::move(state))); + cancelled_ = true; + resume_status_ = internal::CancelledError("upload already finalized", + GCP_ERROR_INFO()); + return; + } + buffer_offset_ = absl::get(state); + } + + void Cancel() { + std::unique_lock lk(mu_); + cancelled_ = true; + auto impl = Impl(lk); + lk.unlock(); + return impl->Cancel(); + } + + std::string UploadId() const { + return UploadId(std::unique_lock(mu_)); + } + + absl::variant PersistedState() + const { + return Impl(std::unique_lock(mu_))->PersistedState(); + } + + future Write(storage_experimental::WritePayload const& p) { + std::unique_lock lk(mu_); + resend_buffer_.Append(WritePayloadImpl::GetImpl(p)); + return HandleNewData(std::move(lk)); + } + + future> Finalize( + storage_experimental::WritePayload const& p) { + std::unique_lock lk(mu_); + resend_buffer_.Append(WritePayloadImpl::GetImpl(p)); + finalize_ = true; + HandleNewData(std::move(lk)); + return std::move(finalized_future_); + } + + future Flush(storage_experimental::WritePayload const& p) { + std::unique_lock lk(mu_); + resend_buffer_.Append(WritePayloadImpl::GetImpl(p)); + flush_ = true; + HandleNewData(std::move(lk), true); + return std::move(flushed_future_); + } + + future> Query() { + return Impl(std::unique_lock(mu_))->Query(); + } + + RpcMetadata GetRequestMetadata() { + return Impl(std::unique_lock(mu_))->GetRequestMetadata(); + } + + private: + std::weak_ptr WeakFromThis() { + return shared_from_this(); + } + + // Cannot use `std::function<>` because we need to capture a move-only + // `promise`. Cannot use `absl::AnyInvocable<>` because we need to + // support versions of Abseil that lack such a class. + struct BufferShrinkHandler { + virtual ~BufferShrinkHandler() = default; + virtual void Execute(Status status) = 0; + }; + + static std::unique_ptr MakeLwmWaiter(promise p) { + struct Impl : public BufferShrinkHandler { + promise p; + + explicit Impl(promise pr) : p(std::move(pr)) {} + ~Impl() override = default; + void Execute(Status status) override { p.set_value(std::move(status)); } + }; + return std::make_unique(std::move(p)); + } + + future HandleNewData(std::unique_lock lk, + bool flush = false) { + if (!resume_status_.ok()) return make_ready_future(resume_status_); + auto const buffer_size = resend_buffer_.size(); + flush_ = (buffer_size >= buffer_size_lwm_) || flush; + auto result = make_ready_future(Status{}); + if (buffer_size >= buffer_size_hwm_) { + auto p = promise(); + result = p.get_future(); + flush_handlers_.push_back(MakeLwmWaiter(std::move(p))); + } + StartWriting(std::move(lk)); + return result; + } + + void StartWriting(std::unique_lock lk) { + if (writing_) return; + WriteLoop(std::move(lk)); + } + + void WriteLoop(std::unique_lock lk) { + writing_ = write_offset_ < resend_buffer_.size(); + if (!writing_ && !finalize_ && !flush_) return; + auto const n = resend_buffer_.size() - write_offset_; + auto payload = resend_buffer_.Subcord(write_offset_, n); + if (finalize_) return FinalizeStep(std::move(lk), std::move(payload)); + if (flush_) return FlushStep(std::move(lk), std::move(payload)); + WriteStep(std::move(lk), std::move(payload)); + } + + void FinalizeStep(std::unique_lock lk, absl::Cord payload) { + auto impl = Impl(lk); + lk.unlock(); + (void)impl->Finalize(WritePayloadImpl::Make(std::move(payload))) + .then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) return self->OnFinalize(f.get()); + }); + } + + void OnFinalize(StatusOr result) { + if (!result) return Resume(std::move(result).status()); + SetFinalized(std::unique_lock(mu_), std::move(result)); + } + + void FlushStep(std::unique_lock lk, absl::Cord payload) { + auto impl = Impl(lk); + lk.unlock(); + auto const size = payload.size(); + (void)impl->Flush(WritePayloadImpl::Make(std::move(payload))) + .then([size, w = WeakFromThis()](auto f) { + if (auto self = w.lock()) { + self->OnFlush(f.get(), size); + return; + } + }); + } + + void OnFlush(Status result, std::size_t write_size) { + if (!result.ok()) return Resume(std::move(result)); + std::unique_lock lk(mu_); + write_offset_ += write_size; + auto impl = Impl(lk); + lk.unlock(); + impl->Query().then([this, result, w = WeakFromThis()](auto f) { + SetFlushed(std::unique_lock(mu_), std::move(result)); + if (auto self = w.lock()) return self->OnQuery(f.get()); + }); + } + + void OnQuery(StatusOr persisted_size) { + if (!persisted_size) return Resume(std::move(persisted_size).status()); + return OnQuery(std::unique_lock(mu_), *persisted_size); + } + + auto ClearHandlers(std::unique_lock const& /* lk */) { + decltype(flush_handlers_) tmp; + flush_handlers_.swap(tmp); + return tmp; + } + + auto ClearHandlersIfEmpty(std::unique_lock const& /* lk */) { + decltype(flush_handlers_) tmp; + if (resend_buffer_.size() >= buffer_size_lwm_) return tmp; + flush_handlers_.swap(tmp); + return tmp; + } + + void OnQuery(std::unique_lock lk, std::int64_t persisted_size) { + if (persisted_size < buffer_offset_) { + return SetError( + std::move(lk), + MakeRewindError(buffer_offset_, persisted_size, GCP_ERROR_INFO())); + } + auto const n = persisted_size - buffer_offset_; + if (n > static_cast(resend_buffer_.size())) { + return SetError(std::move(lk), + MakeFastForwardError(buffer_offset_, persisted_size, + GCP_ERROR_INFO())); + } + resend_buffer_.RemovePrefix(static_cast(n)); + buffer_offset_ = persisted_size; + write_offset_ -= static_cast(n); + // If the buffer is small enough, collect all the handlers to notify them. + auto const handlers = ClearHandlersIfEmpty(lk); + WriteLoop(std::move(lk)); + // The notifications are deferred until the lock is released, as they might + // call back and try to acquire the lock. + for (auto const& h : handlers) { + h->Execute(Status{}); + } + } + + void WriteStep(std::unique_lock lk, absl::Cord payload) { + auto impl = Impl(lk); + lk.unlock(); + auto const size = payload.size(); + (void)impl->Write(WritePayloadImpl::Make(std::move(payload))) + .then([size, w = WeakFromThis()](auto f) { + if (auto self = w.lock()) return self->OnWrite(f.get(), size); + }); + } + + void OnWrite(Status result, std::size_t write_size) { + if (!result.ok()) return Resume(std::move(result)); + std::unique_lock lk(mu_); + write_offset_ += write_size; + return WriteLoop(std::move(lk)); + } + + void Resume(Status s) { + auto proto_status = ExtractGrpcStatus(s); + auto request = google::storage::v2::BidiWriteObjectRequest{}; + auto spec = initial_request_.write_object_spec(); + auto& append_object_spec = *request.mutable_append_object_spec(); + append_object_spec.set_bucket(spec.resource().bucket()); + append_object_spec.set_object(spec.resource().name()); + ApplyWriteRedirectErrors(append_object_spec, std::move(proto_status)); + + if (!s.ok() && cancelled_) { + return SetError(std::unique_lock(mu_), std::move(s)); + } + factory_(std::move(request)).then([w = WeakFromThis()](auto f) { + if (auto self = w.lock()) return self->OnResume(f.get()); + }); + } + + void OnResume(StatusOr res) { + std::unique_lock lk(mu_); + if (!res) return SetError(std::move(lk), std::move(res).status()); + auto state = impl_->PersistedState(); + if (absl::holds_alternative(state)) { + return SetFinalized(std::move(lk), absl::get( + std::move(state))); + } + impl_ = std::make_unique( + options_, initial_request_, std::move(res->stream), hash_function_, + absl::get(std::move(state)), false); + OnQuery(std::move(lk), absl::get(state)); + } + + void SetFinalized(std::unique_lock lk, + StatusOr r) { + if (!r) return SetError(std::move(lk), std::move(r).status()); + SetFinalized(std::move(lk), *std::move(r)); + } + + void SetFinalized(std::unique_lock lk, + google::storage::v2::Object object) { + resend_buffer_.Clear(); + writing_ = false; + finalize_ = false; + flush_ = false; + auto handlers = ClearHandlers(lk); + promise> finalized{null_promise_t{}}; + finalized.swap(finalized_); + lk.unlock(); + for (auto& h : handlers) h->Execute(Status{}); + finalized.set_value(std::move(object)); + } + + void SetFlushed(std::unique_lock lk, Status result) { + if (!result.ok()) return SetError(std::move(lk), std::move(result)); + writing_ = false; + finalize_ = false; + flush_ = false; + auto handlers = ClearHandlers(lk); + promise flushed{null_promise_t{}}; + flushed.swap(flushed_); + lk.unlock(); + for (auto& h : handlers) h->Execute(Status{}); + flushed.set_value(result); + } + + void SetError(std::unique_lock lk, Status status) { + resume_status_ = status; + writing_ = false; + finalize_ = false; + flush_ = false; + auto handlers = ClearHandlers(lk); + promise flushed{null_promise_t{}}; + flushed.swap(flushed_); + promise> finalized{null_promise_t{}}; + finalized.swap(finalized_); + lk.unlock(); + for (auto& h : handlers) h->Execute(status); + finalized.set_value(status); + flushed.set_value(std::move(status)); + } + + std::shared_ptr Impl( + std::unique_lock const& /*lk*/) const { + return impl_; + } + + std::string UploadId(std::unique_lock const& lk) const { + return Impl(lk)->UploadId(); + } + + // Creates new `impl_` instances when needed. + WriterResultFactory const factory_; + + // The remaining member variables need a mutex for access. The background + // threads may change them as the resend_buffer_ is drained and/or as the + // reconnect loop resets `impl_`. + // It may be possible to reduce locking overhead as only one background thread + // operates on these member variables at a time. That seems like too small an + // optimization to increase the complexity of the code. + std::mutex mutable mu_; + + // The state of the resume loop. Once the resume loop fails no more resume + // or write attempts are made. + Status resume_status_; + + // The current writer. + std::shared_ptr impl_; + + // The initial request. + google::storage::v2::BidiWriteObjectRequest initial_request_; + + std::shared_ptr hash_function_; + + google::cloud::internal::ImmutableOptions options_; + + // Request a server-side flush if the buffer goes over this threshold. + std::size_t const buffer_size_lwm_; + + // Stop sending data if the buffer goes over this threshold. Only + // start sending data again if the size goes below buffer_size_lwm_. + std::size_t const buffer_size_hwm_; + + // The result of calling `Finalize()`. Note that only one such call is ever + // made. + promise> finalized_; + + promise flushed_; + + // Retrieve the future in the constructor, as some operations reset + // finalized_. + future> finalized_future_; + + future flushed_future_; + + // The resend buffer. If there is an error, this will have all the data since + // the last persisted byte and will be resent. + // + // If this is larger than `buffer_size_hwm_` then `Write()`, and `Flush()` + // will return futures that become satisfied only once the buffer size gets + // below `buffer_size_lwm_`. + // + // Note that `Finalize()` does not block when the buffer gets too large. It + // always blocks on `finalized_`. + absl::Cord resend_buffer_; + + // If true, all the data to finalize an upload is in `resend_buffer_`. + bool finalize_ = false; + + // If true, all data should be uploaded with `Flush()`. + bool flush_ = true; + + // The offset for the first byte in the resend_buffer_. + std::int64_t buffer_offset_ = 0; + + // The offset in `resend_buffer_` for the last `impl_->Write()` call. + std::size_t write_offset_ = 0; + + // Handle buffer flush events. Some member functions want to be notified of + // permanent errors in the resume loop and changes in the buffer size. + // The most common cases included: + // - A Write() call that returns an unsatisfied future until the buffer size + // is small enough. + // - A Flush() call that returns an unsatisified future until the buffer is + // small enough. + std::vector> flush_handlers_; + + // True if the writing loop is activate. + bool writing_ = false; + + // True if cancelled, in which case any RPC failures are final. + bool cancelled_ = false; +}; + +/** + * Implements an `AsyncWriterConnection` that automatically resumes and resends + * data. + * + * This class is used in the implementation of + * `AsyncClient::StartBufferedUpload()`. Please see that function for the + * motivation. + * + * This implementation of `AsyncWriterConnection` keeps an in-memory + * `resend_buffer_` of type `absl::Cord`. New data is added to the end of the + * Cord. Flushed data is removed from the front of the Cord. + * + * Applications threads add data by calling `Write()` and `Finalize()`. + * + * The buffer is drained by an asynchronous loop running in background threads. + * This loop starts (if needed) when new data is appended to the + * `resend_buffer_`. If the buffer is neither full nor approaching fullness + * the loop calls `impl_->Write()` to upload data to the service. + * + * When the application finalizes an upload the loop calls `impl_->Finalize()` + * and sends any previously buffered data as well as the new data. + * + * If the buffer is getting full, the loop uses `impl_->Flush()` instead of + * `impl_->Write()` to upload data, and it also queries the status of the upload + * after each `impl_->Flush()` call. + * + * If any of these operations fail the loop resumes the upload using a factory + * function to create new `AsyncWriterConnection` instances. This class assumes + * that the factory function implements the retry loop. + * + * If the factory function returns an error the loop ends. + * + * The loop also ends if there are no more bytes to send in the resend buffer. + */ +class AsyncWriterConnectionResumed + : public storage_experimental::AsyncWriterConnection { + public: + explicit AsyncWriterConnectionResumed( + WriterResultFactory factory, + std::unique_ptr impl, + google::storage::v2::BidiWriteObjectRequest initial_request, + std::shared_ptr hash_function, + Options const& options) + : state_(std::make_shared( + std::move(factory), std::move(impl), std::move(initial_request), + std::move(hash_function), options, + options.get(), + options.get())) {} + + void Cancel() override { return state_->Cancel(); } + + std::string UploadId() const override { return state_->UploadId(); } + + absl::variant PersistedState() + const override { + return state_->PersistedState(); + } + + future Write(storage_experimental::WritePayload p) override { + return state_->Write(std::move(p)); + } + + future> Finalize( + storage_experimental::WritePayload p) override { + return state_->Finalize(std::move(p)); + } + + future Flush(storage_experimental::WritePayload p) override { + return state_->Flush(std::move(p)); + } + + future> Query() override { return state_->Query(); } + + RpcMetadata GetRequestMetadata() override { + return state_->GetRequestMetadata(); + } + + private: + std::shared_ptr state_; +}; + +} // namespace + +std::unique_ptr +MakeWriterConnectionResumed( + WriterResultFactory factory, + std::unique_ptr impl, + google::storage::v2::BidiWriteObjectRequest initial_request, + std::shared_ptr hash_function, + Options const& options) { + return absl::make_unique( + std::move(factory), std::move(impl), std::move(initial_request), + std::move(hash_function), std::move(options)); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/async/writer_connection_resumed.h b/google/cloud/storage/internal/async/writer_connection_resumed.h new file mode 100644 index 0000000000000..8f038bfa73e2a --- /dev/null +++ b/google/cloud/storage/internal/async/writer_connection_resumed.h @@ -0,0 +1,52 @@ +// 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_STORAGE_INTERNAL_ASYNC_WRITER_CONNECTION_RESUMED_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_WRITER_CONNECTION_RESUMED_H + +#include "google/cloud/storage/async/writer_connection.h" +#include "google/cloud/storage/internal/async/handle_redirect_error.h" +#include "google/cloud/storage/internal/async/write_object.h" +#include "google/cloud/storage/internal/async/writer_connection_impl.h" +#include "google/cloud/future.h" +#include "google/cloud/options.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +using WriterResultFactory = + std::function>( + google::storage::v2::BidiWriteObjectRequest)>; + +std::unique_ptr +MakeWriterConnectionResumed( + WriterResultFactory factory, + std::unique_ptr impl, + google::storage::v2::BidiWriteObjectRequest initial_request, + std::shared_ptr hash_function, + Options const& options); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_INTERNAL_ASYNC_WRITER_CONNECTION_RESUMED_H diff --git a/google/cloud/storage/internal/async/writer_connection_resumed_test.cc b/google/cloud/storage/internal/async/writer_connection_resumed_test.cc new file mode 100644 index 0000000000000..d4fb59ca98c67 --- /dev/null +++ b/google/cloud/storage/internal/async/writer_connection_resumed_test.cc @@ -0,0 +1,250 @@ +// 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. + +#include "google/cloud/storage/internal/async/writer_connection_resumed.h" +#include "google/cloud/storage/async/connection.h" +#include "google/cloud/storage/mocks/mock_async_writer_connection.h" +#include "google/cloud/storage/testing/canonical_errors.h" +#include "google/cloud/storage/testing/mock_hash_function.h" +#include "google/cloud/testing_util/async_sequencer.h" +#include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/status_matchers.h" +#include +#include + +namespace google { +namespace cloud { +namespace storage_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::storage::testing::canonical_errors::TransientError; +using ::google::cloud::storage_mocks::MockAsyncWriterConnection; +using ::google::cloud::testing_util::AsyncSequencer; +using ::google::cloud::testing_util::IsOkAndHolds; +using ::google::cloud::testing_util::IsProtoEqual; +using ::google::cloud::testing_util::StatusIs; +using ::testing::Return; +using ::testing::VariantWith; + +using MockFactory = + ::testing::MockFunction>( + google::storage::v2::BidiWriteObjectRequest)>; + +using MockStreamingRpc = + ::testing::MockFunction()>; + +absl::variant MakePersistedState( + std::int64_t persisted_size) { + return persisted_size; +} + +storage_experimental::WritePayload TestPayload(std::size_t n) { + return storage_experimental::WritePayload(std::string(n, 'A')); +} + +auto TestObject() { + auto object = google::storage::v2::Object{}; + object.set_bucket("projects/_/buckets/test-bucket"); + object.set_name("test-object"); + return object; +} + +TEST(WriteConnectionResumed, FinalizeEmpty) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + auto initial_request = google::storage::v2::BidiWriteObjectRequest{}; + + EXPECT_CALL(*mock, UploadId).WillOnce(Return("test-upload-id")); + EXPECT_CALL(*mock, PersistedState) + .WillRepeatedly(Return(MakePersistedState(0))); + EXPECT_CALL(*mock, Finalize).WillRepeatedly([&](auto) { + return sequencer.PushBack("Finalize") + .then([](auto f) -> StatusOr { + if (!f.get()) return TransientError(); + return TestObject(); + }); + }); + MockFactory mock_factory; + EXPECT_CALL(mock_factory, Call).Times(0); + + auto connection = + MakeWriterConnectionResumed(mock_factory.AsStdFunction(), std::move(mock), + initial_request, nullptr, Options{}); + EXPECT_EQ(connection->UploadId(), "test-upload-id"); + EXPECT_THAT(connection->PersistedState(), VariantWith(0)); + + auto finalize = connection->Finalize({}); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Finalize"); + next.first.set_value(true); + EXPECT_THAT(finalize.get(), IsOkAndHolds(IsProtoEqual(TestObject()))); +} + +TEST(WriteConnectionResumed, FinalizedOnConstruction) { + AsyncSequencer sequencer; + auto initial_request = google::storage::v2::BidiWriteObjectRequest{}; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id")); + EXPECT_CALL(*mock, PersistedState).WillRepeatedly(Return(TestObject())); + EXPECT_CALL(*mock, Finalize).Times(0); + + MockFactory mock_factory; + EXPECT_CALL(mock_factory, Call).Times(0); + + auto connection = + MakeWriterConnectionResumed(mock_factory.AsStdFunction(), std::move(mock), + initial_request, nullptr, Options{}); + EXPECT_EQ(connection->UploadId(), "test-upload-id"); + EXPECT_THAT( + connection->PersistedState(), + VariantWith(IsProtoEqual(TestObject()))); + + auto finalize = connection->Finalize({}); + EXPECT_TRUE(finalize.is_ready()); + EXPECT_THAT(finalize.get(), IsOkAndHolds(IsProtoEqual(TestObject()))); +} + +TEST(WriteConnectionResumed, Cancel) { + AsyncSequencer sequencer; + auto initial_request = google::storage::v2::BidiWriteObjectRequest{}; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id")); + EXPECT_CALL(*mock, PersistedState) + .WillRepeatedly(Return(MakePersistedState(0))); + EXPECT_CALL(*mock, Flush).WillOnce([&](auto) { + return sequencer.PushBack("Flush").then( + [](auto) { return Status(StatusCode::kCancelled, "cancel"); }); + }); + EXPECT_CALL(*mock, Cancel).Times(1); + + MockFactory mock_factory; + EXPECT_CALL(mock_factory, Call).Times(0); + + auto connection = + MakeWriterConnectionResumed(mock_factory.AsStdFunction(), std::move(mock), + initial_request, nullptr, Options{}); + + auto write = connection->Write(TestPayload(64 * 1024)); + ASSERT_FALSE(write.is_ready()); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Flush"); + connection->Cancel(); + next.first.set_value(true); + + ASSERT_TRUE(write.is_ready()); + auto status = write.get(); + EXPECT_THAT(status, StatusIs(StatusCode::kCancelled)); +} + +TEST(WriterConnectionResumed, FlushEmpty) { + AsyncSequencer sequencer; + auto initial_request = google::storage::v2::BidiWriteObjectRequest{}; + + auto mock = std::make_unique(); + EXPECT_CALL(*mock, PersistedState) + .WillRepeatedly(Return(MakePersistedState(0))); + EXPECT_CALL(*mock, Flush).WillOnce([&](auto const& p) { + EXPECT_TRUE(p.payload().empty()); + return sequencer.PushBack("Flush").then([](auto f) { + if (!f.get()) return TransientError(); + return Status{}; + }); + }); + EXPECT_CALL(*mock, Query).WillOnce([&]() { + return sequencer.PushBack("Query").then( + [](auto f) -> StatusOr { + if (!f.get()) return TransientError(); + return 0; + }); + }); + + MockFactory mock_factory; + EXPECT_CALL(mock_factory, Call).Times(0); + + auto connection = + MakeWriterConnectionResumed(mock_factory.AsStdFunction(), std::move(mock), + initial_request, nullptr, Options{}); + EXPECT_THAT(connection->PersistedState(), VariantWith(0)); + + auto flush = connection->Flush({}); + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Flush"); + next.first.set_value(true); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Query"); + next.first.set_value(true); + + EXPECT_THAT(flush.get(), StatusIs(StatusCode::kOk)); +} + +TEST(WriteConnectionResumed, FlushNonEmpty) { + AsyncSequencer sequencer; + auto mock = std::make_unique(); + auto initial_request = google::storage::v2::BidiWriteObjectRequest{}; + auto const payload = TestPayload(1024); + + EXPECT_CALL(*mock, PersistedState) + .WillRepeatedly(Return(MakePersistedState(0))); + EXPECT_CALL(*mock, Flush).WillOnce([&](auto const& p) { + EXPECT_EQ(p.payload(), payload.payload()); + return sequencer.PushBack("Flush").then([](auto f) { + if (!f.get()) return TransientError(); + return Status{}; + }); + }); + EXPECT_CALL(*mock, Query).WillOnce([&]() { + return sequencer.PushBack("Query").then( + [](auto f) -> StatusOr { + if (!f.get()) return TransientError(); + return 1024; + }); + }); + + MockFactory mock_factory; + EXPECT_CALL(mock_factory, Call).Times(0); + + auto connection = + MakeWriterConnectionResumed(mock_factory.AsStdFunction(), std::move(mock), + initial_request, nullptr, Options{}); + EXPECT_THAT(connection->PersistedState(), VariantWith(0)); + + auto write = connection->Write(payload); + ASSERT_FALSE(write.is_ready()); + + auto flush = connection->Flush({}); + ASSERT_FALSE(flush.is_ready()); + + auto next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Flush"); + next.first.set_value(true); + + next = sequencer.PopFrontWithName(); + EXPECT_EQ(next.second, "Query"); + next.first.set_value(true); + + EXPECT_TRUE(flush.is_ready()); + EXPECT_THAT(flush.get(), StatusIs(StatusCode::kOk)); + + EXPECT_TRUE(write.is_ready()); + EXPECT_THAT(write.get(), StatusIs(StatusCode::kOk)); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/internal/grpc/configure_client_context.cc b/google/cloud/storage/internal/grpc/configure_client_context.cc index 9a0478f67f0d2..4507f6d2af721 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context.cc @@ -50,6 +50,13 @@ void ApplyRoutingHeaders(grpc::ClientContext& context, "bucket=" + google::cloud::internal::UrlEncode(spec.resource().bucket())); } +void ApplyRoutingHeaders(grpc::ClientContext& context, + google::storage::v2::AppendObjectSpec const& spec) { + context.AddMetadata( + "x-goog-request-params", + "bucket=" + google::cloud::internal::UrlEncode(spec.bucket())); +} + void ApplyRoutingHeaders(grpc::ClientContext& context, storage::internal::UploadChunkRequest const& request) { ApplyResumableUploadRoutingHeader(context, request.upload_session_url()); diff --git a/google/cloud/storage/internal/grpc/configure_client_context.h b/google/cloud/storage/internal/grpc/configure_client_context.h index 3839cdf8e8b2f..36b441560f941 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.h +++ b/google/cloud/storage/internal/grpc/configure_client_context.h @@ -79,6 +79,10 @@ void ApplyRoutingHeaders( void ApplyRoutingHeaders(grpc::ClientContext& context, google::storage::v2::WriteObjectSpec const& spec); +/// @copydoc ApplyRoutingHeaders(grpc::ClientContext&,) +void ApplyRoutingHeaders(grpc::ClientContext& context, + google::storage::v2::AppendObjectSpec const& spec); + /** * The generated `StorageMetadata` stub can not handle dynamic routing headers * for client side streaming. So we manually match and extract the headers in diff --git a/google/cloud/storage/internal/grpc/configure_client_context_test.cc b/google/cloud/storage/internal/grpc/configure_client_context_test.cc index 76ecd313a627e..550f956372e69 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context_test.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context_test.cc @@ -210,6 +210,18 @@ TEST_F(GrpcConfigureClientContext, ApplyRoutingHeadersUploadIdNoMatch) { EXPECT_THAT(metadata, Not(Contains(Pair("x-goog-request-params", _)))); } +TEST_F(GrpcConfigureClientContext, ApplyRoutingHeadersAppendObject) { + auto spec = google::storage::v2::AppendObjectSpec{}; + spec.set_bucket("projects/_/buckets/test-bucket"); + + grpc::ClientContext context; + ApplyRoutingHeaders(context, spec); + auto metadata = GetMetadata(context); + EXPECT_THAT(metadata, + Contains(Pair("x-goog-request-params", + "bucket=projects%2F_%2Fbuckets%2Ftest-bucket"))); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_internal diff --git a/google/cloud/storage/mocks/mock_async_connection.h b/google/cloud/storage/mocks/mock_async_connection.h index 33a87f4f35ed6..e72f9a0695698 100644 --- a/google/cloud/storage/mocks/mock_async_connection.h +++ b/google/cloud/storage/mocks/mock_async_connection.h @@ -34,12 +34,24 @@ class MockAsyncConnection : public storage_experimental::AsyncConnection { MOCK_METHOD(Options, options, (), (const, override)); MOCK_METHOD(future>, InsertObject, (InsertObjectParams), (override)); + MOCK_METHOD( + future>>, + Open, (OpenParams), (override)); MOCK_METHOD( future>>, ReadObject, (ReadObjectParams), (override)); MOCK_METHOD(future>, ReadObjectRange, (ReadObjectParams), (override)); + MOCK_METHOD( + future>>, + StartAppendableObjectUpload, (AppendableUploadParams), (override)); + MOCK_METHOD( + future>>, + ResumeAppendableObjectUpload, (AppendableUploadParams), (override)); MOCK_METHOD( future>>, diff --git a/google/cloud/storage/mocks/mock_async_object_descriptor_connection.h b/google/cloud/storage/mocks/mock_async_object_descriptor_connection.h new file mode 100644 index 0000000000000..49b645120634c --- /dev/null +++ b/google/cloud/storage/mocks/mock_async_object_descriptor_connection.h @@ -0,0 +1,42 @@ +// Copyright 2024 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_STORAGE_MOCKS_MOCK_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_MOCKS_MOCK_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H + +#include "google/cloud/storage/async/object_descriptor_connection.h" +#include "google/cloud/version.h" +#include + +namespace google { +namespace cloud { +namespace storage_mocks { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +class MockAsyncObjectDescriptorConnection + : public storage_experimental::ObjectDescriptorConnection { + public: + MOCK_METHOD(absl::optional, metadata, (), + (const, override)); + MOCK_METHOD(std::unique_ptr, + Read, (ReadParams), (override)); + MOCK_METHOD(Options, options, (), (const, override)); +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage_mocks +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_MOCKS_MOCK_ASYNC_OBJECT_DESCRIPTOR_CONNECTION_H diff --git a/google/cloud/storage/storage_client_grpc_unit_tests.bzl b/google/cloud/storage/storage_client_grpc_unit_tests.bzl index 5b0c9049d9e68..ba71aac7386bb 100644 --- a/google/cloud/storage/storage_client_grpc_unit_tests.bzl +++ b/google/cloud/storage/storage_client_grpc_unit_tests.bzl @@ -20,6 +20,7 @@ storage_client_grpc_unit_tests = [ "async/bucket_name_test.cc", "async/client_test.cc", "async/idempotency_policy_test.cc", + "async/object_descriptor_test.cc", "async/read_all_test.cc", "async/reader_test.cc", "async/resume_policy_test.cc", @@ -28,6 +29,7 @@ storage_client_grpc_unit_tests = [ "async/writer_test.cc", "grpc_plugin_test.cc", "internal/async/connection_impl_insert_test.cc", + "internal/async/connection_impl_open_test.cc", "internal/async/connection_impl_read_hash_test.cc", "internal/async/connection_impl_read_test.cc", "internal/async/connection_impl_test.cc", @@ -36,18 +38,27 @@ storage_client_grpc_unit_tests = [ "internal/async/connection_tracing_test.cc", "internal/async/default_options_test.cc", "internal/async/insert_object_test.cc", + "internal/async/object_descriptor_connection_tracing_test.cc", + "internal/async/object_descriptor_impl_test.cc", + "internal/async/object_descriptor_reader_test.cc", + "internal/async/object_descriptor_reader_tracing_test.cc", + "internal/async/open_object_test.cc", + "internal/async/open_stream_test.cc", "internal/async/partial_upload_test.cc", "internal/async/read_payload_impl_test.cc", + "internal/async/read_range_test.cc", "internal/async/reader_connection_factory_test.cc", "internal/async/reader_connection_impl_test.cc", "internal/async/reader_connection_resume_test.cc", "internal/async/reader_connection_tracing_test.cc", "internal/async/rewriter_connection_impl_test.cc", "internal/async/rewriter_connection_tracing_test.cc", + "internal/async/write_object_test.cc", "internal/async/write_payload_impl_test.cc", "internal/async/writer_connection_buffered_test.cc", "internal/async/writer_connection_finalized_test.cc", "internal/async/writer_connection_impl_test.cc", + "internal/async/writer_connection_resumed_test.cc", "internal/async/writer_connection_tracing_test.cc", "internal/grpc/bucket_access_control_parser_test.cc", "internal/grpc/bucket_metadata_parser_test.cc",