This is an automated email from the ASF dual-hosted git repository.

serrislew pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new bc0bca7be4 Fix enabling per-server metrics that disables the outbound 
keep-alive minimum  (#13480)
bc0bca7be4 is described below

commit bc0bca7be4342d0a03c6481d290dab3a143671a1
Author: Serris Santos <[email protected]>
AuthorDate: Tue Aug 4 15:20:45 2026 -0700

    Fix enabling per-server metrics that disables the outbound keep-alive 
minimum  (#13480)
    
    * Fix enabling per-server metrics that disables the outbound keep-alive 
minimum
    
    * int to auto from copilot
    
    * Add autest
---
 include/iocore/net/ConnectionTracker.h             |  10 +-
 src/iocore/net/ConnectionTracker.cc                |  32 ++----
 .../per_server_metric_enabled.replay.yaml          |  53 ++++++++++
 .../per_server_metric_enabled.test.py              | 113 +++++++++++++++++++++
 4 files changed, 179 insertions(+), 29 deletions(-)

diff --git a/include/iocore/net/ConnectionTracker.h 
b/include/iocore/net/ConnectionTracker.h
index 360fda7b64..ae3691fdbe 100644
--- a/include/iocore/net/ConnectionTracker.h
+++ b/include/iocore/net/ConnectionTracker.h
@@ -449,13 +449,13 @@ inline int
 ConnectionTracker::TxnState::reserve()
 {
   _reserved_p = true;
-  // If metric enabled, use metric as count
+  // @a _count is always the authoritative count; the metrics, if enabled, 
only mirror it.
+  auto count = ++_g->_count;
   if (_g->_count_metric != nullptr) {
     ts::Metrics::Gauge::increment(_g->_count_metric);
     ts::Metrics::Counter::increment(_g->_count_total_metric);
-    return _g->_count_metric->load();
   }
-  return ++_g->_count;
+  return count;
 }
 
 inline void
@@ -463,11 +463,9 @@ ConnectionTracker::TxnState::release()
 {
   if (_reserved_p) {
     _reserved_p = false;
-    // If metric enabled, use metric as count
+    --_g->_count;
     if (_g->_count_metric != nullptr) {
       ts::Metrics::Gauge::decrement(_g->_count_metric);
-    } else {
-      --_g->_count;
     }
   }
 }
diff --git a/src/iocore/net/ConnectionTracker.cc 
b/src/iocore/net/ConnectionTracker.cc
index b7ea956723..45ce7e60f0 100644
--- a/src/iocore/net/ConnectionTracker.cc
+++ b/src/iocore/net/ConnectionTracker.cc
@@ -187,8 +187,8 @@ 
Groups_To_JSON(std::vector<std::shared_ptr<ConnectionTracker::Group const>> cons
   static const std::string_view trailer{" \n]}"};
 
   static const auto printer = [](swoc::BufferWriter &w, 
ConnectionTracker::Group const *g) -> swoc::BufferWriter & {
-    w.print(item_fmt, g->_match_type, g->_addr, g->_fqdn, g->_count_metric != 
nullptr ? g->_count_metric->load() : g->_count.load(),
-            g->_count_max.load(), g->_blocked.load(), 
g->get_last_alert_epoch_time());
+    w.print(item_fmt, g->_match_type, g->_addr, g->_fqdn, g->_count.load(), 
g->_count_max.load(), g->_blocked.load(),
+            g->get_last_alert_epoch_time());
     return w;
   };
 
@@ -522,26 +522,13 @@ ConnectionTracker::Group::should_alert(std::time_t *lat)
 void
 ConnectionTracker::Group::release()
 {
-  // If metric enabled, use metric as count
-  if (_count_metric != nullptr) {
-    if (_count_metric->load() > 0) {
+  // @a _count is always the authoritative count; the metric, if enabled, only 
mirrors it.
+  if (_count > 0) {
+    auto count = --_count;
+    if (_count_metric != nullptr) {
       ts::Metrics::Gauge::decrement(_count_metric);
-      if (_count_metric->load() == 0) {
-        TableSingleton             &table = _direction == 
DirectionType::INBOUND ? _inbound_table : _outbound_table;
-        std::lock_guard<std::mutex> lock(table._mutex); // Table lock
-        if (_count_metric->load() > 0) {
-          // Someone else grabbed the Group between our last check and taking 
the
-          // lock.
-          return;
-        }
-        table._table.erase(_key);
-      }
-    } else {
-      // A bit dubious, as there's no guarantee it's still negative, but even 
that would be interesting to know.
-      Error("Number of tracked connections should be greater than or equal to 
zero: %" PRId64, _count_metric->load());
     }
-  } else if (_count > 0) {
-    if (--_count == 0) {
+    if (count == 0) {
       TableSingleton             &table = _direction == DirectionType::INBOUND 
? _inbound_table : _outbound_table;
       std::lock_guard<std::mutex> lock(table._mutex); // Table lock
       if (_count > 0) {
@@ -553,7 +540,7 @@ ConnectionTracker::Group::release()
     }
   } else {
     // A bit dubious, as there's no guarantee it's still negative, but even 
that would be interesting to know.
-    Error("Number of tracked connections should be greater than or equal to 
zero: %u", _count.load());
+    Error("Number of tracked connections should be greater than or equal to 
zero: %d", _count.load());
   }
 }
 
@@ -615,8 +602,7 @@ ConnectionTracker::dump_outbound(FILE *f)
 
     for (std::shared_ptr<Group const> g : groups) {
       swoc::LocalBufferWriter<128> w;
-      w.print("{:7} | {:5} | {:24} | {:33} | {:8} |\n", (g->_count_metric != 
nullptr ? g->_count_metric->load() : g->_count.load()),
-              g->_blocked.load(), g->_addr, g->_hash, g->_match_type);
+      w.print("{:7} | {:5} | {:24} | {:33} | {:8} |\n", g->_count.load(), 
g->_blocked.load(), g->_addr, g->_hash, g->_match_type);
       fwrite(w.data(), w.size(), 1, f);
     }
 
diff --git 
a/tests/gold_tests/origin_connection/per_server_metric_enabled.replay.yaml 
b/tests/gold_tests/origin_connection/per_server_metric_enabled.replay.yaml
new file mode 100644
index 0000000000..9889af26ed
--- /dev/null
+++ b/tests/gold_tests/origin_connection/per_server_metric_enabled.replay.yaml
@@ -0,0 +1,53 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+# Test Notes:
+# A single transaction which leaves one keep-alive origin connection pooled
+# behind it. The test driving this replay file verifies that the outbound
+# connection tracker keeps an accurate connection count while
+# proxy.config.http.per_server.connection.metric_enabled is set, so that the
+# pooled connection is reaped once the keep alive timeout expires.
+
+meta:
+  version: "1.0"
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /some/path/first
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Length, 0 ]
+        - [ uuid, first-request ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+        - [ X-Response, first-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, {value: 'first-response', as: equal } ]
diff --git 
a/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py 
b/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py
new file mode 100644
index 0000000000..510265c3df
--- /dev/null
+++ b/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py
@@ -0,0 +1,113 @@
+'''
+Verify per_server connection tracking stays accurate when
+proxy.config.http.per_server.connection.metric_enabled is set.
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = __doc__
+
+Test.SkipIf(Condition.CurlUsingUnixDomainSocket())
+
+
+class PerServerMetricEnabledTest:
+    """Verify that enabling the per_server metrics does not break the group 
connection count.
+
+    Enabling proxy.config.http.per_server.connection.metric_enabled used to
+    make the metric the authoritative connection count, leaving the group's
+    internal counter at zero. That made every pooled origin session look like
+    it was at or below proxy.config.http.per_server.connection.min, so
+    keep-alive origin connections were never reaped on inactivity timeout.
+    """
+
+    _replay_file: str = 'per_server_metric_enabled.replay.yaml'
+    _keep_alive_timeout: int = 2
+
+    def __init__(self) -> None:
+        """Configure the test processes in preparation for the TestRun."""
+        self._configure_server()
+        self._configure_trafficserver()
+
+    def _configure_server(self) -> None:
+        """Configure the origin server to be used in the test."""
+        self._server = Test.MakeVerifierServerProcess('metric_enabled_server', 
self._replay_file)
+
+    def _configure_trafficserver(self) -> None:
+        """Configure Traffic Server to be used in the test."""
+        self._ts = Test.MakeATSProcess("ts_metric_enabled")
+        self._ts.Disk.remap_config.AddLine(f'map / 
http://127.0.0.1:{self._server.Variables.http_port}')
+        self._ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'http_ss|conn_track',
+                'proxy.config.http.per_server.connection.metric_enabled': 1,
+                'proxy.config.http.per_server.connection.metric_prefix': 'bar',
+                'proxy.config.http.per_server.connection.match': 'port',
+                # No minimum number of keep alive origin connections: the 
pooled
+                # connection should be closed once it times out.
+                'proxy.config.http.per_server.connection.min': 0,
+                'proxy.config.http.keep_alive_no_activity_timeout_out': 
self._keep_alive_timeout,
+                'proxy.config.http.server_session_sharing.pool': 'global',
+            })
+        # The connection count should never be decremented below zero.
+        self._ts.Disk.diags_log.Content += Testers.ExcludesExpression(
+            'Number of tracked connections should be greater than or equal to 
zero',
+            'Verify the group connection count is not double decremented.')
+
+    def _test_connection_is_reaped(self) -> None:
+        """Verify the idle origin connection is closed once it times out."""
+        tr = Test.AddTestRun("Verify the idle keep-alive origin connection is 
reaped")
+        tr.Processes.Default.Command = (
+            f'sleep {self._keep_alive_timeout * 3}; '
+            'traffic_ctl metric get 
proxy.process.http.current_server_connections; '
+            'traffic_ctl metric match per_server')
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Env = self._ts.Env
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            'proxy.process.http.current_server_connections 0',
+            'The idle origin connection should have been closed by the 
keep-alive timeout.')
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            
f'per_server.current_connection.bar.127.0.0.1:{self._server.Variables.http_port}
 0',
+            'The per_server connection gauge should have been decremented back 
to zero.')
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            
f'per_server.total_connection.bar.127.0.0.1:{self._server.Variables.http_port} 
1',
+            'A single origin connection should have been tracked.')
+
+    def _test_tracker_info(self) -> None:
+        """Verify the JSONRPC connection tracker report agrees with the 
metrics."""
+        tr = Test.AddTestRun("Verify the connection tracker report")
+        tr.Processes.Default.Command = "traffic_ctl rpc invoke 
get_connection_tracker_info -p 'table: outbound' -f json"
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Env = self._ts.Env
+        # Once the connection is released the group count drops to zero and the
+        # group is removed from the table, so either the table is empty or the
+        # remaining group reports no current connections.
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            r'"(count|current)":\s*"?0"?', 'The tracker should report no 
current outbound connections.')
+
+    def run(self) -> None:
+        """Configure the TestRuns."""
+        tr = Test.AddTestRun('Perform a transaction that leaves a pooled 
origin connection')
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+
+        tr.AddVerifierClientProcess('metric_enabled_client', 
self._replay_file, http_ports=[self._ts.Variables.port])
+
+        self._test_connection_is_reaped()
+        self._test_tracker_info()
+
+
+PerServerMetricEnabledTest().run()

Reply via email to