Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions google/cloud/internal/curl_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ CurlImpl::CurlImpl(CurlHandle handle,

http_version_ = options.get<HttpVersionOption>();

if (options.has<HttpConnectTimeoutOption>()) {
connect_timeout_ms_ = options.get<HttpConnectTimeoutOption>();
}

transfer_stall_timeout_ = options.get<TransferStallTimeoutOption>();
transfer_stall_minimum_rate_ = options.get<TransferStallMinimumRateOption>();
download_stall_timeout_ = options.get<DownloadStallTimeoutOption>();
Expand Down Expand Up @@ -449,6 +453,15 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
status = handle_.SetOption(CURLOPT_LOW_SPEED_TIME, timeout);
if (!status.ok()) return OnTransferError(context, std::move(status));
}
// Set after the stall timeouts: `CURLOPT_CONNECTTIMEOUT_MS` and
// `CURLOPT_CONNECTTIMEOUT` configure the same setting in libcurl, an
// explicitly configured connect timeout should win.
if (connect_timeout_ms_ != std::chrono::milliseconds::zero()) {
// NOLINTNEXTLINE(google-runtime-int) - libcurl *requires* long
auto const timeout_ms = static_cast<long>(connect_timeout_ms_.count());
status = handle_.SetOption(CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
if (!status.ok()) return OnTransferError(context, std::move(status));
}
return MakeRequestImpl(context);
}

