Skip to content

Commit 3549453

Browse files
committed
[core][gcs] Split out Redis wrapping and capture TableName as std::string
Address review on #65583: - Move the Redis-backend wrapping (MaybeObserve, the gcs_redis_storage_metrics_enabled flag, and the shared_ptr delegate change) out of this PR into a separate one. - NormalizeTableNameLabel now returns a std::string and the latency callbacks capture it by value. The string_view-into-static-storage design saved at most one SSO copy per operation, which is not measurable next to the storage round trip, so drop it along with the lifetime/SSO comments and the tests that only existed to support it. Signed-off-by: zzchun <zzchun8@gmail.com>
1 parent 4c43475 commit 3549453

13 files changed

Lines changed: 32 additions & 322 deletions

‎doc/source/ray-observability/reference/system-metrics.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ These variables must be set in the Ray node and Dashboard Reporter process envir
5050
- Current number of placement groups by state. The State label (e.g., PENDING, CREATED, REMOVED) describes the state of the placement group. See [rpc::PlacementGroupTable](https://github.com/ray-project/ray/blob/e85355b9b593742b4f5cb72cab92051980fa73d3/src/ray/protobuf/gcs.proto#L517) for more information.
5151
* - `ray_gcs_storage_operation_count_total`
5252
- `Operation`, `TableName`
53-
- Number of operations the GCS invoked on its storage backend, broken down by operation (`Put`, `Get`, `GetAll`, `MultiGet`, `Delete`, `BatchDelete`, `GetKeys`, `Exists`, `GetNextJobID`) and by the GCS table it addressed. `TableName` is bounded to the names of Ray's internal `TablePrefix` enum, plus `JobCounter` for `GetNextJobID` and `OTHER` for a table name outside that enum. The values the GCS produces in practice are `JOB`, `ACTOR`, `ACTOR_TASK_SPEC`, `PLACEMENT_GROUP`, `NODE`, `WORKERS` and `KV`; the remaining enum names are legacy and no current code path emits them. Keys, internal-KV namespaces and any other user-supplied string never appear in labels. Recorded when the operation is issued, so this leads `ray_gcs_storage_operation_latency_ms` by the number of operations in flight. On the external-Redis backend, set `RAY_gcs_redis_storage_metrics_enabled=false` to stop exporting both metrics; the in-memory and RocksDB backends always export them.
53+
- Number of operations the GCS invoked on its storage backend, broken down by operation (`Put`, `Get`, `GetAll`, `MultiGet`, `Delete`, `BatchDelete`, `GetKeys`, `Exists`, `GetNextJobID`) and by the GCS table it addressed. `TableName` is bounded to the names of Ray's internal `TablePrefix` enum, plus `JobCounter` for `GetNextJobID` and `OTHER` for a table name outside that enum. The values the GCS produces in practice are `JOB`, `ACTOR`, `ACTOR_TASK_SPEC`, `PLACEMENT_GROUP`, `NODE`, `WORKERS` and `KV`; the remaining enum names are legacy and no current code path emits them. Keys, internal-KV namespaces and any other user-supplied string never appear in labels. Recorded when the operation is issued, so this leads `ray_gcs_storage_operation_latency_ms` by the number of operations in flight. Exported only when the GCS storage backend is in-memory or RocksDB.
5454
* - `ray_gcs_storage_operation_latency_ms`
5555
- `Operation`, `TableName`
5656
- Time to invoke an operation on the GCS storage backend, with the same label domain as `ray_gcs_storage_operation_count_total`. Recorded when the operation completes.

‎src/ray/common/ray_config_def.h‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,6 @@ RAY_CONFIG(int64_t, redis_tcp_keepalive_interval_seconds, 30)
443443
/// keepalive is enabled.
444444
RAY_CONFIG(int64_t, redis_tcp_keepalive_probes, 3)
445445

446-
/// Whether the GCS wraps its Redis store client in ObservableStoreClient, which
447-
/// is what makes gcs_storage_operation_count and
448-
/// gcs_storage_operation_latency_ms exist on the external-Redis backend at all.
449-
/// Costs two ray::stats::Metric::Record calls per storage operation on a path
450-
/// that previously had none. Set to false to restore the pre-change behavior;
451-
/// the in-memory and RocksDB backends are unaffected either way, because they
452-
/// have always been wrapped.
453-
RAY_CONFIG(bool, gcs_redis_storage_metrics_enabled, true)
454-
455446
/// Number of retries for a redis request failure.
456447
RAY_CONFIG(size_t, num_redis_request_retries, 5)
457448

‎src/ray/gcs/gcs_server.cc‎

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,7 @@ GcsServer::GcsServer(const ray::gcs::GcsServerConfig &config,
221221
RayConfig::instance().gcs_redis_heartbeat_interval_milliseconds(),
222222
"GCSServer.redis_health_check");
223223

224-
// Observe the Redis backend like every other one. The health check above
225-
// keeps its own concrete pointer and so bypasses the wrapper, which is
226-
// correct: a PING is not a storage operation and should not appear in
227-
// gcs_storage_operation_count. Only the Redis backend is gated -- the other
228-
// two have always been observed, so there is nothing new to roll back.
229-
store_client = MaybeObserve(std::move(redis_store_client),
230-
RayConfig::instance().gcs_redis_storage_metrics_enabled(),
231-
metrics_.storage_operation_latency_in_ms_histogram,
232-
metrics_.storage_operation_count_counter,
233-
clock_);
224+
store_client = redis_store_client;
234225
break;
235226
}
236227
#if defined(__linux__)
@@ -1011,16 +1002,11 @@ void GcsServer::InitUsageStatsClient() {
10111002

10121003
void GcsServer::InitKVManager() {
10131004
auto &io_context = io_context_provider_.GetIOContext<GcsInternalKVManager>();
1014-
std::shared_ptr<StoreClient> store_client;
1005+
std::unique_ptr<StoreClient> store_client;
10151006
switch (storage_type_) {
10161007
case (StorageType::REDIS_PERSIST):
1017-
store_client = MaybeObserve(
1018-
std::make_shared<RedisStoreClient>(
1019-
io_context, GetRedisClientOptions(), clock_, MakeRedisMetrics(metrics_)),
1020-
RayConfig::instance().gcs_redis_storage_metrics_enabled(),
1021-
metrics_.storage_operation_latency_in_ms_histogram,
1022-
metrics_.storage_operation_count_counter,
1023-
clock_);
1008+
store_client = std::make_unique<RedisStoreClient>(
1009+
io_context, GetRedisClientOptions(), clock_, MakeRedisMetrics(metrics_));
10241010
break;
10251011
case (StorageType::IN_MEMORY):
10261012
store_client = std::make_unique<ObservableStoreClient>(

‎src/ray/gcs/store_client/observable_store_client.cc‎

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,6 @@ namespace {
2929

3030
// Both tags for one recording site. `table` must already be normalized (see
3131
// NormalizeTableNameLabel) so that TableName stays inside a bounded domain.
32-
//
33-
// MetricInterface::Record takes the tag value as an owned std::string, so the
34-
// copy here is unavoidable. Every table name a StoreClient caller can actually
35-
// produce is at most 15 characters (PLACEMENT_GROUP and ACTOR_TASK_SPEC are the
36-
// longest), which fits libstdc++'s small-string buffer, so in practice it does
37-
// not allocate. The label domain is wider than that -- NormalizeTableNameLabel
38-
// accepts every rpc::TablePrefix name, and four of those exceed 15 characters,
39-
// PLACEMENT_GROUP_SCHEDULE being the longest at 24 -- but none of them is
40-
// reachable from any current caller, and reaching one would cost a small
41-
// allocation rather than a wrong label. TableNameLabelTest pins the
42-
// reachable-name bound so this comment cannot rot silently.
4332
std::vector<std::pair<std::string_view, std::string>> MetricTags(
4433
std::string_view operation, std::string_view table) {
4534
return {{"Operation", std::string(operation)}, {"TableName", std::string(table)}};
@@ -55,35 +44,13 @@ double ElapsedMs(ClockInterface &clock, SteadyTimePoint start) {
5544

5645
} // namespace
5746

58-
std::shared_ptr<StoreClient> MaybeObserve(
59-
std::shared_ptr<StoreClient> delegate,
60-
bool enabled,
61-
ray::observability::MetricInterface &storage_operation_latency_in_ms_histogram,
62-
ray::observability::MetricInterface &storage_operation_count_counter,
63-
ClockInterface &clock) {
64-
if (!enabled) {
65-
return delegate;
66-
}
67-
return std::make_shared<ObservableStoreClient>(
68-
std::move(delegate),
69-
storage_operation_latency_in_ms_histogram,
70-
storage_operation_count_counter,
71-
clock);
72-
}
73-
7447
void ObservableStoreClient::AsyncPut(const std::string &table_name,
7548
const std::string &key,
7649
std::string data,
7750
bool overwrite,
7851
Postable<void(bool)> callback) {
7952
const auto start = clock_.SteadyNow();
80-
// Normalized once per operation and captured as a view, not as a string: the
81-
// observer below runs on the io_context after this call has returned, so a
82-
// view borrowed from `table_name` could outlive the caller's string and
83-
// dangle. NormalizeTableNameLabel returns a view into protobuf's static
84-
// descriptor storage instead, which is why capturing it costs no allocation
85-
// and carries no lifetime risk.
86-
const std::string_view table = NormalizeTableNameLabel(table_name);
53+
const std::string table = NormalizeTableNameLabel(table_name);
8754
storage_operation_count_counter_.Record(1, MetricTags("Put", table));
8855
delegate_->AsyncPut(table_name,
8956
key,
@@ -100,7 +67,7 @@ void ObservableStoreClient::AsyncGet(
10067
const std::string &key,
10168
ToPostable<rpc::OptionalItemCallback<std::string>> callback) {
10269
const auto start = clock_.SteadyNow();
103-
const std::string_view table = NormalizeTableNameLabel(table_name);
70+
const std::string table = NormalizeTableNameLabel(table_name);
10471
storage_operation_count_counter_.Record(1, MetricTags("Get", table));
10572
delegate_->AsyncGet(
10673
table_name, key, std::move(callback).OnInvocation([this, start, table]() {
@@ -113,7 +80,7 @@ void ObservableStoreClient::AsyncGetAll(
11380
const std::string &table_name,
11481
Postable<void(absl::flat_hash_map<std::string, std::string>)> callback) {
11582
const auto start = clock_.SteadyNow();
116-
const std::string_view table = NormalizeTableNameLabel(table_name);
83+
const std::string table = NormalizeTableNameLabel(table_name);
11784
storage_operation_count_counter_.Record(1, MetricTags("GetAll", table));
11885
delegate_->AsyncGetAll(table_name,
11986
std::move(callback).OnInvocation([this, start, table]() {
@@ -127,7 +94,7 @@ void ObservableStoreClient::AsyncMultiGet(
12794
const std::vector<std::string> &keys,
12895
Postable<void(absl::flat_hash_map<std::string, std::string>)> callback) {
12996
const auto start = clock_.SteadyNow();
130-
const std::string_view table = NormalizeTableNameLabel(table_name);
97+
const std::string table = NormalizeTableNameLabel(table_name);
13198
storage_operation_count_counter_.Record(1, MetricTags("MultiGet", table));
13299
delegate_->AsyncMultiGet(
133100
table_name, keys, std::move(callback).OnInvocation([this, start, table]() {
@@ -140,7 +107,7 @@ void ObservableStoreClient::AsyncDelete(const std::string &table_name,
140107
const std::string &key,
141108
Postable<void(bool)> callback) {
142109
const auto start = clock_.SteadyNow();
143-
const std::string_view table = NormalizeTableNameLabel(table_name);
110+
const std::string table = NormalizeTableNameLabel(table_name);
144111
storage_operation_count_counter_.Record(1, MetricTags("Delete", table));
145112
delegate_->AsyncDelete(
146113
table_name, key, std::move(callback).OnInvocation([this, start, table]() {
@@ -153,7 +120,7 @@ void ObservableStoreClient::AsyncBatchDelete(const std::string &table_name,
153120
const std::vector<std::string> &keys,
154121
Postable<void(int64_t)> callback) {
155122
const auto start = clock_.SteadyNow();
156-
const std::string_view table = NormalizeTableNameLabel(table_name);
123+
const std::string table = NormalizeTableNameLabel(table_name);
157124
storage_operation_count_counter_.Record(1, MetricTags("BatchDelete", table));
158125
delegate_->AsyncBatchDelete(
159126
table_name, keys, std::move(callback).OnInvocation([this, start, table]() {
@@ -182,7 +149,7 @@ void ObservableStoreClient::AsyncGetKeys(
182149
const std::string &prefix,
183150
Postable<void(std::vector<std::string>)> callback) {
184151
const auto start = clock_.SteadyNow();
185-
const std::string_view table = NormalizeTableNameLabel(table_name);
152+
const std::string table = NormalizeTableNameLabel(table_name);
186153
storage_operation_count_counter_.Record(1, MetricTags("GetKeys", table));
187154
delegate_->AsyncGetKeys(
188155
table_name, prefix, std::move(callback).OnInvocation([this, start, table]() {
@@ -195,7 +162,7 @@ void ObservableStoreClient::AsyncExists(const std::string &table_name,
195162
const std::string &key,
196163
Postable<void(bool)> callback) {
197164
const auto start = clock_.SteadyNow();
198-
const std::string_view table = NormalizeTableNameLabel(table_name);
165+
const std::string table = NormalizeTableNameLabel(table_name);
199166
storage_operation_count_counter_.Record(1, MetricTags("Exists", table));
200167
delegate_->AsyncExists(
201168
table_name, key, std::move(callback).OnInvocation([this, start, table]() {

‎src/ray/gcs/store_client/observable_store_client.h‎

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,11 @@ namespace ray {
2727

2828
namespace gcs {
2929

30-
/// Wraps `delegate` in an ObservableStoreClient when `enabled`, and returns it
31-
/// unchanged otherwise.
32-
///
33-
/// Exists as a named function, and takes `enabled` rather than reading
34-
/// RayConfig itself, so that both GCS construction sites share one branch and
35-
/// so that the branch is testable without standing up a GcsServer.
36-
///
37-
/// \param delegate The store client to wrap.
38-
/// \param enabled Whether to observe. GcsServer passes
39-
/// RayConfig::gcs_redis_storage_metrics_enabled() for the Redis backend; the
40-
/// in-memory and RocksDB backends are always observed and do not call this.
41-
/// \param storage_operation_latency_in_ms_histogram Sink for per-operation
42-
/// latency, recorded when each operation completes.
43-
/// \param storage_operation_count_counter Sink for per-operation counts,
44-
/// recorded when each operation is issued.
45-
/// \param clock Clock used to measure operation latency.
46-
/// \return Either an ObservableStoreClient owning `delegate`, or `delegate`.
47-
std::shared_ptr<StoreClient> MaybeObserve(
48-
std::shared_ptr<StoreClient> delegate,
49-
bool enabled,
50-
ray::observability::MetricInterface &storage_operation_latency_in_ms_histogram,
51-
ray::observability::MetricInterface &storage_operation_count_counter,
52-
ClockInterface &clock);
53-
5430
/// Wraps around a StoreClient instance and observe the metrics.
5531
class ObservableStoreClient : public StoreClient {
5632
public:
57-
/// \param delegate The store client to observe. Taken as a shared_ptr rather
58-
/// than a unique_ptr because the Redis backend has to be reachable
59-
/// concretely as well: AsyncCheckHealth is declared on RedisStoreClient and
60-
/// not on this interface, so GcsServer keeps its own pointer for the periodic
61-
/// health check while handing a copy here. std::unique_ptr converts
62-
/// implicitly, so the in-memory and RocksDB construction sites are unchanged.
63-
/// \param storage_operation_latency_in_ms_histogram Sink for per-operation
64-
/// latency, recorded when each operation completes.
65-
/// \param storage_operation_count_counter Sink for per-operation counts,
66-
/// recorded when each operation is issued.
67-
/// \param clock Clock used to measure operation latency.
6833
explicit ObservableStoreClient(
69-
std::shared_ptr<StoreClient> delegate,
34+
std::unique_ptr<StoreClient> delegate,
7035
ray::observability::MetricInterface &storage_operation_latency_in_ms_histogram,
7136
ray::observability::MetricInterface &storage_operation_count_counter,
7237
ClockInterface &clock)
@@ -114,7 +79,7 @@ class ObservableStoreClient : public StoreClient {
11479
Postable<void(bool)> callback) override;
11580

11681
private:
117-
std::shared_ptr<StoreClient> delegate_;
82+
std::unique_ptr<StoreClient> delegate_;
11883
ray::observability::MetricInterface &storage_operation_latency_in_ms_histogram_;
11984
ray::observability::MetricInterface &storage_operation_count_counter_;
12085
ClockInterface &clock_;

‎src/ray/gcs/store_client/table_name_label.cc‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,18 @@
1414

1515
#include "ray/gcs/store_client/table_name_label.h"
1616

17-
#include <string_view>
17+
#include <string>
1818

1919
#include "src/ray/protobuf/gcs.pb.h"
2020

2121
namespace ray::gcs {
2222

23-
std::string_view NormalizeTableNameLabel(std::string_view table_name) {
23+
std::string NormalizeTableNameLabel(const std::string &table_name) {
2424
rpc::TablePrefix prefix;
2525
if (!rpc::TablePrefix_Parse(table_name, &prefix)) {
26-
return kUnknownTable;
26+
return std::string(kUnknownTable);
2727
}
28-
// Deliberately not `table_name`: that view would dangle once the caller's
29-
// string goes out of scope, and the latency label is read from a completion
30-
// callback. TablePrefix_Name returns a reference into the generated
31-
// descriptor's name storage, which lives for the whole process.
32-
return rpc::TablePrefix_Name(prefix);
28+
return table_name;
3329
}
3430

3531
} // namespace ray::gcs

‎src/ray/gcs/store_client/table_name_label.h‎

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#pragma once
1616

17+
#include <string>
1718
#include <string_view>
1819

1920
namespace ray::gcs {
@@ -33,27 +34,14 @@ inline constexpr std::string_view kJobCounterTable = "JobCounter";
3334
/// Maps a GCS table name onto a closed label domain: the names of the
3435
/// rpc::TablePrefix enum, plus kUnknownTable.
3536
///
36-
/// Every table name a StoreClient sees today is produced by TablePrefix_Name
37-
/// at its definition site -- the six GcsTable subclasses in gcs_table_storage.h
38-
/// and StoreClientInternalKV -- so this normally returns its argument's
39-
/// spelling unchanged. What it adds is that the domain is bounded by
40-
/// construction rather than by convention: StoreClient takes an arbitrary
41-
/// std::string, so without this a future caller could turn a job or actor id
42-
/// into unbounded label cardinality.
43-
///
44-
/// The round trip through the enum is also what makes the result safe to
45-
/// capture. Parsing proves the name is a TablePrefix constant; re-deriving the
46-
/// name from the parsed value returns protobuf's generated static string
47-
/// rather than a view into the caller's argument, so the result outlives the
48-
/// call and can be held by a completion callback without copying a std::string
49-
/// into it.
50-
///
51-
/// Self-maintaining: adding a TablePrefix value makes it a valid label with no
52-
/// change here, and the domain can only grow by editing gcs.proto.
37+
/// Every table name a StoreClient sees today is produced by TablePrefix_Name,
38+
/// so this normally returns its argument unchanged. What it adds is that the
39+
/// domain is bounded by construction rather than by convention: StoreClient
40+
/// takes an arbitrary std::string, so without this a future caller could turn a
41+
/// job or actor id into unbounded label cardinality.
5342
///
5443
/// \param table_name The table name a StoreClient method was called with.
55-
/// \return The matching enum name, or kUnknownTable. Points into static
56-
/// storage, so it outlives the argument.
57-
std::string_view NormalizeTableNameLabel(std::string_view table_name);
44+
/// \return The matching enum name, or kUnknownTable.
45+
std::string NormalizeTableNameLabel(const std::string &table_name);
5846

5947
} // namespace ray::gcs

0 commit comments

Comments
 (0)