Skip to content

Commit 9a014d7

Browse files
committed
feat(storage): separate read and hedging thread pools
- Extract lazy, dynamically scaling ThreadPool primitive from HedgingThreadPool. - Separate StorageConnectionImpl thread pool into a dedicated ReadThreadPool (for primary stream opens) and a HedgingThreadPool (for speculative secondary hedges). - Add ReadThreadPoolSizeOption and HedgingThreadPoolSizeOption with auto-scaling defaults to prevent read bottlenecking under high concurrency. - Extract DefaultReadThreadPoolSize() and DefaultHedgingThreadPoolSize() helpers to share sizing logic between DefaultOptions() and connection initialization. - Enqueue primary read attempt to ReadThreadPool and speculative hedge attempts to HedgingThreadPool, ensuring complete fault and stall isolation. - Clamp ThreadPool capacity to at least 1 to prevent deadlock on zero sizing. - Add unit tests verifying thread pool execution, default sizes, zero-size handling, lazy spawning, and pool isolation under saturation.
1 parent 82fcb08 commit 9a014d7

9 files changed

Lines changed: 424 additions & 147 deletions

google/cloud/storage/client.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "google/cloud/storage/idempotency_policy.h"
1818
#include "google/cloud/storage/internal/base64.h"
1919
#include "google/cloud/storage/internal/connection_factory.h"
20+
#include "google/cloud/storage/internal/hedging_thread_pool.h"
2021
#include "google/cloud/storage/options.h"
2122
#include "google/cloud/internal/curl_handle.h"
2223
#include "google/cloud/internal/curl_options.h"
@@ -600,6 +601,15 @@ Options DefaultOptions(Options opts) {
600601
if (!o.has<storage_experimental::MaxReadHedgesOption>()) {
601602
o.set<storage_experimental::MaxReadHedgesOption>(2);
602603
}
604+
if (!o.has<storage_experimental::ReadThreadPoolSizeOption>()) {
605+
o.set<storage_experimental::ReadThreadPoolSizeOption>(
606+
internal::DefaultReadThreadPoolSize());
607+
}
608+
if (!o.has<storage_experimental::HedgingThreadPoolSizeOption>()) {
609+
o.set<storage_experimental::HedgingThreadPoolSizeOption>(
610+
internal::DefaultHedgingThreadPoolSize(
611+
o.get<storage_experimental::MaxConcurrentHedgesOption>()));
612+
}
603613

604614
auto logging = GetEnv("CLOUD_STORAGE_ENABLE_TRACING");
605615
if (logging) {

google/cloud/storage/internal/connection_impl.cc

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,23 +159,29 @@ StorageConnectionImpl::StorageConnectionImpl(
159159
: stub_(std::move(stub)),
160160
options_(MergeOptions(std::move(options), stub_->options())) {
161161
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());
162+
// `DefaultOptions()` normally resolves these, but a connection can be
163+
// built without it, in which case the option is left at 0 ("automatic").
164+
// A pool sized 0 would accept reads it never runs, hanging the caller.
165+
std::size_t read_threads =
166+
options_.get<storage_experimental::ReadThreadPoolSizeOption>();
167+
if (read_threads == 0) read_threads = DefaultReadThreadPoolSize();
168+
// The read pool only ever has one thread per in-flight application read,
169+
// and each one blocks inside a synchronous read. Once it saturates, new
170+
// primaries queue behind blocked ones and reads degrade to hedge-only.
171+
read_pool_ = std::make_shared<ThreadPool>(read_threads);
172+
173+
std::int64_t const max_concurrent =
174+
options_.get<storage_experimental::MaxConcurrentHedgesOption>();
175+
std::size_t hedge_threads =
176+
options_.get<storage_experimental::HedgingThreadPoolSizeOption>();
177+
if (hedge_threads == 0) {
178+
hedge_threads = DefaultHedgingThreadPoolSize(max_concurrent);
170179
}
171-
auto const max_threads = 2 * pool_size;
172-
auto const rate_limit =
180+
double const rate_limit =
173181
options_.get<storage_experimental::ReadHedgeRateLimitOption>();
174-
auto const max_concurrent =
175-
options_.get<storage_experimental::MaxConcurrentHedgesOption>();
176182
// Allow bursts of up to one second worth of hedges.
177183
hedge_pool_ = std::make_shared<HedgingThreadPool>(
178-
max_threads, rate_limit, rate_limit, max_concurrent);
184+
hedge_threads, rate_limit, rate_limit, max_concurrent);
179185
}
180186
}
181187

