From 22145969d6d9bd49ad250846d4c2b86fe522fba6 Mon Sep 17 00:00:00 2001 From: Ajay Yadav Date: Wed, 28 Oct 2026 06:49:24 +0000 Subject: [PATCH 1/3] fix(storage): apply TransferStallTimeoutOption deadline to unary gRPC calls Apply `TransferStallTimeoutOption` as a per-attempt deadline on `grpc::ClientContext` for unary gRPC RPCs in `GrpcStub`, `AsyncConnectionImpl`, and `RewriterConnectionImpl` when no explicit deadline has already been set, while allowing `GrpcSetupOption` to override the deadline and leaving streaming RPCs (`ReadObject`, `InsertObjectMedia`, `UploadChunk`) on their per-chunk stall timers. Also wire `GrpcStub::CreateResumableUpload`, `QueryResumableUpload`, `DeleteResumableUpload`, and `SignBlob` through `ApplyQueryParameters` so that `GrpcSetupOption` and query parameters are consistently applied. --- .../storage/internal/async/connection_impl.cc | 5 + .../internal/async/connection_impl_test.cc | 2 + .../async/rewriter_connection_impl.cc | 1 + .../async/rewriter_connection_impl_test.cc | 4 + .../internal/grpc/configure_client_context.h | 72 ++++++++++-- .../grpc/configure_client_context_test.cc | 111 ++++++++++++++++++ google/cloud/storage/internal/grpc/stub.cc | 17 +-- .../cloud/storage/internal/grpc/stub_test.cc | 68 +++++++++++ 8 files changed, 254 insertions(+), 26 deletions(-) diff --git a/google/cloud/storage/internal/async/connection_impl.cc b/google/cloud/storage/internal/async/connection_impl.cc index 6373365d95cc5..3c7b490c7f794 100644 --- a/google/cloud/storage/internal/async/connection_impl.cc +++ b/google/cloud/storage/internal/async/connection_impl.cc @@ -241,6 +241,7 @@ future> AsyncConnectionImpl::GetBucket( std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::GetBucketRequest const& request) { + ApplyUnaryRpcDeadline(*context, *options); return stub->AsyncGetBucket(cq, std::move(context), std::move(options), request); }; @@ -623,6 +624,7 @@ AsyncConnectionImpl::ComposeObject(ComposeObjectParams p) { std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::ComposeObjectRequest const& request) { + ApplyUnaryRpcDeadline(*context, *options); AddIdempotencyToken(*context, id); return stub->AsyncComposeObject(cq, std::move(context), std::move(options), request); @@ -646,6 +648,7 @@ future AsyncConnectionImpl::DeleteObject(DeleteObjectParams p) { CompletionQueue& cq, std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::DeleteObjectRequest const& proto) { + ApplyUnaryRpcDeadline(*context, *options); AddIdempotencyToken(*context, id); return stub->AsyncDeleteObject(cq, std::move(context), std::move(options), proto); @@ -756,6 +759,7 @@ AsyncConnectionImpl::StartResumableWrite( CompletionQueue& cq, std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::StartResumableWriteRequest const& proto) { + ApplyUnaryRpcDeadline(*context, *options); internal::ConfigureContext(*context, *options); return stub->AsyncStartResumableWrite(cq, std::move(context), std::move(options), proto); @@ -779,6 +783,7 @@ AsyncConnectionImpl::QueryWriteStatus( google::cloud::internal::ImmutableOptions options, google::storage::v2::QueryWriteStatusRequest const& proto) { google::cloud::internal::ConfigurePollContext(*context, *options); + ApplyUnaryRpcDeadline(*context, *options); return stub->AsyncQueryWriteStatus(cq, std::move(context), std::move(options), proto); }, diff --git a/google/cloud/storage/internal/async/connection_impl_test.cc b/google/cloud/storage/internal/async/connection_impl_test.cc index 6f47a8adcfd5a..c1f660178a475 100644 --- a/google/cloud/storage/internal/async/connection_impl_test.cc +++ b/google/cloud/storage/internal/async/connection_impl_test.cc @@ -107,6 +107,8 @@ TEST_F(AsyncConnectionImplTest, GetBucket) { EXPECT_THAT(GetMetadata(*context), testing::Not(testing::Contains( testing::Key("x-goog-gcs-idempotency-token")))); + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); EXPECT_EQ(options->get(), kAuthority); auto expected = google::storage::v2::GetBucketRequest{}; EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected)); diff --git a/google/cloud/storage/internal/async/rewriter_connection_impl.cc b/google/cloud/storage/internal/async/rewriter_connection_impl.cc index 81babb43d4e5b..a3687d82340a2 100644 --- a/google/cloud/storage/internal/async/rewriter_connection_impl.cc +++ b/google/cloud/storage/internal/async/rewriter_connection_impl.cc @@ -54,6 +54,7 @@ RewriterConnectionImpl::Iterate() { std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::RewriteObjectRequest const& proto) { + ApplyUnaryRpcDeadline(*context, *options); AddIdempotencyToken(*context, id); return stub->AsyncRewriteObject(cq, std::move(context), std::move(options), proto); diff --git a/google/cloud/storage/internal/async/rewriter_connection_impl_test.cc b/google/cloud/storage/internal/async/rewriter_connection_impl_test.cc index cdf74fd1ac261..d2dc159a12216 100644 --- a/google/cloud/storage/internal/async/rewriter_connection_impl_test.cc +++ b/google/cloud/storage/internal/async/rewriter_connection_impl_test.cc @@ -249,6 +249,8 @@ TEST(RewriterConnectionImplTest, IterateReusesIdempotencyTokenOnRetry) { EXPECT_CALL(*mock, AsyncRewriteObject) .WillOnce([&](auto&, auto const& context, auto const&, google::storage::v2::RewriteObjectRequest const&) { + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); auto metadata = validate_metadata_fixture.GetMetadata(*context); auto l = metadata.find("x-goog-gcs-idempotency-token"); EXPECT_NE(l, metadata.end()); @@ -263,6 +265,8 @@ TEST(RewriterConnectionImplTest, IterateReusesIdempotencyTokenOnRetry) { }) .WillOnce([&](auto&, auto const& context, auto const&, google::storage::v2::RewriteObjectRequest const&) { + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); auto metadata = validate_metadata_fixture.GetMetadata(*context); auto l = metadata.find("x-goog-gcs-idempotency-token"); EXPECT_NE(l, metadata.end()); diff --git a/google/cloud/storage/internal/grpc/configure_client_context.h b/google/cloud/storage/internal/grpc/configure_client_context.h index b948330b811da..323824ab3f15f 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.h +++ b/google/cloud/storage/internal/grpc/configure_client_context.h @@ -17,11 +17,14 @@ #include "google/cloud/storage/internal/generic_request.h" #include "google/cloud/storage/internal/object_requests.h" +#include "google/cloud/storage/options.h" #include "google/cloud/storage/version.h" #include "google/cloud/grpc_options.h" #include "google/cloud/internal/rest_context.h" #include "google/storage/v2/storage.pb.h" #include +#include +#include namespace google { namespace cloud { @@ -40,18 +43,35 @@ void AddIdempotencyToken(grpc::ClientContext& ctx, /// Configures @p ctx using @p token. void AddIdempotencyToken(grpc::ClientContext& ctx, std::string const& token); -/** - * Inject request query parameters into grpc::ClientContext. - * - * The REST API has a number of "standard" query parameters that are not part of - * the gRPC request body, instead they are sent via metadata headers in the gRPC - * request. - * - * @see https://cloud.google.com/apis/docs/system-parameters - */ template -void ApplyQueryParameters(grpc::ClientContext& ctx, Options const& options, - Request const& request) { +struct IsStreamingRequest : std::false_type {}; + +template <> +struct IsStreamingRequest + : std::true_type {}; + +template <> +struct IsStreamingRequest + : std::true_type {}; + +template <> +struct IsStreamingRequest + : std::true_type {}; + +/// Applies the unary RPC deadline from `TransferStallTimeoutOption` to @p ctx +/// if a deadline has not already been set on @p ctx. +inline void ApplyUnaryRpcDeadline(grpc::ClientContext& ctx, + Options const& options) { + auto const timeout = options.get(); + if (timeout > std::chrono::seconds::zero() && + ctx.deadline() == std::chrono::system_clock::time_point::max()) { + ctx.set_deadline(std::chrono::system_clock::now() + timeout); + } +} + +template +void ApplyQuotaUserAndFieldMask(grpc::ClientContext& ctx, + Request const& request, std::true_type) { // The gRPC API has a single field for the `QuotaUser` parameter, while the // JSON API has two: // https://cloud.google.com/storage/docs/json_api/v1/parameters#quotaUser @@ -71,6 +91,36 @@ void ApplyQueryParameters(grpc::ClientContext& ctx, Options const& options, ctx.AddMetadata("x-goog-fieldmask", request.template GetOption().value()); } +} + +template +void ApplyQuotaUserAndFieldMask(grpc::ClientContext&, Request const&, + std::false_type) {} + +/** + * Inject request query parameters into grpc::ClientContext. + * + * The REST API has a number of "standard" query parameters that are not part of + * the gRPC request body, instead they are sent via metadata headers in the gRPC + * request. + * + * @see https://cloud.google.com/apis/docs/system-parameters + */ +template +void ApplyQueryParameters(grpc::ClientContext& ctx, Options const& options, + Request const& request) { + using HasStandardParameters = std::integral_constant< + bool, + storage::internal::SupportsOption::type::value && + storage::internal::SupportsOption::type::value && + storage::internal::SupportsOption::type::value>; + ApplyQuotaUserAndFieldMask(ctx, request, HasStandardParameters{}); + if (!IsStreamingRequest::value) { + ApplyUnaryRpcDeadline(ctx, options); + } internal::ConfigureContext(ctx, options); } 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 e96a3d85fd24a..f1edad413da15 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context_test.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context_test.cc @@ -15,6 +15,7 @@ #include "google/cloud/storage/internal/grpc/configure_client_context.h" #include "google/cloud/storage/internal/hash_function.h" #include "google/cloud/storage/internal/object_requests.h" +#include "google/cloud/storage/internal/sign_blob_requests.h" #include "google/cloud/grpc_options.h" #include "google/cloud/testing_util/validate_metadata.h" #include @@ -140,6 +141,116 @@ TEST_F(GrpcConfigureClientContext, ApplyQueryParametersGrpcOptions) { storage::internal::ReadObjectRangeRequest("test-bucket", "test-object")); } +TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadline) { + auto const timeout = std::chrono::seconds(42); + auto const options = + Options{}.set(timeout); + + auto const before = std::chrono::system_clock::now(); + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::GetObjectMetadataRequest( + "test-bucket", "test-object")); + auto const after = std::chrono::system_clock::now(); + + EXPECT_GE(ctx.deadline(), before + timeout); + EXPECT_LE(ctx.deadline(), after + timeout); +} + +TEST_F(GrpcConfigureClientContext, ApplyQueryParametersSignBlobUnaryDeadline) { + auto const timeout = std::chrono::seconds(15); + auto const options = + Options{}.set(timeout); + + auto const before = std::chrono::system_clock::now(); + grpc::ClientContext ctx; + ApplyQueryParameters( + ctx, options, + storage::internal::SignBlobRequest("test-sa", "dGVzdA==", {})); + auto const after = std::chrono::system_clock::now(); + + EXPECT_GE(ctx.deadline(), before + timeout); + EXPECT_LE(ctx.deadline(), after + timeout); +} + +TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineZero) { + auto const options = Options{}.set( + std::chrono::seconds(0)); + + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::GetObjectMetadataRequest( + "test-bucket", "test-object")); + EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); +} + +TEST_F(GrpcConfigureClientContext, + ApplyQueryParametersUnaryDeadlineRespectsExistingDeadline) { + auto const existing_deadline = + std::chrono::system_clock::now() + std::chrono::seconds(5); + auto const options = Options{}.set( + std::chrono::seconds(120)); + + grpc::ClientContext ctx; + ctx.set_deadline(existing_deadline); + ApplyQueryParameters(ctx, options, + storage::internal::GetObjectMetadataRequest( + "test-bucket", "test-object")); + // gRPC converts system_clock::time_point to gpr_timespec (nanosecond + // resolution), allow 1ms tolerance for clock conversion. + EXPECT_LE(std::chrono::abs(ctx.deadline() - existing_deadline), + std::chrono::milliseconds(1)); +} + +TEST_F(GrpcConfigureClientContext, + ApplyQueryParametersUnaryDeadlineOverriddenByGrpcSetupOption) { + auto const custom_deadline = + std::chrono::system_clock::now() + std::chrono::seconds(7); + auto const options = + Options{} + .set(std::chrono::seconds(120)) + .set( + [custom_deadline](grpc::ClientContext& c) { + c.set_deadline(custom_deadline); + }); + + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::GetObjectMetadataRequest( + "test-bucket", "test-object")); + EXPECT_LE(std::chrono::abs(ctx.deadline() - custom_deadline), + std::chrono::milliseconds(1)); +} + +TEST_F(GrpcConfigureClientContext, + ApplyQueryParametersStreamingRequestsExcludeWholeRpcDeadline) { + auto const options = Options{}.set( + std::chrono::seconds(42)); + + { + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::ReadObjectRangeRequest( + "test-bucket", "test-object")); + EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); + } + { + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::InsertObjectMediaRequest( + "test-bucket", "test-object", "content")); + EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); + } + { + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::UploadChunkRequest( + "projects/_/buckets/test-bucket/upload-id", 0, {}, + CreateNullHashFunction())); + EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); + } +} + TEST_F(GrpcConfigureClientContext, ApplyRoutingHeadersInsertObjectMedia) { storage::internal::InsertObjectMediaRequest req("test-bucket", "test-object", "content"); diff --git a/google/cloud/storage/internal/grpc/stub.cc b/google/cloud/storage/internal/grpc/stub.cc index 086da25985fed..4b8a81db4beb0 100644 --- a/google/cloud/storage/internal/grpc/stub.cc +++ b/google/cloud/storage/internal/grpc/stub.cc @@ -575,10 +575,6 @@ GrpcStub::CreateResumableUpload( grpc::ClientContext ctx; ApplyQueryParameters(ctx, options, request); AddIdempotencyToken(ctx, context); - auto const timeout = options.get(); - if (timeout.count() != 0) { - ctx.set_deadline(std::chrono::system_clock::now() + timeout); - } auto response = stub_->StartResumableWrite(ctx, options, *proto_request); if (!response.ok()) return std::move(response).status(); @@ -593,10 +589,6 @@ GrpcStub::QueryResumableUpload( grpc::ClientContext ctx; ApplyQueryParameters(ctx, options, request); AddIdempotencyToken(ctx, context); - auto const timeout = options.get(); - if (timeout.count() != 0) { - ctx.set_deadline(std::chrono::system_clock::now() + timeout); - } auto response = stub_->QueryWriteStatus(ctx, options, ToProto(request)); if (!response) return std::move(response).status(); return FromProto(*response, options); @@ -608,10 +600,6 @@ StatusOr GrpcStub::DeleteResumableUpload( grpc::ClientContext ctx; ApplyQueryParameters(ctx, options, request); AddIdempotencyToken(ctx, context); - auto const timeout = options.get(); - if (timeout.count() != 0) { - ctx.set_deadline(std::chrono::system_clock::now() + timeout); - } auto response = stub_->CancelResumableWrite(ctx, options, ToProto(request)); if (!response) return std::move(response).status(); return storage::internal::EmptyResponse{}; @@ -1040,12 +1028,11 @@ StatusOr GrpcStub::UpdateHmacKey( } StatusOr GrpcStub::SignBlob( - rest_internal::RestContext& context, Options const&, + rest_internal::RestContext& context, Options const& options, storage::internal::SignBlobRequest const& request) { auto proto = ToProto(request); grpc::ClientContext ctx; - // This request does not have any options that require using - // ApplyQueryParameters(ctx, options, request); + ApplyQueryParameters(ctx, options, request); AddIdempotencyToken(ctx, context); auto response = iam_stub_->SignBlob(ctx, proto); if (!response) return std::move(response).status(); diff --git a/google/cloud/storage/internal/grpc/stub_test.cc b/google/cloud/storage/internal/grpc/stub_test.cc index dd5a86425a05e..42170020f844e 100644 --- a/google/cloud/storage/internal/grpc/stub_test.cc +++ b/google/cloud/storage/internal/grpc/stub_test.cc @@ -16,8 +16,10 @@ #include "google/cloud/storage/grpc_plugin.h" #include "google/cloud/storage/testing/mock_storage_stub.h" #include "google/cloud/credentials.h" +#include "google/cloud/grpc_options.h" #include "google/cloud/options.h" #include "google/cloud/testing_util/is_proto_equal.h" +#include "google/cloud/testing_util/mock_minimal_iam_credentials_stub.h" #include "google/cloud/testing_util/status_matchers.h" #include "google/cloud/testing_util/validate_metadata.h" #include @@ -850,6 +852,72 @@ TEST_F(GrpcClientTest, CreateResumableUpload) { EXPECT_EQ(response.status(), PermanentError()); } +TEST_F(GrpcClientTest, UnaryRpcAppliesTransferStallTimeoutAndGrpcSetupOption) { + auto const stall_timeout = std::chrono::seconds(30); + auto const custom_deadline = + std::chrono::system_clock::now() + std::chrono::seconds(5); + + auto mock = std::make_shared(); + EXPECT_CALL(*mock, GetBucket) + .WillOnce([stall_timeout](grpc::ClientContext& context, Options const&, + google::storage::v2::GetBucketRequest const&) { + auto const now = std::chrono::system_clock::now(); + EXPECT_GT(context.deadline(), now); + EXPECT_LE(context.deadline(), + now + stall_timeout + std::chrono::seconds(2)); + return PermanentError(); + }) + .WillOnce( + [custom_deadline](grpc::ClientContext& context, Options const&, + google::storage::v2::GetBucketRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); + + EXPECT_CALL(*mock, StartResumableWrite) + .WillOnce([custom_deadline](grpc::ClientContext& context, Options const&, + v2::StartResumableWriteRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); + + auto mock_iam = + std::make_shared(); + EXPECT_CALL(*mock_iam, SignBlob) + .WillOnce( + [custom_deadline](grpc::ClientContext& context, + google::iam::credentials::v1::SignBlobRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); + + auto client = std::make_unique(mock, mock_iam, TestOptions()); + auto context = TestContext(); + auto options_with_stall = + TestOptions().set(stall_timeout); + (void)client->GetBucketMetadata( + context, options_with_stall, + storage::internal::GetBucketMetadataRequest("test-bucket")); + + auto options_with_setup = + options_with_stall.set( + [custom_deadline](grpc::ClientContext& c) { + c.set_deadline(custom_deadline); + }); + (void)client->GetBucketMetadata( + context, options_with_setup, + storage::internal::GetBucketMetadataRequest("test-bucket")); + (void)client->CreateResumableUpload( + context, options_with_setup, + storage::internal::ResumableUploadRequest("test-bucket", "test-object")); + (void)client->SignBlob( + context, options_with_setup, + storage::internal::SignBlobRequest("sa@example.com", "blob", {})); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_internal From 87d664e8255eff35c7386ecbf5c6439abf30c1f2 Mon Sep 17 00:00:00 2001 From: Ajay Yadav Date: Wed, 28 Oct 2026 20:31:18 +0000 Subject: [PATCH 2/3] style(storage): format unary deadline files and use explicit types --- .../storage/internal/async/connection_impl.cc | 2 +- .../internal/grpc/configure_client_context.h | 19 +++++------ .../grpc/configure_client_context_test.cc | 32 +++++++++++-------- .../cloud/storage/internal/grpc/stub_test.cc | 31 +++++++++--------- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/google/cloud/storage/internal/async/connection_impl.cc b/google/cloud/storage/internal/async/connection_impl.cc index 3c7b490c7f794..6cf944aecefe0 100644 --- a/google/cloud/storage/internal/async/connection_impl.cc +++ b/google/cloud/storage/internal/async/connection_impl.cc @@ -782,8 +782,8 @@ AsyncConnectionImpl::QueryWriteStatus( CompletionQueue& cq, std::shared_ptr context, google::cloud::internal::ImmutableOptions options, google::storage::v2::QueryWriteStatusRequest const& proto) { - google::cloud::internal::ConfigurePollContext(*context, *options); ApplyUnaryRpcDeadline(*context, *options); + google::cloud::internal::ConfigurePollContext(*context, *options); return stub->AsyncQueryWriteStatus(cq, std::move(context), std::move(options), proto); }, diff --git a/google/cloud/storage/internal/grpc/configure_client_context.h b/google/cloud/storage/internal/grpc/configure_client_context.h index 323824ab3f15f..7c84eb2b288dc 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.h +++ b/google/cloud/storage/internal/grpc/configure_client_context.h @@ -62,7 +62,8 @@ struct IsStreamingRequest /// if a deadline has not already been set on @p ctx. inline void ApplyUnaryRpcDeadline(grpc::ClientContext& ctx, Options const& options) { - auto const timeout = options.get(); + std::chrono::seconds const timeout = + options.get(); if (timeout > std::chrono::seconds::zero() && ctx.deadline() == std::chrono::system_clock::time_point::max()) { ctx.set_deadline(std::chrono::system_clock::now() + timeout); @@ -109,14 +110,14 @@ void ApplyQuotaUserAndFieldMask(grpc::ClientContext&, Request const&, template void ApplyQueryParameters(grpc::ClientContext& ctx, Options const& options, Request const& request) { - using HasStandardParameters = std::integral_constant< - bool, - storage::internal::SupportsOption::type::value && - storage::internal::SupportsOption::type::value && - storage::internal::SupportsOption::type::value>; + using HasStandardParameters = + std::integral_constant::type::value && + storage::internal::SupportsOption< + Request, storage::UserIp>::type::value && + storage::internal::SupportsOption< + Request, storage::Fields>::type::value>; ApplyQuotaUserAndFieldMask(ctx, request, HasStandardParameters{}); if (!IsStreamingRequest::value) { ApplyUnaryRpcDeadline(ctx, options); 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 f1edad413da15..23d95170a673c 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context_test.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context_test.cc @@ -142,39 +142,43 @@ TEST_F(GrpcConfigureClientContext, ApplyQueryParametersGrpcOptions) { } TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadline) { - auto const timeout = std::chrono::seconds(42); - auto const options = + std::chrono::seconds const timeout(42); + Options const options = Options{}.set(timeout); - auto const before = std::chrono::system_clock::now(); + std::chrono::system_clock::time_point const before = + std::chrono::system_clock::now(); grpc::ClientContext ctx; ApplyQueryParameters(ctx, options, storage::internal::GetObjectMetadataRequest( "test-bucket", "test-object")); - auto const after = std::chrono::system_clock::now(); + std::chrono::system_clock::time_point const after = + std::chrono::system_clock::now(); EXPECT_GE(ctx.deadline(), before + timeout); EXPECT_LE(ctx.deadline(), after + timeout); } TEST_F(GrpcConfigureClientContext, ApplyQueryParametersSignBlobUnaryDeadline) { - auto const timeout = std::chrono::seconds(15); - auto const options = + std::chrono::seconds const timeout(15); + Options const options = Options{}.set(timeout); - auto const before = std::chrono::system_clock::now(); + std::chrono::system_clock::time_point const before = + std::chrono::system_clock::now(); grpc::ClientContext ctx; ApplyQueryParameters( ctx, options, storage::internal::SignBlobRequest("test-sa", "dGVzdA==", {})); - auto const after = std::chrono::system_clock::now(); + std::chrono::system_clock::time_point const after = + std::chrono::system_clock::now(); EXPECT_GE(ctx.deadline(), before + timeout); EXPECT_LE(ctx.deadline(), after + timeout); } TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineZero) { - auto const options = Options{}.set( + Options const options = Options{}.set( std::chrono::seconds(0)); grpc::ClientContext ctx; @@ -186,9 +190,9 @@ TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineZero) { TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineRespectsExistingDeadline) { - auto const existing_deadline = + std::chrono::system_clock::time_point const existing_deadline = std::chrono::system_clock::now() + std::chrono::seconds(5); - auto const options = Options{}.set( + Options const options = Options{}.set( std::chrono::seconds(120)); grpc::ClientContext ctx; @@ -204,9 +208,9 @@ TEST_F(GrpcConfigureClientContext, TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineOverriddenByGrpcSetupOption) { - auto const custom_deadline = + std::chrono::system_clock::time_point const custom_deadline = std::chrono::system_clock::now() + std::chrono::seconds(7); - auto const options = + Options const options = Options{} .set(std::chrono::seconds(120)) .set( @@ -224,7 +228,7 @@ TEST_F(GrpcConfigureClientContext, TEST_F(GrpcConfigureClientContext, ApplyQueryParametersStreamingRequestsExcludeWholeRpcDeadline) { - auto const options = Options{}.set( + Options const options = Options{}.set( std::chrono::seconds(42)); { diff --git a/google/cloud/storage/internal/grpc/stub_test.cc b/google/cloud/storage/internal/grpc/stub_test.cc index 42170020f844e..92005a13944fa 100644 --- a/google/cloud/storage/internal/grpc/stub_test.cc +++ b/google/cloud/storage/internal/grpc/stub_test.cc @@ -853,15 +853,16 @@ TEST_F(GrpcClientTest, CreateResumableUpload) { } TEST_F(GrpcClientTest, UnaryRpcAppliesTransferStallTimeoutAndGrpcSetupOption) { - auto const stall_timeout = std::chrono::seconds(30); - auto const custom_deadline = + std::chrono::seconds const stall_timeout(30); + std::chrono::system_clock::time_point const custom_deadline = std::chrono::system_clock::now() + std::chrono::seconds(5); auto mock = std::make_shared(); EXPECT_CALL(*mock, GetBucket) .WillOnce([stall_timeout](grpc::ClientContext& context, Options const&, google::storage::v2::GetBucketRequest const&) { - auto const now = std::chrono::system_clock::now(); + std::chrono::system_clock::time_point const now = + std::chrono::system_clock::now(); EXPECT_GT(context.deadline(), now); EXPECT_LE(context.deadline(), now + stall_timeout + std::chrono::seconds(2)); @@ -883,26 +884,26 @@ TEST_F(GrpcClientTest, UnaryRpcAppliesTransferStallTimeoutAndGrpcSetupOption) { return PermanentError(); }); - auto mock_iam = - std::make_shared(); + auto mock_iam = std::make_shared< + google::cloud::testing_util::MockMinimalIamCredentialsStub>(); EXPECT_CALL(*mock_iam, SignBlob) - .WillOnce( - [custom_deadline](grpc::ClientContext& context, - google::iam::credentials::v1::SignBlobRequest const&) { - EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), - std::chrono::milliseconds(5)); - return PermanentError(); - }); + .WillOnce([custom_deadline]( + grpc::ClientContext& context, + google::iam::credentials::v1::SignBlobRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); auto client = std::make_unique(mock, mock_iam, TestOptions()); - auto context = TestContext(); - auto options_with_stall = + rest_internal::RestContext context = TestContext(); + Options options_with_stall = TestOptions().set(stall_timeout); (void)client->GetBucketMetadata( context, options_with_stall, storage::internal::GetBucketMetadataRequest("test-bucket")); - auto options_with_setup = + Options const options_with_setup = options_with_stall.set( [custom_deadline](grpc::ClientContext& c) { c.set_deadline(custom_deadline); From 1ab65b6e63965d41cd2f91e8d38e7c9e7478391a Mon Sep 17 00:00:00 2001 From: Ajay Yadav Date: Thu, 29 Oct 2026 09:31:20 +0000 Subject: [PATCH 3/3] test(storage): expand unary deadline tests and update docs per review --- .../internal/async/connection_impl_test.cc | 4 +++ .../async/connection_impl_upload_test.cc | 29 +++++++++++++------ .../internal/grpc/configure_client_context.h | 13 ++++++--- .../grpc/configure_client_context_test.cc | 11 +++++++ .../cloud/storage/internal/grpc/stub_test.cc | 22 ++++++++++++++ google/cloud/storage/options.h | 5 ++++ 6 files changed, 71 insertions(+), 13 deletions(-) diff --git a/google/cloud/storage/internal/async/connection_impl_test.cc b/google/cloud/storage/internal/async/connection_impl_test.cc index c1f660178a475..914e588260b3d 100644 --- a/google/cloud/storage/internal/async/connection_impl_test.cc +++ b/google/cloud/storage/internal/async/connection_impl_test.cc @@ -172,6 +172,8 @@ TEST_F(AsyncConnectionImplTest, ComposeObject) { GetMetadata(*context), testing::Contains(testing::Pair("x-goog-gcs-idempotency-token", testing::Not(testing::IsEmpty())))); + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); // Verify at least one option is initialized with the correct value. EXPECT_EQ(options->get(), kAuthority); auto expected = google::storage::v2::ComposeObjectRequest{}; @@ -277,6 +279,8 @@ TEST_F(AsyncConnectionImplTest, DeleteObject) { GetMetadata(*context), testing::Contains(testing::Pair("x-goog-gcs-idempotency-token", testing::Not(testing::IsEmpty())))); + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); // Verify at least one option is initialized with the correct values. EXPECT_EQ(options->get(), kAuthority); google::storage::v2::DeleteObjectRequest expected; diff --git a/google/cloud/storage/internal/async/connection_impl_upload_test.cc b/google/cloud/storage/internal/async/connection_impl_upload_test.cc index 6d3941de03266..ace4177c4f4b9 100644 --- a/google/cloud/storage/internal/async/connection_impl_upload_test.cc +++ b/google/cloud/storage/internal/async/connection_impl_upload_test.cc @@ -130,8 +130,10 @@ TEST_F(AsyncConnectionImplTest, StartUnbufferedUpload) { }); }) .WillOnce( - [&](auto&, auto, auto, + [&](auto&, auto const& context, auto, google::storage::v2::StartResumableWriteRequest const& request) { + EXPECT_EQ(context->deadline(), + std::chrono::system_clock::time_point::max()); auto expected = google::storage::v2::StartResumableWriteRequest{}; EXPECT_TRUE( TextFormat::ParseFromString(kExpectedRequest, &expected)); @@ -284,13 +286,20 @@ TEST_F(AsyncConnectionImplTest, StartUnbufferedUpload) { TEST_F(AsyncConnectionImplTest, UnbufferedUploadNewUploadWithTimeout) { AsyncSequencer sequencer; auto mock = std::make_shared(); - EXPECT_CALL(*mock, AsyncStartResumableWrite).WillOnce([&sequencer] { - return sequencer.PushBack("StartResumableWrite").then([](auto) { - auto response = google::storage::v2::StartResumableWriteResponse{}; - response.set_upload_id("test-upload-id"); - return make_status_or(response); - }); - }); + EXPECT_CALL(*mock, AsyncStartResumableWrite) + .WillOnce([&sequencer]( + CompletionQueue&, + std::shared_ptr const& context, + google::cloud::internal::ImmutableOptions const&, + google::storage::v2::StartResumableWriteRequest const&) { + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); + return sequencer.PushBack("StartResumableWrite").then([](auto) { + auto response = google::storage::v2::StartResumableWriteResponse{}; + response.set_upload_id("test-upload-id"); + return make_status_or(response); + }); + }); EXPECT_CALL(*mock, AsyncBidiWriteObject).WillOnce([&]() { auto stream = std::make_unique(); EXPECT_CALL(*stream, Start).WillOnce([&sequencer] { @@ -498,8 +507,10 @@ TEST_F(AsyncConnectionImplTest, ResumeUnbufferedUpload) { }); }) .WillOnce( - [&](auto&, auto, auto, + [&](auto&, auto const& context, auto, google::storage::v2::QueryWriteStatusRequest const& request) { + EXPECT_LT(context->deadline(), + std::chrono::system_clock::time_point::max()); auto expected = google::storage::v2::QueryWriteStatusRequest{}; EXPECT_TRUE(TextFormat::ParseFromString(kRequestText, &expected)); EXPECT_THAT(request, IsProtoEqual(expected)); diff --git a/google/cloud/storage/internal/grpc/configure_client_context.h b/google/cloud/storage/internal/grpc/configure_client_context.h index 7c84eb2b288dc..f919a0cd79544 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context.h +++ b/google/cloud/storage/internal/grpc/configure_client_context.h @@ -99,11 +99,16 @@ void ApplyQuotaUserAndFieldMask(grpc::ClientContext&, Request const&, std::false_type) {} /** - * Inject request query parameters into grpc::ClientContext. + * Configure a grpc::ClientContext for a storage request. * - * The REST API has a number of "standard" query parameters that are not part of - * the gRPC request body, instead they are sent via metadata headers in the gRPC - * request. + * Inject the request query parameters into the context. The REST API has a + * number of "standard" query parameters that are not part of the gRPC request + * body, instead they are sent via metadata headers in the gRPC request. + * + * For non-streaming requests this also sets a per-attempt deadline from + * `storage::TransferStallTimeoutOption` (see `ApplyUnaryRpcDeadline()`). + * Finally, for all requests, it applies any `GrpcSetupOption`; this runs last + * so applications can override the deadline. * * @see https://cloud.google.com/apis/docs/system-parameters */ 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 23d95170a673c..d3d8a5391b670 100644 --- a/google/cloud/storage/internal/grpc/configure_client_context_test.cc +++ b/google/cloud/storage/internal/grpc/configure_client_context_test.cc @@ -188,6 +188,17 @@ TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineZero) { EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); } +TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineNegative) { + Options const options = Options{}.set( + std::chrono::seconds(-1)); + + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, + storage::internal::GetObjectMetadataRequest( + "test-bucket", "test-object")); + EXPECT_EQ(ctx.deadline(), std::chrono::system_clock::time_point::max()); +} + TEST_F(GrpcConfigureClientContext, ApplyQueryParametersUnaryDeadlineRespectsExistingDeadline) { std::chrono::system_clock::time_point const existing_deadline = diff --git a/google/cloud/storage/internal/grpc/stub_test.cc b/google/cloud/storage/internal/grpc/stub_test.cc index 92005a13944fa..039902bc85749 100644 --- a/google/cloud/storage/internal/grpc/stub_test.cc +++ b/google/cloud/storage/internal/grpc/stub_test.cc @@ -884,6 +884,22 @@ TEST_F(GrpcClientTest, UnaryRpcAppliesTransferStallTimeoutAndGrpcSetupOption) { return PermanentError(); }); + EXPECT_CALL(*mock, QueryWriteStatus) + .WillOnce([custom_deadline](grpc::ClientContext& context, Options const&, + v2::QueryWriteStatusRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); + + EXPECT_CALL(*mock, CancelResumableWrite) + .WillOnce([custom_deadline](grpc::ClientContext& context, Options const&, + v2::CancelResumableWriteRequest const&) { + EXPECT_LE(std::chrono::abs(context.deadline() - custom_deadline), + std::chrono::milliseconds(5)); + return PermanentError(); + }); + auto mock_iam = std::make_shared< google::cloud::testing_util::MockMinimalIamCredentialsStub>(); EXPECT_CALL(*mock_iam, SignBlob) @@ -914,6 +930,12 @@ TEST_F(GrpcClientTest, UnaryRpcAppliesTransferStallTimeoutAndGrpcSetupOption) { (void)client->CreateResumableUpload( context, options_with_setup, storage::internal::ResumableUploadRequest("test-bucket", "test-object")); + (void)client->QueryResumableUpload( + context, options_with_setup, + storage::internal::QueryResumableUploadRequest("test-upload-id")); + (void)client->DeleteResumableUpload( + context, options_with_setup, + storage::internal::DeleteResumableUploadRequest("test-upload-id")); (void)client->SignBlob( context, options_with_setup, storage::internal::SignBlobRequest("sa@example.com", "blob", {})); diff --git a/google/cloud/storage/options.h b/google/cloud/storage/options.h index da2d1915485a3..b4f545a5cee46 100644 --- a/google/cloud/storage/options.h +++ b/google/cloud/storage/options.h @@ -465,6 +465,11 @@ struct MaximumCurlSocketSendSizeOption { * this is as effective as a timeout parameter, but maybe unfamiliar and thus * harder to reason about. * + * For gRPC unary RPCs, where no incremental progress can be observed, this + * option is applied as a per-attempt deadline instead. It is enabled by + * default (120 seconds); setting it to `std::chrono::seconds(0)` (or a + * negative value) disables the deadline for these RPCs. + * * @ingroup storage-options */ struct TransferStallTimeoutOption {