Skip to content

Commit 4b81317

Browse files
authored
feat(storage): implement TTFB speculative hedging with configurable connect timeouts (#16344)
1 parent b9dc56c commit 4b81317

16 files changed

Lines changed: 1097 additions & 15 deletions

google/cloud/internal/curl_impl.cc

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ CurlImpl::CurlImpl(CurlHandle handle,
197197

198198
http_version_ = options.get<HttpVersionOption>();
199199

200+
if (options.has<HttpConnectTimeoutOption>()) {
201+
connect_timeout_ms_ = options.get<HttpConnectTimeoutOption>();
202+
}
203+
200204
transfer_stall_timeout_ = options.get<TransferStallTimeoutOption>();
201205
transfer_stall_minimum_rate_ = options.get<TransferStallMinimumRateOption>();
202206
download_stall_timeout_ = options.get<DownloadStallTimeoutOption>();
@@ -262,6 +266,22 @@ void CurlImpl::WriteHeader(std::string const& header) {
262266
request_headers_.reset(headers);
263267
}
264268

269+
Status CurlImpl::SetConnectTimeout(std::chrono::seconds fallback) {
270+
// libcurl stores `CURLOPT_CONNECTTIMEOUT` and `CURLOPT_CONNECTTIMEOUT_MS` in
271+
// a single setting, so there must be exactly one place that decides the
272+
// value. An explicitly configured connect timeout wins; otherwise fall back
273+
// to the timeout implied by the stall options, preserving the historical
274+
// behavior for applications that do not set one.
275+
auto const connect_timeout =
276+
connect_timeout_ms_ != std::chrono::milliseconds::zero()
277+
? connect_timeout_ms_
278+
: std::chrono::duration_cast<std::chrono::milliseconds>(fallback);
279+
if (connect_timeout == std::chrono::milliseconds::zero()) return {};
280+
// NOLINTNEXTLINE(google-runtime-int) - libcurl *requires* long
281+
auto const timeout_ms = static_cast<long>(connect_timeout.count());
282+
return handle_.SetOption(CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
283+
}
284+
265285
void CurlImpl::MergeAndWriteHeaders(
266286
std::function<void(HttpHeader const&)> const& write_fn) {
267287
// There are some headers that we do not want to merge. These headers
@@ -432,6 +452,13 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
432452
#endif
433453
}
434454

455+
// Nothing else sets a connection timeout, so this can be decided once, before
456+
// the per-method options.
457+
status =
458+
SetConnectTimeout(method == HttpMethod::kGet ? download_stall_timeout_
459+
: transfer_stall_timeout_);
460+
if (!status.ok()) return OnTransferError(context, std::move(status));
461+
435462
if (method == HttpMethod::kGet) {
436463
status = handle_.SetOption(CURLOPT_NOPROGRESS, 1L);
437464
if (!status.ok()) return OnTransferError(context, std::move(status));
@@ -440,8 +467,6 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
440467
auto const timeout = static_cast<long>(download_stall_timeout_.count());
441468
// NOLINTNEXTLINE(google-runtime-int) - libcurl *requires* long
442469
auto const limit = static_cast<long>(download_stall_minimum_rate_);
443-
status = handle_.SetOption(CURLOPT_CONNECTTIMEOUT, timeout);
444-
if (!status.ok()) return OnTransferError(context, std::move(status));
445470
// Timeout if the request sends or receives less than 1 byte/second
446471
// (i.e. effectively no bytes) for download_stall_timeout_.
447472
status = handle_.SetOption(CURLOPT_LOW_SPEED_LIMIT, limit);
@@ -457,8 +482,6 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
457482
auto const timeout = static_cast<long>(transfer_stall_timeout_.count());
458483
// NOLINTNEXTLINE(google-runtime-int) - libcurl *requires* long
459484
auto const limit = static_cast<long>(transfer_stall_minimum_rate_);
460-
status = handle_.SetOption(CURLOPT_CONNECTTIMEOUT, timeout);
461-
if (!status.ok()) return OnTransferError(context, std::move(status));
462485
// Timeout if the request sends or receives less than 1 byte/second
463486
// (i.e. effectively no bytes) for transfer_stall_timeout_.
464487
status = handle_.SetOption(CURLOPT_LOW_SPEED_LIMIT, limit);

google/cloud/internal/curl_impl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ class CurlImpl {
123123

124124
void WriteHeader(std::string const& header);
125125

126+
// Sets the connection timeout, using `HttpConnectTimeoutOption` when the
127+
// application configured one and @p fallback (the relevant stall timeout)
128+
// otherwise. This is the only place that sets a connection timeout: libcurl
129+
// keeps a single value for `CURLOPT_CONNECTTIMEOUT` and
130+
// `CURLOPT_CONNECTTIMEOUT_MS`.
131+
Status SetConnectTimeout(std::chrono::seconds fallback);
132+
126133
// Cleanup the CURL handles, leaving them ready for reuse.
127134
void CleanupHandles();
128135
// Perform at least part of the request.
@@ -146,6 +153,7 @@ class CurlImpl {
146153
CurlHandle::SocketOptions socket_options_;
147154
std::string user_agent_;
148155
std::string http_version_;
156+
std::chrono::milliseconds connect_timeout_ms_{0};
149157
std::chrono::seconds transfer_stall_timeout_;
150158
std::uint32_t transfer_stall_minimum_rate_;
151159
std::chrono::seconds download_stall_timeout_;

google/cloud/internal/rest_options.h

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ struct TransferStallMinimumRateOption {
5656
using Type = std::int32_t;
5757
};
5858

59+
/**
60+
* Sets the TCP/TLS connection timeout.
61+
*
62+
* If the connection cannot be established within this time, the request is
63+
* aborted. This is useful as a fail-safe against OS-level TCP locks during
64+
* severe network routing anomalies.
65+
*
66+
* This applies to all HTTP methods, and it only bounds establishing the
67+
* connection: it has no effect once bytes start flowing. Note that this takes
68+
* precedence over the connection timeout implied by
69+
* `TransferStallTimeoutOption` and `DownloadStallTimeoutOption`, as libcurl
70+
* uses a single setting for all of them.
71+
*/
72+
struct HttpConnectTimeoutOption {
73+
using Type = std::chrono::milliseconds;
74+
};
75+
5976
/**
6077
* Sets the download stall timeout.
6178
*
@@ -101,9 +118,10 @@ struct TargetApiVersionOption {
101118

102119
/// The complete list of options accepted by `CurlRestClient`
103120
using RestInternalOptionList = ::google::cloud::OptionList<
104-
TransferStallTimeoutOption, TransferStallMinimumRateOption,
105-
DownloadStallTimeoutOption, DownloadStallMinimumRateOption,
106-
LongrunningEndpointOption, TargetApiVersionOption>;
121+
HttpConnectTimeoutOption, TransferStallTimeoutOption,
122+
TransferStallMinimumRateOption, DownloadStallTimeoutOption,
123+
DownloadStallMinimumRateOption, LongrunningEndpointOption,
124+
TargetApiVersionOption>;
107125

108126
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
109127
} // namespace rest_internal

google/cloud/storage/client.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,26 @@ Options DefaultOptions(Options opts) {
581581
"/iamapi");
582582
}
583583

584+
if (!o.has<storage_experimental::EnableReadHedgingOption>()) {
585+
o.set<storage_experimental::EnableReadHedgingOption>(false);
586+
}
587+
if (!o.has<storage_experimental::ReadHedgeRateLimitOption>()) {
588+
o.set<storage_experimental::ReadHedgeRateLimitOption>(0.0);
589+
}
590+
if (!o.has<storage_experimental::MaxConcurrentHedgesOption>()) {
591+
o.set<storage_experimental::MaxConcurrentHedgesOption>(0);
592+
}
593+
if (!o.has<storage_experimental::MaximumHedgeBufferOption>()) {
594+
o.set<storage_experimental::MaximumHedgeBufferOption>(64 * 1024 * 1024);
595+
}
596+
if (!o.has<storage_experimental::ReadHedgeDelayOption>()) {
597+
o.set<storage_experimental::ReadHedgeDelayOption>(
598+
std::chrono::milliseconds(500));
599+
}
600+
if (!o.has<storage_experimental::MaxReadHedgesOption>()) {
601+
o.set<storage_experimental::MaxReadHedgesOption>(2);
602+
}
603+
584604
auto logging = GetEnv("CLOUD_STORAGE_ENABLE_TRACING");
585605
if (logging) {
586606
for (auto c : absl::StrSplit(*logging, ',')) {
@@ -633,6 +653,12 @@ Options DefaultOptions(Options opts) {
633653
rest_defaults.set<rest::CAPathOption>(o.get<internal::CAPathOption>());
634654
}
635655

656+
// The (experimental) connect timeout is mapped the same way.
657+
if (o.has<storage_experimental::HttpConnectTimeoutOption>()) {
658+
rest_defaults.set<rest::HttpConnectTimeoutOption>(
659+
o.get<storage_experimental::HttpConnectTimeoutOption>());
660+
}
661+
636662
return google::cloud::internal::MergeOptions(std::move(o),
637663
std::move(rest_defaults));
638664
}

google/cloud/storage/client_test.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,21 @@ TEST_F(ClientTest, Timeouts) {
470470
internal::DefaultOptions().get<DownloadStallTimeoutOption>());
471471
}
472472

473+
TEST_F(ClientTest, ConnectTimeout) {
474+
namespace rest = ::google::cloud::rest_internal;
475+
476+
// The connect timeout is opt-in: when the application does not set it the
477+
// REST layer keeps libcurl's own default.
478+
EXPECT_FALSE(
479+
internal::DefaultOptions().has<rest::HttpConnectTimeoutOption>());
480+
481+
auto const options = internal::DefaultOptions(
482+
Options{}.set<storage_experimental::HttpConnectTimeoutOption>(
483+
std::chrono::milliseconds(1500)));
484+
EXPECT_EQ(std::chrono::milliseconds(1500),
485+
options.get<rest::HttpConnectTimeoutOption>());
486+
}
487+
473488
} // namespace
474489
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
475490
} // namespace storage

google/cloud/storage/google_cloud_cpp_storage.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ google_cloud_cpp_storage_hdrs = [
7474
"internal/hash_validator.h",
7575
"internal/hash_validator_impl.h",
7676
"internal/hash_values.h",
77+
"internal/hedged_object_read_source.h",
78+
"internal/hedging_thread_pool.h",
7779
"internal/hmac_key_metadata_parser.h",
7880
"internal/hmac_key_requests.h",
7981
"internal/http_response.h",
@@ -184,6 +186,7 @@ google_cloud_cpp_storage_srcs = [
184186
"internal/hash_validator.cc",
185187
"internal/hash_validator_impl.cc",
186188
"internal/hash_values.cc",
189+
"internal/hedged_object_read_source.cc",
187190
"internal/hmac_key_metadata_parser.cc",
188191
"internal/hmac_key_requests.cc",
189192
"internal/http_response.cc",

google/cloud/storage/google_cloud_cpp_storage.cmake

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ add_library(
117117
internal/hash_validator_impl.h
118118
internal/hash_values.cc
119119
internal/hash_values.h
120+
internal/hedged_object_read_source.cc
121+
internal/hedged_object_read_source.h
122+
internal/hedging_thread_pool.h
120123
internal/hmac_key_metadata_parser.cc
121124
internal/hmac_key_metadata_parser.h
122125
internal/hmac_key_requests.cc
@@ -447,6 +450,8 @@ if (BUILD_TESTING)
447450
internal/hash_function_impl_test.cc
448451
internal/hash_validator_test.cc
449452
internal/hash_values_test.cc
453+
internal/hedged_object_read_source_test.cc
454+
internal/hedging_thread_pool_test.cc
450455
internal/hmac_key_requests_test.cc
451456
internal/http_response_test.cc
452457
internal/logging_stub_test.cc

google/cloud/storage/internal/connection_impl.cc

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
#include "google/cloud/internal/disable_deprecation_warnings.inc"
1616
#include "google/cloud/storage/internal/connection_impl.h"
17+
#include "google/cloud/storage/internal/hedged_object_read_source.h"
1718
#include "google/cloud/storage/internal/retry_object_read_source.h"
1819
#include "google/cloud/storage/parallel_upload.h"
1920
#include "google/cloud/internal/filesystem.h"
2021
#include "google/cloud/internal/opentelemetry.h"
2122
#include "google/cloud/internal/rest_retry_loop.h"
2223
#include "google/cloud/log.h"
2324
#include "absl/strings/match.h"
25+
#include <algorithm>
2426
#include <chrono>
2527
#include <fstream>
2628
#include <functional>
@@ -155,7 +157,27 @@ std::shared_ptr<StorageConnectionImpl> StorageConnectionImpl::Create(
155157
StorageConnectionImpl::StorageConnectionImpl(
156158
std::unique_ptr<storage_internal::GenericStub> stub, Options options)
157159
: stub_(std::move(stub)),
158-
options_(MergeOptions(std::move(options), stub_->options())) {}
160+
options_(MergeOptions(std::move(options), stub_->options())) {
161+
if (options_.get<storage_experimental::EnableReadHedgingOption>()) {
162+
// The pool only runs stream-open attempts: one primary and (at most) a few
163+
// hedges per stream being opened. Size it to the number of connections the
164+
// REST layer can use, falling back to the hardware concurrency when the
165+
// connection pool is unbounded (`ConnectionPoolSizeOption == 0`).
166+
auto pool_size = options_.get<ConnectionPoolSizeOption>();
167+
if (pool_size == 0) {
168+
pool_size =
169+
(std::max<std::size_t>)(4, std::thread::hardware_concurrency());
170+
}
171+
auto const max_threads = 2 * pool_size;
172+
auto const rate_limit =
173+
options_.get<storage_experimental::ReadHedgeRateLimitOption>();
174+
auto const max_concurrent =
175+
options_.get<storage_experimental::MaxConcurrentHedgesOption>();
176+
// Allow bursts of up to one second worth of hedges.
177+
hedge_pool_ = std::make_shared<HedgingThreadPool>(
178+
max_threads, rate_limit, rate_limit, max_concurrent);
179+
}
180+
}
159181

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

@@ -392,15 +414,37 @@ StatusOr<std::unique_ptr<ObjectReadSource>> StorageConnectionImpl::ReadObject(
392414
*current, request, where);
393415
};
394416

395-
auto retry_policy = current->get<RetryPolicyOption>()->clone();
396-
auto backoff_policy = current->get<BackoffPolicyOption>()->clone();
397-
auto child = factory(request, *retry_policy, *backoff_policy);
398-
if (!child) return child;
417+
auto retry_source_factory =
418+
[factory, current,
419+
request]() -> StatusOr<std::unique_ptr<ObjectReadSource>> {
420+
auto retry_policy = current->get<RetryPolicyOption>()->clone();
421+
auto backoff_policy = current->get<BackoffPolicyOption>()->clone();
422+
auto child = factory(request, *retry_policy, *backoff_policy);
423+
if (!child) return child;
424+
return std::unique_ptr<ObjectReadSource>(
425+
std::make_unique<RetryObjectReadSource>(
426+
factory, current, request, *std::move(child),
427+
std::move(retry_policy), std::move(backoff_policy)));
428+
};
429+
430+
auto const enable_hedging =
431+
current->get<storage_experimental::EnableReadHedgingOption>();
432+
auto const delay = current->get<storage_experimental::ReadHedgeDelayOption>();
433+
auto const max_hedges =
434+
current->get<storage_experimental::MaxReadHedgesOption>();
435+
auto const max_buffer =
436+
current->get<storage_experimental::MaximumHedgeBufferOption>();
437+
438+
if (!enable_hedging || max_hedges <= 0 || !hedge_pool_) {
439+
return retry_source_factory();
440+
}
399441

442+
// `max_buffer` bounds the size of an individual read, which is only known
443+
// when the application calls `Read()`; the source applies it there.
400444
return std::unique_ptr<ObjectReadSource>(
401-
std::make_unique<RetryObjectReadSource>(
402-
std::move(factory), std::move(current), request, *std::move(child),
403-
std::move(retry_policy), std::move(backoff_policy)));
445+
std::make_unique<HedgedObjectReadSource>(hedge_pool_,
446+
std::move(retry_source_factory),
447+
delay, max_hedges, max_buffer));
404448
}
405449

406450
StatusOr<ListObjectsResponse> StorageConnectionImpl::ListObjects(

google/cloud/storage/internal/connection_impl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "google/cloud/storage/idempotency_policy.h"
1919
#include "google/cloud/storage/internal/generic_stub.h"
20+
#include "google/cloud/storage/internal/hedging_thread_pool.h"
2021
#include "google/cloud/storage/internal/storage_connection.h"
2122
#include "google/cloud/storage/object_read_stream.h"
2223
#include "google/cloud/storage/retry_policy.h"
@@ -187,6 +188,7 @@ class StorageConnectionImpl
187188

188189
std::unique_ptr<storage_internal::GenericStub> stub_;
189190
Options options_;
191+
std::shared_ptr<HedgingThreadPool> hedge_pool_;
190192
google::cloud::internal::InvocationIdGenerator invocation_id_generator_;
191193
};
192194

0 commit comments

Comments
 (0)