Skip to content

Commit 112a78d

Browse files
committed
feat(storage): add OpenTelemetry counters for read hedging
Add storage.read_hedging.hedges_dispatched and storage.read_hedging.hedge_won counters to track speculative attempts and secondary hedge wins.
1 parent 4200a62 commit 112a78d

10 files changed

Lines changed: 455 additions & 38 deletions

google/cloud/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ cc_library(
5454
name = "google_cloud_cpp_common_private",
5555
srcs = google_cloud_cpp_common_srcs + ["internal/build_info.cc"],
5656
hdrs = google_cloud_cpp_common_hdrs,
57+
defines = [
58+
"GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY",
59+
],
5760
linkopts = select({
5861
"@platforms//os:windows": [
5962
"-DEFAULTLIB:bcrypt.lib",

google/cloud/google_cloud_cpp_common.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ if (opentelemetry IN_LIST GOOGLE_CLOUD_CPP_ENABLE)
196196
if (opentelemetry-cpp_FOUND)
197197
target_link_libraries(google_cloud_cpp_common
198198
PUBLIC opentelemetry-cpp::api)
199+
target_compile_definitions(
200+
google_cloud_cpp_common
201+
PUBLIC # Enable OpenTelemetry features in google-cloud-cpp
202+
GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY)
199203
set(GOOGLE_CLOUD_CPP_FIND_OPTIONAL_DEPENDENCIES
200204
"find_dependency(opentelemetry-cpp)")
201205
set(GOOGLE_CLOUD_CPP_OPENTELEMETRY_API "opentelemetry_api")

google/cloud/storage/google_cloud_cpp_storage.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,12 @@ if (BUILD_TESTING)
498498
well_known_headers_test.cc
499499
well_known_parameters_test.cc)
500500

501+
if (opentelemetry IN_LIST GOOGLE_CLOUD_CPP_ENABLE)
502+
list(APPEND storage_client_unit_tests
503+
internal/hedged_object_read_source_otel_test.cc)
504+
list(SORT storage_client_unit_tests)
505+
endif ()
506+
501507
foreach (fname ${storage_client_unit_tests})
502508
google_cloud_cpp_add_executable(target "storage" "${fname}")
503509
target_link_libraries(

google/cloud/storage/internal/connection_impl.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ StorageConnectionImpl::StorageConnectionImpl(
159159
: stub_(std::move(stub)),
160160
options_(MergeOptions(std::move(options), stub_->options())) {
161161
if (options_.get<storage_experimental::EnableReadHedgingOption>()) {
162+
hedged_read_metrics_ = std::make_shared<HedgedReadMetrics>();
162163
// `DefaultOptions()` normally resolves these, but a connection can be
163164
// built without it, in which case the option is left at 0 ("automatic").
164165
// A pool sized 0 would accept reads it never runs, hanging the caller.
@@ -448,9 +449,9 @@ StatusOr<std::unique_ptr<ObjectReadSource>> StorageConnectionImpl::ReadObject(
448449
// `max_buffer` bounds the size of an individual read, which is only known
449450
// when the application calls `Read()`; the source applies it there.
450451
return std::unique_ptr<ObjectReadSource>(
451-
std::make_unique<HedgedObjectReadSource>(read_pool_, hedge_pool_,
452-
std::move(retry_source_factory),
453-
delay, max_hedges, max_buffer));
452+
std::make_unique<HedgedObjectReadSource>(
453+
read_pool_, hedge_pool_, std::move(retry_source_factory), delay,
454+
max_hedges, max_buffer, hedged_read_metrics_));
454455
}
455456

456457
StatusOr<ListObjectsResponse> StorageConnectionImpl::ListObjects(

google/cloud/storage/internal/connection_impl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ namespace cloud {
3232
namespace storage {
3333
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
3434
namespace internal {
35+
class HedgedReadMetrics;
36+
3537
/**
3638
* Decorates a `StorageConnection` to retry each operation.
3739
*/
@@ -190,6 +192,7 @@ class StorageConnectionImpl
190192
Options options_;
191193
std::shared_ptr<ThreadPool> read_pool_;
192194
std::shared_ptr<HedgingThreadPool> hedge_pool_;
195+
std::shared_ptr<HedgedReadMetrics> hedged_read_metrics_;
193196
google::cloud::internal::InvocationIdGenerator invocation_id_generator_;
194197
};
195198

google/cloud/storage/internal/hedged_object_read_source.cc

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
#include "google/cloud/storage/internal/hedged_object_read_source.h"
1616
#include "google/cloud/internal/make_status.h"
17+
#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
18+
#include <opentelemetry/metrics/meter.h>
19+
#include <opentelemetry/metrics/provider.h>
20+
#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
1721
#include <atomic>
1822
#include <cstring>
1923
#include <future>
@@ -24,12 +28,44 @@ namespace cloud {
2428
namespace storage {
2529
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2630
namespace internal {
31+
32+
#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
33+
HedgedReadMetrics::HedgedReadMetrics()
34+
: HedgedReadMetrics(opentelemetry::metrics::Provider::GetMeterProvider()) {}
35+
36+
HedgedReadMetrics::HedgedReadMetrics(
37+
opentelemetry::nostd::shared_ptr<
38+
opentelemetry::metrics::MeterProvider> const& provider) {
39+
if (!provider) return;
40+
opentelemetry::nostd::shared_ptr<opentelemetry::metrics::Meter> meter =
41+
provider->GetMeter("gl-cpp", version_string());
42+
if (!meter) return;
43+
44+
hedges_dispatched_ = meter->CreateUInt64Counter(
45+
"storage.read_hedging.hedges_dispatched",
46+
"Total number of speculative hedge read attempts dispatched", "{hedge}");
47+
hedge_won_ = meter->CreateUInt64Counter(
48+
"storage.read_hedging.hedge_won",
49+
"Total number of hedged read operations won by a secondary hedge attempt",
50+
"{request}");
51+
}
52+
53+
void HedgedReadMetrics::IncrementHedgesDispatched() {
54+
if (hedges_dispatched_) hedges_dispatched_->Add(1);
55+
}
56+
57+
void HedgedReadMetrics::IncrementHedgeWon() {
58+
if (hedge_won_) hedge_won_->Add(1);
59+
}
60+
#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
61+
2762
namespace {
2863

2964
struct RaceResult {
3065
StatusOr<ReadSourceResult> result;
3166
std::unique_ptr<ObjectReadSource> source;
3267
std::unique_ptr<char[]> buffer;
68+
bool is_primary;
3369
};
3470

3571
struct RaceState {
@@ -44,7 +80,8 @@ struct RaceState {
4480
void RunAttempt(std::shared_ptr<RaceState> const& state,
4581
HedgedObjectReadSource::ChildFactory const& factory,
4682
std::size_t n, bool resolve_on_open_error,
47-
std::shared_ptr<HedgingThreadPool> release_slot) {
83+
std::shared_ptr<HedgingThreadPool> release_slot,
84+
bool is_primary) {
4885
// Releases the acquired hedge concurrency slot upon function exit across
4986
// all code paths (early return on open/allocation error, race winner, or
5087
// race loser). For primary attempts, release_slot is nullptr.
@@ -55,13 +92,13 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
5592
}
5693
} guard{std::move(release_slot)};
5794

58-
auto source = factory();
95+
StatusOr<std::unique_ptr<ObjectReadSource>> source = factory();
5996
if (!source) {
6097
if (!resolve_on_open_error) return;
6198
bool expected = false;
6299
if (state->resolved.compare_exchange_strong(expected, true)) {
63100
state->promise.set_value(
64-
RaceResult{std::move(source).status(), nullptr, {}});
101+
RaceResult{std::move(source).status(), nullptr, {}, is_primary});
65102
}
66103
return;
67104
}
@@ -74,32 +111,84 @@ void RunAttempt(std::shared_ptr<RaceState> const& state,
74111
google::cloud::internal::ResourceExhaustedError(
75112
"Out of memory allocating hedge buffer", GCP_ERROR_INFO()),
76113
nullptr,
77-
{}});
114+
{},
115+
is_primary});
78116
}
79117
return;
80118
}
81-
auto result = (*source)->Read(buffer.get(), n);
119+
StatusOr<ReadSourceResult> result = (*source)->Read(buffer.get(), n);
82120
bool expected = false;
83121
if (state->resolved.compare_exchange_strong(expected, true)) {
84-
state->promise.set_value(
85-
RaceResult{std::move(result), *std::move(source), std::move(buffer)});
122+
state->promise.set_value(RaceResult{std::move(result), *std::move(source),
123+
std::move(buffer), is_primary});
86124
} else {
87125
(*source)->Close();
88126
}
89127
}
90128

129+
void DispatchHedges(std::shared_ptr<RaceState> const& state,
130+
std::future<RaceResult>& future, std::size_t n,
131+
int max_hedges, std::chrono::milliseconds delay,
132+
std::shared_ptr<HedgingThreadPool> const& hedge_pool,
133+
HedgedObjectReadSource::ChildFactory const& child_factory,
134+
HedgedReadMetrics& metrics) {
135+
for (int hedges_dispatched = 0; hedges_dispatched < max_hedges;) {
136+
if (future.wait_for(delay) != std::future_status::timeout) break;
137+
if (!hedge_pool->TryAcquireHedgeToken()) {
138+
// When delay is 0ms (or token acquisition fails), back off briefly on
139+
// the future instead of busy-spinning if tokens or concurrency slots are
140+
// temporarily exhausted.
141+
if (delay == std::chrono::milliseconds::zero() &&
142+
future.wait_for(std::chrono::milliseconds(10)) !=
143+
std::future_status::timeout) {
144+
break;
145+
}
146+
continue;
147+
}
148+
auto hedge = [state, factory = child_factory, n, pool = hedge_pool] {
149+
RunAttempt(state, factory, n, /*resolve_on_open_error=*/false, pool,
150+
/*is_primary=*/false);
151+
};
152+
if (!hedge_pool->Enqueue(hedge)) {
153+
hedge_pool->ReleaseHedgeSlot();
154+
break;
155+
}
156+
++hedges_dispatched;
157+
metrics.IncrementHedgesDispatched();
158+
}
159+
}
160+
91161
} // namespace
92162

93163
HedgedObjectReadSource::HedgedObjectReadSource(
94164
std::shared_ptr<ThreadPool> read_pool,
95165
std::shared_ptr<HedgingThreadPool> hedge_pool, ChildFactory child_factory,
96166
std::chrono::milliseconds delay, int max_hedges, std::size_t max_buffer)
167+
: HedgedObjectReadSource(std::move(read_pool), std::move(hedge_pool),
168+
std::move(child_factory), delay, max_hedges,
169+
max_buffer,
170+
std::make_shared<HedgedReadMetrics>()) {}
171+
172+
HedgedObjectReadSource::HedgedObjectReadSource(
173+
std::shared_ptr<ThreadPool> read_pool,
174+
std::shared_ptr<HedgingThreadPool> hedge_pool, ChildFactory child_factory,
175+
std::chrono::milliseconds delay, int max_hedges, std::size_t max_buffer,
176+
std::shared_ptr<HedgedReadMetrics> metrics)
97177
: read_pool_(std::move(read_pool)),
98178
hedge_pool_(std::move(hedge_pool)),
99179
child_factory_(std::move(child_factory)),
100180
delay_(delay),
101181
max_hedges_(max_hedges),
102-
max_buffer_(max_buffer) {}
182+
max_buffer_(max_buffer),
183+
#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
184+
metrics_(metrics ? std::move(metrics)
185+
: std::make_shared<HedgedReadMetrics>(nullptr)) {
186+
}
187+
#else
188+
metrics_(metrics ? std::move(metrics)
189+
: std::make_shared<HedgedReadMetrics>()) {
190+
}
191+
#endif
103192

104193
bool HedgedObjectReadSource::IsOpen() const {
105194
if (active_child_) return active_child_->IsOpen();
@@ -130,48 +219,30 @@ StatusOr<ReadSourceResult> HedgedObjectReadSource::Read(char* buf,
130219
// the tail latency it avoids, so open the stream without hedging and read
131220
// straight into the caller's buffer.
132221
if (n > max_buffer_) {
133-
auto child = child_factory_();
222+
StatusOr<std::unique_ptr<ObjectReadSource>> child = child_factory_();
134223
if (!child) return std::move(child).status();
135224
active_child_ = *std::move(child);
136225
return active_child_->Read(buf, n);
137226
}
138227

139228
auto state = std::make_shared<RaceState>();
140-
auto future = state->promise.get_future();
229+
std::future<RaceResult> future = state->promise.get_future();
141230

142231
auto primary = [state, factory = child_factory_, n] {
143-
RunAttempt(state, factory, n, /*resolve_on_open_error=*/true, nullptr);
232+
RunAttempt(state, factory, n, /*resolve_on_open_error=*/true, nullptr,
233+
/*is_primary=*/true);
144234
};
145235
// The primary attempt is scheduled on the dedicated read pool.
146236
// If the pool is shutting down run the attempt inline, the read must
147237
// complete either way.
148238
if (!read_pool_->Enqueue(primary)) primary();
149239

150-
for (int hedges_dispatched = 0; hedges_dispatched < max_hedges_;) {
151-
if (future.wait_for(delay_) != std::future_status::timeout) break;
152-
if (!hedge_pool_->TryAcquireHedgeToken()) {
153-
// When delay_ is 0ms (or token acquisition fails), back off briefly on
154-
// the future instead of busy-spinning if tokens or concurrency slots are
155-
// temporarily exhausted.
156-
if (delay_ == std::chrono::milliseconds::zero()) {
157-
if (future.wait_for(std::chrono::milliseconds(10)) !=
158-
std::future_status::timeout) {
159-
break;
160-
}
161-
}
162-
continue;
163-
}
164-
auto hedge = [state, factory = child_factory_, n, pool = hedge_pool_] {
165-
RunAttempt(state, factory, n, /*resolve_on_open_error=*/false, pool);
166-
};
167-
if (!hedge_pool_->Enqueue(hedge)) {
168-
hedge_pool_->ReleaseHedgeSlot();
169-
break;
170-
}
171-
++hedges_dispatched;
172-
}
240+
DispatchHedges(state, future, n, max_hedges_, delay_, hedge_pool_,
241+
child_factory_, *metrics_);
242+
243+
RaceResult race = future.get();
244+
if (!race.is_primary) metrics_->IncrementHedgeWon();
173245

174-
auto race = future.get();
175246
active_child_ = std::move(race.source);
176247
if (race.result.ok() && race.result->bytes_received > 0) {
177248
std::memcpy(buf, race.buffer.get(), race.result->bytes_received);

google/cloud/storage/internal/hedged_object_read_source.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
#include "google/cloud/storage/internal/hedging_thread_pool.h"
1919
#include "google/cloud/storage/internal/object_read_source.h"
2020
#include "google/cloud/storage/version.h"
21+
#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
22+
#include <opentelemetry/metrics/meter_provider.h>
23+
#include <opentelemetry/metrics/sync_instruments.h>
24+
#include <opentelemetry/nostd/shared_ptr.h>
25+
#include <opentelemetry/nostd/unique_ptr.h>
26+
#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
2127
#include <chrono>
28+
#include <cstdint>
2229
#include <functional>
2330
#include <memory>
2431

@@ -28,6 +35,56 @@ namespace storage {
2835
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2936
namespace internal {
3037

38+
#ifdef GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
39+
/**
40+
* OpenTelemetry metrics helper for read hedging operations.
41+
*/
42+
class HedgedReadMetrics {
43+
public:
44+
HedgedReadMetrics();
45+
explicit HedgedReadMetrics(
46+
opentelemetry::nostd::shared_ptr<
47+
opentelemetry::metrics::MeterProvider> const& provider);
48+
49+
~HedgedReadMetrics() = default;
50+
HedgedReadMetrics(HedgedReadMetrics const&) = delete;
51+
HedgedReadMetrics& operator=(HedgedReadMetrics const&) = delete;
52+
HedgedReadMetrics(HedgedReadMetrics&&) = default;
53+
HedgedReadMetrics& operator=(HedgedReadMetrics&&) = default;
54+
55+
/// Records that a speculative hedge read attempt was dispatched.
56+
void IncrementHedgesDispatched();
57+
58+
/// Records that a hedged read operation was won by a hedge attempt.
59+
void IncrementHedgeWon();
60+
61+
private:
62+
opentelemetry::nostd::unique_ptr<
63+
opentelemetry::metrics::Counter<std::uint64_t>>
64+
hedges_dispatched_;
65+
opentelemetry::nostd::unique_ptr<
66+
opentelemetry::metrics::Counter<std::uint64_t>>
67+
hedge_won_;
68+
};
69+
#else
70+
/**
71+
* No-op OpenTelemetry metrics fallback for read hedging operations when
72+
* OpenTelemetry is disabled.
73+
*/
74+
class HedgedReadMetrics {
75+
public:
76+
HedgedReadMetrics() = default;
77+
~HedgedReadMetrics() = default;
78+
HedgedReadMetrics(HedgedReadMetrics const&) = delete;
79+
HedgedReadMetrics& operator=(HedgedReadMetrics const&) = delete;
80+
HedgedReadMetrics(HedgedReadMetrics&&) = default;
81+
HedgedReadMetrics& operator=(HedgedReadMetrics&&) = default;
82+
83+
void IncrementHedgesDispatched() {}
84+
void IncrementHedgeWon() {}
85+
};
86+
#endif // GOOGLE_CLOUD_CPP_HAVE_OPENTELEMETRY
87+
3188
/**
3289
* Hedge the *open* of an `ObjectReadSource` to reduce tail latency.
3390
*
@@ -60,6 +117,13 @@ class HedgedObjectReadSource : public ObjectReadSource {
60117
std::chrono::milliseconds delay, int max_hedges,
61118
std::size_t max_buffer);
62119

120+
HedgedObjectReadSource(std::shared_ptr<ThreadPool> read_pool,
121+
std::shared_ptr<HedgingThreadPool> hedge_pool,
122+
ChildFactory child_factory,
123+
std::chrono::milliseconds delay, int max_hedges,
124+
std::size_t max_buffer,
125+
std::shared_ptr<HedgedReadMetrics> metrics);
126+
63127
~HedgedObjectReadSource() override = default;
64128

65129
bool IsOpen() const override;
@@ -73,6 +137,7 @@ class HedgedObjectReadSource : public ObjectReadSource {
73137
std::chrono::milliseconds delay_;
74138
int max_hedges_;
75139
std::size_t max_buffer_;
140+
std::shared_ptr<HedgedReadMetrics> metrics_;
76141

77142
std::unique_ptr<ObjectReadSource> active_child_;
78143
bool is_closed_ = false;

0 commit comments

Comments
 (0)