Copilot commented on code in PR #13666:
URL: https://github.com/apache/trafficserver/pull/13666#discussion_r4032123534


##########
src/tsutil/Metrics.cc:
##########
@@ -358,6 +386,21 @@ Metrics::Derived::add_source(std::string_view 
derived_name, Metrics::MetricType
   details::DerivativeMetrics::instance().add_source(id, source, op);
 }
 
+void
+Metrics::Derived::remove_source(std::string_view derived_name, 
Metrics::AtomicType *source)
+{
+  auto &instance = Metrics::instance();
+  auto  id       = instance.lookup(derived_name);
+
+  if (id == Metrics::NOT_FOUND) {
+    return;
+  }
+
+  if (details::DerivativeMetrics::instance().remove_source(id, source)) {
+    instance.unlist(id);

Review Comment:
   At line 399, the source-list mutation is protected by `metrics_lock`, but 
`instance.unlist(id)` runs after that lock is released. `add_source()` relists 
an existing name in `_create()` before taking the same lock, so another thread 
can add a source between these operations and then have the metric unlisted 
despite still having a contributor. Keep the source-list mutation and 
relist/unlist transition coordinated so re-adding a source cannot leave it 
hidden.



##########
src/iocore/net/ConnectionTracker.cc:
##########
@@ -485,33 +486,70 @@ ConnectionTracker::Group::Group(DirectionType direction, 
Key const &key, std::st
     std::string _host_metric_name = host_metric_name(key, fqdn, 
_global_config->metric_prefix);
     bool const  has_aggregate     = !_host_metric_name.empty();
 
-    if (has_aggregate && metric_aggregate != AGGREGATE_NONE) {
-      
Metrics::Derived::add_source("proxy.process.http.per_server.current_connection."
 + _host_metric_name,
-                                   Metrics::MetricType::GAUGE, _count_metric, 
Metrics::Derived::Op::SUM);
-      
Metrics::Derived::add_source("proxy.process.http.per_server.total_connection." 
+ _host_metric_name,
-                                   Metrics::MetricType::COUNTER, 
_count_total_metric, Metrics::Derived::Op::SUM);
-      
Metrics::Derived::add_source("proxy.process.http.per_server.blocked_connection."
 + _host_metric_name,
-                                   Metrics::MetricType::COUNTER, 
_blocked_metric, Metrics::Derived::Op::SUM);
+    // A plugin can set an out of range value through the overridable config, 
see
+    // METRIC_AGGREGATE_CONV. Anything unrecognized publishes everything.
+    if (metric_aggregate < AGGREGATE_NONE || metric_aggregate > AGGREGATE_SUM) 
{
+      metric_aggregate = AGGREGATE_GROUP;
+    }
+
+    // See MetricAggregate for the table these three implement. A group with 
no hostname to
+    // aggregate under keeps its own metrics whatever the setting says, since 
suppressing them would
+    // report nothing at all for that upstream.
+    bool const publish_sums  = has_aggregate && (metric_aggregate == 
AGGREGATE_GROUP || metric_aggregate == AGGREGATE_SUM);
+    bool const publish_max   = has_aggregate && metric_aggregate != 
AGGREGATE_NONE;
+    bool const publish_group = !has_aggregate || metric_aggregate == 
AGGREGATE_NONE || metric_aggregate == AGGREGATE_GROUP;
+
+    std::array<std::string, 3> const sum_names{
+      "proxy.process.http.per_server.current_connection." + _host_metric_name,
+      "proxy.process.http.per_server.total_connection." + _host_metric_name,
+      "proxy.process.http.per_server.blocked_connection." + _host_metric_name,
+    };
+    std::array<std::string, 3> const group_names{
+      "proxy.process.http.per_server.current_connection." + _metric_name,
+      "proxy.process.http.per_server.total_connection." + _metric_name,
+      "proxy.process.http.per_server.blocked_connection." + _metric_name,
+    };
+    std::string const max_name = 
"proxy.process.http.per_server.current_connection.max." + _host_metric_name;

Review Comment:
   `current_connection.max.<host>` can collide with a valid MATCH_HOST 
per-group name. A MATCH_HOST mapping for `max.<host>` publishes 
`current_connection.max.<host>`, exactly the name used here for a MATCH_BOTH 
`<host>` aggregate. Because `Derived::add_source` keeps the first operation for 
an existing name, construction order makes this metric either a SUM or MAX, so 
one of the two reported values is wrong. Please choose a collision-free/escaped 
name or otherwise handle this mixed-match case.



##########
src/iocore/net/unit_tests/test_ConnectionTracker.cc:
##########
@@ -61,3 +65,234 @@ TEST_CASE("Connection tracker server match conversion", 
"[libinknet][ConnectionT
     CHECK(match == ConnectionTracker::MATCH_BOTH);
   }
 }
+
+namespace
+{
+
+constexpr std::string_view FQDN{"unit.test.origin"};
+
+// Whether the published store enumerates this name. Deliberately for_each 
rather than lookup(),
+// because enumeration is what traffic_ctl, the JSONRPC record lookup and 
stats_over_http walk, and
+// so is what "published" means to an operator.
+bool
+is_published(std::string_view metric_name)
+{
+  bool found = false;
+
+  ts::Metrics::instance().for_each(
+    [&](std::string_view name, ts::Metrics::MetricType, int64_t) { found |= 
(name == metric_name); });
+
+  return found;
+}
+
+std::string
+group_metric(std::string_view stem, std::string_view addr)
+{
+  return 
std::string("proxy.process.http.per_server.").append(stem).append(".").append(FQDN).append(".").append(addr);
+}
+
+std::string
+host_metric(std::string_view stem)
+{
+  return 
std::string("proxy.process.http.per_server.").append(stem).append(".").append(FQDN);
+}
+
+// One upstream connection, opened and closed, following the same path as 
production: HttpSM
+// reserves and then drops the group into the PoolableSession, and the session 
releases it when the
+// connection closes. Group::release() is what erases the group at a zero 
count, and only that makes
+// the next transaction to the same upstream construct a fresh Group and 
re-evaluate
+// metric_aggregate. TxnState::release() alone decrements without erasing.
+void
+open_and_close_connection(ConnectionTracker::TxnConfig const &txn, IpEndpoint 
const &addr)
+{
+  auto state = ConnectionTracker::obtain_outbound(txn, FQDN, addr);
+
+  REQUIRE(state.is_active());
+  state.reserve();
+
+  auto group = state.drop();
+  group->release();
+}
+
+ConnectionTracker::TxnConfig &
+test_config()
+{
+  // config_init keeps pointers to these for the records callbacks, so they 
must outlive the test.
+  static ConnectionTracker::GlobalConfig global;
+  static ConnectionTracker::TxnConfig    txn;
+  static bool                            initialized = false;
+
+  if (!initialized) {
+    ink_net_init(NET_SYSTEM_MODULE_PUBLIC_VERSION);
+    ConnectionTracker::config_init(&global, &txn, [](const char *, RecDataT, 
RecData, void *) -> int { return REC_ERR_OKAY; });
+    initialized = true;
+  }
+
+  return txn;
+}
+
+} // namespace
+
+TEST_CASE("ConnectionTracker aggregate metric publication", 
"[libinknet][ConnectionTracker]")
+{
+  auto &txn = test_config();
+
+  txn.metric_enabled = 1;
+  txn.server_match   = ConnectionTracker::MATCH_BOTH;
+
+  IpEndpoint addr;
+  REQUIRE(ats_ip_pton("10.9.8.7:443", &addr) == 0);
+
+  const std::string current_group = group_metric("current_connection", 
"10.9.8.7:443");
+  const std::string total_group   = group_metric("total_connection", 
"10.9.8.7:443");
+  const std::string blocked_group = group_metric("blocked_connection", 
"10.9.8.7:443");
+
+  SECTION("AGGREGATE_NONE publishes the per group metrics and no aggregate")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_NONE;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(current_group));
+    CHECK(is_published(total_group));
+    CHECK(is_published(blocked_group));
+    CHECK_FALSE(is_published(host_metric("current_connection.max")));
+  }
+
+  SECTION("AGGREGATE_GROUP publishes the per group metrics, the sums and the 
max")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_GROUP;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(current_group));
+    CHECK(is_published(host_metric("current_connection")));
+    CHECK(is_published(host_metric("total_connection")));
+    CHECK(is_published(host_metric("blocked_connection")));
+    CHECK(is_published(host_metric("current_connection.max")));
+  }
+
+  SECTION("AGGREGATE_MAX publishes the max and nothing else")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(host_metric("current_connection.max")));
+
+    CHECK_FALSE(is_published(host_metric("current_connection")));
+    CHECK_FALSE(is_published(host_metric("total_connection")));
+    CHECK_FALSE(is_published(host_metric("blocked_connection")));
+    CHECK_FALSE(is_published(current_group));
+    CHECK_FALSE(is_published(total_group));
+    CHECK_FALSE(is_published(blocked_group));
+  }
+
+  SECTION("AGGREGATE_SUM publishes the sums and the max, but not the per group 
metrics")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_SUM;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(host_metric("current_connection")));
+    CHECK(is_published(host_metric("total_connection")));
+    CHECK(is_published(host_metric("blocked_connection")));
+    CHECK(is_published(host_metric("current_connection.max")));
+
+    CHECK_FALSE(is_published(current_group));
+    CHECK_FALSE(is_published(total_group));
+    CHECK_FALSE(is_published(blocked_group));
+  }
+
+  SECTION("switching to AGGREGATE_MAX retracts already published per group 
metrics")
+  {
+    // The production sequence: run for a while with the per group metrics 
published, then change
+    // the setting. Without a retraction the first set of names is published 
forever.
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_NONE;
+    open_and_close_connection(txn, addr);
+    REQUIRE(is_published(current_group));
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, addr);
+
+    CHECK_FALSE(is_published(current_group));
+    CHECK_FALSE(is_published(total_group));
+    CHECK_FALSE(is_published(blocked_group));
+    CHECK(is_published(host_metric("current_connection.max")));
+  }
+
+  SECTION("switching from AGGREGATE_SUM to AGGREGATE_MAX retracts the sums")
+  {
+    // The sums are aggregates rather than per group names, but they are 
published the same way and
+    // so need withdrawing the same way when the setting stops asking for them.
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_SUM;
+    open_and_close_connection(txn, addr);
+    REQUIRE(is_published(host_metric("current_connection")));
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, addr);
+
+    CHECK_FALSE(is_published(host_metric("current_connection")));
+    CHECK_FALSE(is_published(host_metric("total_connection")));
+    CHECK_FALSE(is_published(host_metric("blocked_connection")));
+    CHECK(is_published(host_metric("current_connection.max")));
+  }
+
+  SECTION("switching from AGGREGATE_MAX to AGGREGATE_SUM republishes the sums")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, addr);
+    REQUIRE_FALSE(is_published(host_metric("total_connection")));
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_SUM;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(host_metric("current_connection")));
+    CHECK(is_published(host_metric("total_connection")));
+    CHECK(is_published(host_metric("blocked_connection")));
+  }
+
+  SECTION("switching back to AGGREGATE_GROUP republishes the per group 
metrics")
+  {
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, addr);
+    REQUIRE_FALSE(is_published(current_group));
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_GROUP;
+    open_and_close_connection(txn, addr);
+
+    CHECK(is_published(current_group));
+    CHECK(is_published(host_metric("current_connection")));
+  }
+
+  SECTION("one hostname's groups do not unlist each other's aggregate")
+  {
+    // metric_aggregate is overridable, so two mappings to one hostname can 
disagree. Both groups
+    // share the hostname's aggregate names, so a group that does not want 
them must stop
+    // contributing rather than unlist a name the other one is still 
publishing.
+    IpEndpoint other;
+    REQUIRE(ats_ip_pton("10.9.8.5:443", &other) == 0);
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_SUM;
+    open_and_close_connection(txn, addr);
+    REQUIRE(is_published(host_metric("current_connection")));
+
+    txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
+    open_and_close_connection(txn, other);
+
+    CHECK(is_published(host_metric("current_connection")));
+    CHECK(is_published(host_metric("current_connection.max")));
+  }

Review Comment:
   This new sharing test covers two MATCH_BOTH groups only; it does not 
exercise the source-ownership guard for the real name collision between a 
MATCH_HOST group's own `stem.<fqdn>` metric and a MATCH_BOTH aggregate. Keep a 
MATCH_HOST group alive while constructing a MATCH_BOTH group at 
`AGGREGATE_NONE` and `AGGREGATE_MAX`, then assert the three shared names remain 
listed, so a future name-only unlist cannot hide metrics still owned by the 
host group.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to