@@ -435,14 +441,14 @@ StatusOr<std::unique_ptr<ObjectReadSource>> StorageConnectionImpl::ReadObject(
435441
auto const max_buffer =
436442
current->get<storage_experimental::MaximumHedgeBufferOption>();
437443

438-
if (!enable_hedging || max_hedges <= 0 || !hedge_pool_) {
444+
if (!enable_hedging || max_hedges <= 0 || !hedge_pool_ || !read_pool_) {
439445
return retry_source_factory();
440446
}
441447

442448
// `max_buffer` bounds the size of an individual read, which is only known
443449
// when the application calls `Read()`; the source applies it there.
444450
return std::unique_ptr<ObjectReadSource>(
445-
std::make_unique<HedgedObjectReadSource>(hedge_pool_,
451+
std::make_unique<HedgedObjectReadSource>(read_pool_, hedge_pool_,
446452
std::move(retry_source_factory),
447453
delay, max_hedges, max_buffer));
448454
}

google/cloud/storage/internal/connection_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class StorageConnectionImpl
188188

189189
std::unique_ptr<storage_internal::GenericStub> stub_;
190190
Options options_;
191+
std::shared_ptr<ThreadPool> read_pool_;
191192
std::shared_ptr<HedgingThreadPool> hedge_pool_;
192193
google::cloud::internal::InvocationIdGenerator invocation_id_generator_;
193194
};

google/cloud/storage/internal/hedged_object_read_source.cc

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
5555
auto source = factory();
5656
if (!source) {
5757
if (!resolve_on_open_error) return;
58-
auto expected = false;
58+
bool expected = false;
5959
if (state->resolved.compare_exchange_strong(expected, true)) {
6060
state->promise.set_value(
6161
RaceResult{std::move(source).status(), nullptr, {}});
@@ -65,7 +65,7 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
6565
std::unique_ptr<char[]> buffer(new (std::nothrow) char[n]);
6666
if (!buffer) {
6767
if (!resolve_on_open_error) return;
68-
auto expected = false;
68+
bool expected = false;
6969
if (state->resolved.compare_exchange_strong(expected, true)) {
7070
state->promise.set_value(RaceResult{
7171
google::cloud::internal::ResourceExhaustedError(
@@ -76,7 +76,7 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
7676
return;
7777
}
7878
auto result = (*source)->Read(buffer.get(), n);
79-
auto expected = false;
79+
bool expected = false;
8080
if (state->resolved.compare_exchange_strong(expected, true)) {
8181
state->promise.set_value(
8282
RaceResult{std::move(result), *std::move(source), std::move(buffer)});
@@ -88,9 +88,11 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
8888
} // namespace
8989

9090
HedgedObjectReadSource::HedgedObjectReadSource(
91+
std::shared_ptr<ThreadPool> read_pool,
9192
std::shared_ptr<HedgingThreadPool> hedge_pool, ChildFactory child_factory,
9293
std::chrono::milliseconds delay, int max_hedges, std::size_t max_buffer)
93-
: hedge_pool_(std::move(hedge_pool)),
94+
: read_pool_(std::move(read_pool)),
95+
hedge_pool_(std::move(hedge_pool)),
9496
child_factory_(std::move(child_factory)),
9597
delay_(delay),
9698
max_hedges_(max_hedges),
@@ -135,9 +137,10 @@ StatusOr<ReadSourceResult> HedgedObjectReadSource::Read(char* buf,
135137
auto primary = [state, factory = child_factory_, n] {
136138
RunAttempt(state, factory, n, /*resolve_on_open_error=*/true, nullptr);
137139
};
140+
// The primary attempt is scheduled on the dedicated read pool.
138141
// If the pool is shutting down run the attempt inline, the read must
139142
// complete either way.
140-
if (!hedge_pool_->Enqueue(primary)) primary();
143+
if (!read_pool_->Enqueue(primary)) primary();
141144

142145
for (int i = 0; i != max_hedges_; ++i) {
143146
if (future.wait_for(delay_) != std::future_status::timeout) break;

google/cloud/storage/internal/hedged_object_read_source.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ class HedgedObjectReadSource : public ObjectReadSource {
5454
using ChildFactory =
5555
std::function<StatusOr<std::unique_ptr<ObjectReadSource>>()>;
5656

57-
HedgedObjectReadSource(std::shared_ptr<HedgingThreadPool> hedge_pool,
57+
HedgedObjectReadSource(std::shared_ptr<ThreadPool> read_pool,
58+
std::shared_ptr<HedgingThreadPool> hedge_pool,
5859
ChildFactory child_factory,
5960
std::chrono::milliseconds delay, int max_hedges,
6061
std::size_t max_buffer);
@@ -66,6 +67,7 @@ class HedgedObjectReadSource : public ObjectReadSource {
6667
StatusOr<ReadSourceResult> Read(char* buf, std::size_t n) override;
6768

6869
private:
70+
std::shared_ptr<ThreadPool> read_pool_;
6971
std::shared_ptr<HedgingThreadPool> hedge_pool_;
7072
ChildFactory child_factory_;
7173
std::chrono::milliseconds delay_;

0 commit comments

Comments
 (0)