Expand Down
1 change: 1 addition & 0 deletions google/cloud/internal/curl_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class CurlImpl {
CurlHandle::SocketOptions socket_options_;
std::string user_agent_;
std::string http_version_;
std::chrono::milliseconds connect_timeout_ms_{0};
std::chrono::seconds transfer_stall_timeout_;
std::uint32_t transfer_stall_minimum_rate_;
std::chrono::seconds download_stall_timeout_;
Expand Down
18 changes: 15 additions & 3 deletions google/cloud/internal/rest_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ struct TransferStallMinimumRateOption {
using Type = std::int32_t;
};

/**
* Sets the TCP/TLS connection timeout.
*
* If the connection cannot be established within this time, the request is
* aborted. This is useful as a fail-safe against OS-level TCP locks during
* severe network routing anomalies.
*/
struct HttpConnectTimeoutOption {
using Type = std::chrono::milliseconds;
};

/**
* Sets the download stall timeout.
*
Expand Down Expand Up @@ -101,9 +112,10 @@ struct TargetApiVersionOption {

/// The complete list of options accepted by `CurlRestClient`
using RestInternalOptionList = ::google::cloud::OptionList<
TransferStallTimeoutOption, TransferStallMinimumRateOption,
DownloadStallTimeoutOption, DownloadStallMinimumRateOption,
LongrunningEndpointOption, TargetApiVersionOption>;
HttpConnectTimeoutOption, TransferStallTimeoutOption,
TransferStallMinimumRateOption, DownloadStallTimeoutOption,
DownloadStallMinimumRateOption, LongrunningEndpointOption,
TargetApiVersionOption>;

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace rest_internal
Expand Down
23 changes: 23 additions & 0 deletions google/cloud/storage/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,23 @@ Options DefaultOptions(Options opts) {
"/iamapi");
}

if (!o.has<storage_experimental::EnableReadHedgingOption>()) {
o.set<storage_experimental::EnableReadHedgingOption>(false);
}
if (!o.has<storage_experimental::ReadHedgeRateLimitOption>()) {
o.set<storage_experimental::ReadHedgeRateLimitOption>(0.0);
}
if (!o.has<storage_experimental::MaxConcurrentHedgesOption>()) {
o.set<storage_experimental::MaxConcurrentHedgesOption>(0);
}
if (!o.has<storage_experimental::ReadHedgeDelayOption>()) {
o.set<storage_experimental::ReadHedgeDelayOption>(
std::chrono::milliseconds(500));
}
if (!o.has<storage_experimental::MaxReadHedgesOption>()) {
o.set<storage_experimental::MaxReadHedgesOption>(2);
}

auto logging = GetEnv("CLOUD_STORAGE_ENABLE_TRACING");
if (logging) {
for (auto c : absl::StrSplit(*logging, ',')) {
Expand Down Expand Up @@ -633,6 +650,12 @@ Options DefaultOptions(Options opts) {
rest_defaults.set<rest::CAPathOption>(o.get<internal::CAPathOption>());
}

// The (experimental) connect timeout is mapped the same way.
if (o.has<storage_experimental::HttpConnectTimeoutOption>()) {
rest_defaults.set<rest::HttpConnectTimeoutOption>(
o.get<storage_experimental::HttpConnectTimeoutOption>());
}

return google::cloud::internal::MergeOptions(std::move(o),
std::move(rest_defaults));
}
Expand Down
3 changes: 3 additions & 0 deletions google/cloud/storage/google_cloud_cpp_storage.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ google_cloud_cpp_storage_hdrs = [
"internal/hash_validator.h",
"internal/hash_validator_impl.h",
"internal/hash_values.h",
"internal/hedged_object_read_source.h",
"internal/hedging_thread_pool.h",
"internal/hmac_key_metadata_parser.h",
"internal/hmac_key_requests.h",
"internal/http_response.h",
Expand Down Expand Up @@ -184,6 +186,7 @@ google_cloud_cpp_storage_srcs = [
"internal/hash_validator.cc",
"internal/hash_validator_impl.cc",
"internal/hash_values.cc",
"internal/hedged_object_read_source.cc",
"internal/hmac_key_metadata_parser.cc",
"internal/hmac_key_requests.cc",
"internal/http_response.cc",
Expand Down
5 changes: 5 additions & 0 deletions google/cloud/storage/google_cloud_cpp_storage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ add_library(
internal/hash_validator_impl.h
internal/hash_values.cc
internal/hash_values.h
internal/hedged_object_read_source.cc
internal/hedged_object_read_source.h
internal/hedging_thread_pool.h
internal/hmac_key_metadata_parser.cc
internal/hmac_key_metadata_parser.h
internal/hmac_key_requests.cc
Expand Down Expand Up @@ -447,6 +450,8 @@ if (BUILD_TESTING)
internal/hash_function_impl_test.cc
internal/hash_validator_test.cc
internal/hash_values_test.cc
internal/hedged_object_read_source_test.cc
internal/hedging_thread_pool_test.cc
internal/hmac_key_requests_test.cc
internal/http_response_test.cc
internal/logging_stub_test.cc
Expand Down
55 changes: 47 additions & 8 deletions google/cloud/storage/internal/connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@

#include "google/cloud/internal/disable_deprecation_warnings.inc"
#include "google/cloud/storage/internal/connection_impl.h"
#include "google/cloud/storage/internal/hedged_object_read_source.h"
#include "google/cloud/storage/internal/retry_object_read_source.h"
#include "google/cloud/storage/parallel_upload.h"
#include "google/cloud/internal/filesystem.h"
#include "google/cloud/internal/opentelemetry.h"
#include "google/cloud/internal/rest_retry_loop.h"
#include "google/cloud/log.h"
#include "absl/strings/match.h"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <functional>
Expand Down Expand Up @@ -155,7 +157,27 @@ std::shared_ptr<StorageConnectionImpl> StorageConnectionImpl::Create(
StorageConnectionImpl::StorageConnectionImpl(
std::unique_ptr<storage_internal::GenericStub> stub, Options options)
: stub_(std::move(stub)),
options_(MergeOptions(std::move(options), stub_->options())) {}
options_(MergeOptions(std::move(options), stub_->options())) {
if (options_.get<storage_experimental::EnableReadHedgingOption>()) {
// The pool only runs stream-open attempts: one primary and (at most) a few
// hedges per stream being opened. Size it to the number of connections the
// REST layer can use, falling back to the hardware concurrency when the
// connection pool is unbounded (`ConnectionPoolSizeOption == 0`).
auto pool_size = options_.get<ConnectionPoolSizeOption>();
if (pool_size == 0) {
pool_size =
(std::max<std::size_t>)(4, std::thread::hardware_concurrency());
}
auto const max_threads = 2 * pool_size;
auto const rate_limit =
options_.get<storage_experimental::ReadHedgeRateLimitOption>();
auto const max_concurrent =
options_.get<storage_experimental::MaxConcurrentHedgesOption>();
// Allow bursts of up to one second worth of hedges.
hedge_pool_ = std::make_shared<HedgingThreadPool>(
max_threads, rate_limit, rate_limit, max_concurrent);
}
}

Options StorageConnectionImpl::options() const { return options_; }

Expand Down Expand Up @@ -392,15 +414,32 @@ StatusOr<std::unique_ptr<ObjectReadSource>> StorageConnectionImpl::ReadObject(
*current, request, where);
};

auto retry_policy = current->get<RetryPolicyOption>()->clone();
auto backoff_policy = current->get<BackoffPolicyOption>()->clone();
auto child = factory(request, *retry_policy, *backoff_policy);
if (!child) return child;
auto retry_source_factory =
[factory, current,
request]() -> StatusOr<std::unique_ptr<ObjectReadSource>> {
auto retry_policy = current->get<RetryPolicyOption>()->clone();
auto backoff_policy = current->get<BackoffPolicyOption>()->clone();
auto child = factory(request, *retry_policy, *backoff_policy);
if (!child) return child;
return std::unique_ptr<ObjectReadSource>(
std::make_unique<RetryObjectReadSource>(
factory, current, request, *std::move(child),
std::move(retry_policy), std::move(backoff_policy)));
};

auto const enable_hedging =
current->get<storage_experimental::EnableReadHedgingOption>();
auto const delay = current->get<storage_experimental::ReadHedgeDelayOption>();
auto const max_hedges =
current->get<storage_experimental::MaxReadHedgesOption>();

if (!enable_hedging || max_hedges <= 0 || !hedge_pool_) {
return retry_source_factory();
}

return std::unique_ptr<ObjectReadSource>(
std::make_unique<RetryObjectReadSource>(
std::move(factory), std::move(current), request, *std::move(child),
std::move(retry_policy), std::move(backoff_policy)));
std::make_unique<HedgedObjectReadSource>(
hedge_pool_, std::move(retry_source_factory), delay, max_hedges));
}

StatusOr<ListObjectsResponse> StorageConnectionImpl::ListObjects(
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/storage/internal/connection_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "google/cloud/storage/idempotency_policy.h"
#include "google/cloud/storage/internal/generic_stub.h"
#include "google/cloud/storage/internal/hedging_thread_pool.h"
#include "google/cloud/storage/internal/storage_connection.h"
#include "google/cloud/storage/object_read_stream.h"
#include "google/cloud/storage/retry_policy.h"
Expand Down Expand Up @@ -187,6 +188,7 @@ class StorageConnectionImpl

std::unique_ptr<storage_internal::GenericStub> stub_;
Options options_;
std::shared_ptr<HedgingThreadPool> hedge_pool_;
google::cloud::internal::InvocationIdGenerator invocation_id_generator_;
};

Expand Down
138 changes: 138 additions & 0 deletions google/cloud/storage/internal/hedged_object_read_source.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/storage/internal/hedged_object_read_source.h"
#include <atomic>
#include <cstring>
#include <future>
#include <utility>

namespace google {
namespace cloud {
namespace storage {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace internal {
namespace {

struct RaceResult {
StatusOr<ReadSourceResult> result;
std::unique_ptr<ObjectReadSource> source;
std::unique_ptr<char[]> buffer;
};
Comment thread
ajayky-os marked this conversation as resolved.

struct RaceState {
std::promise<RaceResult> promise;
std::atomic<bool> resolved{false};
};

// Opens a new child and performs its initial read, resolving the race if this
// attempt finishes first. Losing attempts close their child. Only the primary
// attempt resolves the race on an open error: a hedge that fails to open must
// not mask a slower, but successful, primary.
void RunAttempt(std::shared_ptr<RaceState> const& state,
HedgedObjectReadSource::ChildFactory const& factory,
std::size_t n, bool resolve_on_open_error,
std::shared_ptr<HedgingThreadPool> release_slot) {
struct SlotGuard {
std::shared_ptr<HedgingThreadPool> pool;
~SlotGuard() {
if (pool) pool->ReleaseHedgeSlot();
}
} guard{std::move(release_slot)};

auto source = factory();
if (!source) {
if (!resolve_on_open_error) return;
auto expected = false;
if (state->resolved.compare_exchange_strong(expected, true)) {
state->promise.set_value(
RaceResult{std::move(source).status(), nullptr, {}});
}
return;
}
std::unique_ptr<char[]> buffer(new char[n]);
auto result = (*source)->Read(buffer.get(), n);
auto expected = false;
if (state->resolved.compare_exchange_strong(expected, true)) {
state->promise.set_value(
RaceResult{std::move(result), *std::move(source), std::move(buffer)});
} else {
(*source)->Close();
}
}
Comment thread
ajayky-os marked this conversation as resolved.

} // namespace

HedgedObjectReadSource::HedgedObjectReadSource(
std::shared_ptr<HedgingThreadPool> hedge_pool, ChildFactory child_factory,
std::chrono::milliseconds delay, int max_hedges)
: hedge_pool_(std::move(hedge_pool)),
child_factory_(std::move(child_factory)),
delay_(delay),
max_hedges_(max_hedges) {}

bool HedgedObjectReadSource::IsOpen() const {
if (active_child_) return active_child_->IsOpen();
return true;
}

StatusOr<HttpResponse> HedgedObjectReadSource::Close() {
if (active_child_) return active_child_->Close();
// The source was never read from, there is no child (or HTTP response) to
// close.
return HttpResponse{HttpStatusCode::kOk, {}, {}};
}

StatusOr<ReadSourceResult> HedgedObjectReadSource::Read(char* buf,
std::size_t n) {
// Only the stream open is hedged. Once a child has won the race all
// subsequent reads continue on it, at its current offset, without any
// thread hops or extra copies.
if (active_child_) return active_child_->Read(buf, n);

auto state = std::make_shared<RaceState>();
auto future = state->promise.get_future();

auto primary = [state, factory = child_factory_, n] {
RunAttempt(state, factory, n, /*resolve_on_open_error=*/true, nullptr);
};
// If the pool is shutting down run the attempt inline, the read must
// complete either way.
if (!hedge_pool_->Enqueue(primary)) primary();

for (int i = 0; i != max_hedges_; ++i) {
if (future.wait_for(delay_) != std::future_status::timeout) break;
if (!hedge_pool_->TryAcquireHedgeToken()) continue;
auto hedge = [state, factory = child_factory_, n, pool = hedge_pool_] {
RunAttempt(state, factory, n, /*resolve_on_open_error=*/false, pool);
};
Comment thread
ajayky-os marked this conversation as resolved.
if (!hedge_pool_->Enqueue(hedge)) {
hedge_pool_->ReleaseHedgeSlot();
break;
}
}

auto race = future.get();
active_child_ = std::move(race.source);
if (race.result.ok() && race.result->bytes_received > 0) {
std::memcpy(buf, race.buffer.get(), race.result->bytes_received);
}
Comment thread
ajayky-os marked this conversation as resolved.
return race.result;
}

} // namespace internal
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace storage
} // namespace cloud
} // namespace google
Loading
Loading