You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by cm...@apache.org on 2026/06/26 00:31:17 UTC
(trafficserver) branch 10.2.x updated (1a07ca9cdf -> bf218ea66a)
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a change to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
from 1a07ca9cdf Emit type-default for custom log fields with no transaction (#13243)
new 3a5904aa19 header_rewrite: count operators and conditions run (#13286)
new 0246aec026 Fix set-status crash inside if/endif at remap time (#13052)
new 24c34a1bb1 Fix hdrHeap/hdrStrHeap allocator inuse metric underflow (#13218)
new fefa7fe7bd Fix incorrect errno short names on FreeBSD/macOS in bwf::Errno (#13240)
new 4cde4ed3be Add S3-FIFO RAM cache eviction algorithm (ram_cache.algorithm = 2) (#13255)
new 447dd6dcd4 header_rewrite: Improve URL Error messages (#13261)
new af98f1ba15 Use ls-hpack's fast Huffman decoder for HPACK/QPACK strings (#13259)
new 197ecbdeb0 logging: add a marshalled-bytes counter (#13277)
new 2b0ccc94ad Cripts: make URL::Query copyable via deep-copy of _state (#13269)
new de3841dab7 Test TLS async without ENGINE (#13264)
new 641d15d07a Fix bounds check in CacheVC::scanObject (#13263)
new c958a7dd30 Enable probes in Fedora C++20 CI (#13280)
new 0fd5a7ff3c Fix connect attempt retries (#13102)
new 39bcc86588 net: count application bytes in read_bytes for TLS (#13282)
new 4fbb38df68 compress: count uncompressed input bytes (#13287)
new ddc283c280 TLS: count handshake signatures by key type (#13289)
new e2fb345993 ProxyProtocol: free pp_info heap on NetVConnection recycle (#13293)
new 5081848180 Fix pending HostDB DNS queue removal race (#13294)
new b02d0640f2 traffic_crashlog: fix false-positive crash logs on clean shutdown (#13296)
new b8a6099c30 tools/clang-format.sh: cache clang-format in the common git dir (#13302)
new e8fe4526bb header_rewrite: Fix a leak and truncation in set-body-from (#13303)
new 4ee1a8b9dd tests: give cripts ATS startup a longer readiness window (#13304)
new 245a0a78d8 ci: modernize the coverage helper script (#13305)
new 7d95f45b63 tests: add TLS gold tests (#13306)
new 1020680f40 dns: destruct HostEnt on free to fix SRV vector leak (#13307)
new 70a2448244 Fix server entry cleanup after request tunnel setup (#13295)
new ab3a1ccbc6 Introduce Clang Thread Safety Analysis, and apply it to two subsystems (#13310)
new a1aa2ed7dd Fix redirected cache write without write VC (#13309)
new 83bb02235c TLS: Fix memory leaks in cert load and OCSP stapling (#13318)
new 3df7f9e3d9 pqsi-pqsp.test.py: address log order flakiness (#13321)
new ff3ec35eba slice_prefetch.test.py: address cache.log flakiness (#13322)
new 5336b5c43d HttpSM.cc: fix cache read end milestone order (#13325)
new bf218ea66a Add per-plugin workload counters (#13278)
The 33 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
AGENTS.md | 7 +
CMakeLists.txt | 31 ++
CMakePresets.json | 11 +-
NOTICE | 9 +
ci/coverage | 178 +++++----
doc/admin-guide/files/records.yaml.en.rst | 84 +++-
.../monitoring/statistics/core/network-io.en.rst | 19 +
doc/admin-guide/storage/index.en.rst | 14 +-
.../api/functions/TSHttpOverridableConfig.en.rst | 269 ++++++-------
.../api/types/TSOverridableConfigKey.en.rst | 1 +
doc/release-notes/upgrading.en.rst | 5 +
include/cripts/Configs.hpp | 2 +
include/cripts/Urls.hpp | 38 ++
include/iocore/cache/Cache.h | 5 +-
include/iocore/eventsystem/Thread.h | 1 +
include/iocore/hostdb/HostDBProcessor.h | 2 +-
include/iocore/net/ProxyProtocol.h | 19 +
include/proxy/PluginThreadContext.h | 61 +++
include/proxy/PluginVC.h | 6 +
include/proxy/hdrs/HTTP.h | 4 +-
include/proxy/hdrs/HuffmanCodec.h | 10 +
include/proxy/http/HttpConfig.h | 10 +-
include/proxy/http/HttpTransact.h | 5 +-
include/proxy/http/OverridableConfigDefs.h | 3 +-
include/proxy/http/remap/PluginDso.h | 11 +-
include/proxy/logging/LogConfig.h | 1 +
include/ts/apidefs.h.in | 1 +
include/tsutil/Metrics.h | 18 +-
include/tsutil/TsMutex.h | 100 +++++
include/tsutil/TsSharedMutex.h | 54 ++-
include/tsutil/ts_thread_safety.h | 97 +++++
lib/ls-hpack/README.md | 16 +-
lib/ls-hpack/lshpack.cc | 190 ++++++++-
lib/ls-hpack/lshpack.h | 6 +
lib/swoc/src/bw_format.cc | 155 ++++----
lib/swoc/unit_tests/test_bw_format.cc | 35 ++
plugins/compress/compress.cc | 20 +-
plugins/header_rewrite/condition.h | 4 +
plugins/header_rewrite/conditions.cc | 58 ++-
plugins/header_rewrite/conditions.h | 2 +
plugins/header_rewrite/header_rewrite.cc | 4 +
plugins/header_rewrite/operator.h | 3 +
plugins/header_rewrite/operators.cc | 31 +-
plugins/header_rewrite/resources.cc | 1 +
plugins/header_rewrite/resources.h | 1 +
plugins/header_rewrite/ruleset.cc | 6 +-
plugins/header_rewrite/statement.cc | 28 +-
plugins/header_rewrite/statement.h | 47 ++-
plugins/header_rewrite/value.cc | 6 +
src/api/InkContInternal.cc | 9 +-
src/cripts/tests/query_test.cc | 15 +
src/iocore/cache/CMakeLists.txt | 1 +
src/iocore/cache/Cache.cc | 70 ++--
src/iocore/cache/CacheProcessor.cc | 7 +
src/iocore/cache/CacheTest.cc | 28 +-
src/iocore/cache/CacheVC.cc | 14 +-
src/iocore/cache/P_CacheInternal.h | 4 +
src/iocore/cache/P_RamCache.h | 1 +
src/iocore/cache/RamCacheS3FIFO.cc | 437 +++++++++++++++++++++
src/iocore/dns/DNS.cc | 2 +-
src/iocore/eventsystem/ProxyAllocator.cc | 8 +
src/iocore/hostdb/HostDB.cc | 19 +-
src/iocore/net/OCSPStapling.cc | 17 +-
src/iocore/net/SSLNetVConnection.cc | 15 +-
src/iocore/net/SSLSessionCache.cc | 21 +-
src/iocore/net/SSLSessionCache.h | 8 +-
src/iocore/net/SSLStats.cc | 4 +
src/iocore/net/SSLStats.h | 4 +
src/iocore/net/SSLUtils.cc | 63 +--
src/iocore/net/TLSBasicSupport.cc | 22 ++
src/iocore/net/UnixNetVConnection.cc | 3 +
src/iocore/net/unit_tests/test_ProxyProtocol.cc | 50 +++
src/proxy/CMakeLists.txt | 1 +
src/proxy/Plugin.cc | 38 ++
src/proxy/PluginThreadContext.cc | 65 +++
src/proxy/PluginVC.cc | 16 +
src/proxy/hdrs/CMakeLists.txt | 2 +
src/proxy/hdrs/HTTP.cc | 4 +-
src/proxy/hdrs/HuffmanCodec.cc | 4 +-
src/proxy/hdrs/unit_tests/test_HdrHeap.cc | 101 +++++
src/proxy/hdrs/unit_tests/test_Huffmancode.cc | 137 +++++++
src/proxy/http/HttpConfig.cc | 40 +-
src/proxy/http/HttpSM.cc | 88 +++--
src/proxy/http/HttpTransact.cc | 199 ++++++----
src/proxy/http/HttpTunnel.cc | 2 +
src/proxy/http/remap/PluginDso.cc | 7 +
src/proxy/http/remap/RemapPlugins.cc | 2 +
src/proxy/logging/LogConfig.cc | 1 +
src/proxy/logging/LogObject.cc | 2 +
src/proxy/unit_tests/stub.cc | 2 -
src/records/RecordsConfig.cc | 19 +-
src/traffic_crashlog/traffic_crashlog.cc | 21 +-
src/traffic_server/Crash.cc | 9 +
src/tsutil/CMakeLists.txt | 15 +
src/tsutil/Metrics.cc | 16 +-
src/tsutil/unit_tests/test_thread_safety.cc | 182 +++++++++
tests/gold_tests/autest-site/ats_replay.test.ext | 30 +-
.../gold_tests/autest-site/trafficserver.test.ext | 3 +
.../autest-site/verifier_client.test.ext | 15 +-
.../replay/connect_down_policy_4.replay.yaml | 2 +-
tests/gold_tests/cripts/cripts.test.py | 44 ++-
tests/gold_tests/cripts/files/query_copy.cript | 64 +++
tests/gold_tests/cripts/gold/basic_cript.gold | 1 +
tests/gold_tests/cripts/gold/certs_cript.gold | 1 +
tests/gold_tests/dns/connect_attempts.test.py | 5 +
tests/gold_tests/dns/dns_host_down.test.py | 14 +
.../connect_attempts_rr_max_retries_error_log.gold | 4 +-
.../connect_attempts_rr_retries_error_log.gold | 7 +-
...nect_attempts_single_max_retries_error_log.gold | 9 +
.../connect_attempts_rr_max_retries.replay.yaml | 2 +-
.../connect_attempts_rr_no_retry.replay.yaml | 2 +-
.../replay/connect_attempts_rr_retries.replay.yaml | 59 +--
...onnect_attempts_single_max_retries.replay.yaml} | 79 ++--
tests/gold_tests/logging/gold/pqsi-pqsp.gold | 2 +-
tests/gold_tests/logging/pqsi-pqsp.test.py | 6 +-
.../pluginTest/escalate/escalate.test.py | 10 +-
.../header_rewrite_bundle.replay.yaml | 37 ++
...rule_cond_method.conf => set_status_in_if.conf} | 17 +-
tests/gold_tests/pluginTest/lua/metrics.sh | 3 +-
.../per_plugin_metrics/per_plugin_metrics.test.py | 111 ++++++
.../per_plugin_metrics/rules/global.conf | 13 +-
.../pluginTest/regex_revalidate/metrics.sh | 3 +-
.../pluginTest/regex_revalidate/metrics_miss.sh | 3 +-
.../pluginTest/slice/gold/slice_prefetch.gold | 27 --
.../pluginTest/slice/slice_prefetch.test.py | 33 +-
.../shutdown/crashlog_no_false_positive.test.py | 73 ++++
..._engine.test.py => tls_async_handshake.test.py} | 43 +-
tests/gold_tests/tls/tls_flow_control.test.py | 116 ++++++
tests/gold_tests/tls/tls_flow_control_client.py | 100 +++++
.../gold_tests/tls/tls_origin_open_failed.test.py | 78 ++++
tests/gold_tests/tls/tls_origin_post_abort.test.py | 114 ++++++
tests/gold_tests/tls/tls_post_abort_client.py | 127 ++++++
tests/gold_tests/tls/tls_post_abort_origin.py | 92 +++++
tests/gold_tests/tls/tls_record_size.test.py | 117 ++++++
tests/gold_tests/tls/tls_record_size_client.py | 155 ++++++++
tests/gold_tests/tls/tls_reload_under_load.test.py | 100 +++++
.../gold_tests/tls/tls_reload_under_load_client.py | 183 +++++++++
tests/gold_tests/tls/tls_renegotiation.test.py | 124 ++++++
.../tls/tls_renegotiation_allowed.test.py | 145 +++++++
.../tls/tls_renegotiation_allowed_client.py | 132 +++++++
tests/gold_tests/tls/tls_renegotiation_client.py | 67 ++++
tests/tools/plugins/CMakeLists.txt | 6 +-
tests/tools/plugins/async_engine.c | 325 ---------------
tests/tools/plugins/async_handshake.cc | 182 +++++++++
tools/benchmark/CMakeLists.txt | 4 +
tools/benchmark/benchmark_HuffmanDecode.cc | 142 +++++++
tools/clang-format.sh | 14 +-
147 files changed, 5369 insertions(+), 1160 deletions(-)
create mode 100644 include/proxy/PluginThreadContext.h
create mode 100644 include/tsutil/TsMutex.h
create mode 100644 include/tsutil/ts_thread_safety.h
create mode 100644 src/iocore/cache/RamCacheS3FIFO.cc
create mode 100644 src/proxy/PluginThreadContext.cc
create mode 100644 src/tsutil/unit_tests/test_thread_safety.cc
create mode 100644 tests/gold_tests/cripts/files/query_copy.cript
create mode 100644 tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold
copy tests/gold_tests/dns/replay/{connect_attempts_rr_retries.replay.yaml => connect_attempts_single_max_retries.replay.yaml} (63%)
copy tests/gold_tests/pluginTest/header_rewrite/rules/{rule_cond_method.conf => set_status_in_if.conf} (70%)
create mode 100644 tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py
copy plugins/experimental/magick/verify.sh => tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf (74%)
mode change 100755 => 100644
delete mode 100644 tests/gold_tests/pluginTest/slice/gold/slice_prefetch.gold
create mode 100644 tests/gold_tests/shutdown/crashlog_no_false_positive.test.py
rename tests/gold_tests/tls/{tls_engine.test.py => tls_async_handshake.test.py} (66%)
create mode 100644 tests/gold_tests/tls/tls_flow_control.test.py
create mode 100644 tests/gold_tests/tls/tls_flow_control_client.py
create mode 100644 tests/gold_tests/tls/tls_origin_open_failed.test.py
create mode 100644 tests/gold_tests/tls/tls_origin_post_abort.test.py
create mode 100644 tests/gold_tests/tls/tls_post_abort_client.py
create mode 100644 tests/gold_tests/tls/tls_post_abort_origin.py
create mode 100644 tests/gold_tests/tls/tls_record_size.test.py
create mode 100644 tests/gold_tests/tls/tls_record_size_client.py
create mode 100644 tests/gold_tests/tls/tls_reload_under_load.test.py
create mode 100644 tests/gold_tests/tls/tls_reload_under_load_client.py
create mode 100644 tests/gold_tests/tls/tls_renegotiation.test.py
create mode 100644 tests/gold_tests/tls/tls_renegotiation_allowed.test.py
create mode 100644 tests/gold_tests/tls/tls_renegotiation_allowed_client.py
create mode 100644 tests/gold_tests/tls/tls_renegotiation_client.py
delete mode 100644 tests/tools/plugins/async_engine.c
create mode 100644 tests/tools/plugins/async_handshake.cc
create mode 100644 tools/benchmark/benchmark_HuffmanDecode.cc
(trafficserver) 31/33: slice_prefetch.test.py: address cache.log flakiness (#13322)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit ff3ec35ebaf7fad34f1890d7f484e61622baada5
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Thu Jun 25 09:44:00 2026 -0500
slice_prefetch.test.py: address cache.log flakiness (#13322)
Background slice prefetch requests can log around the client
transactions. A run can therefore contain the expected cache records
while still failing an ordered cache.log gold comparison.
This replaces the full-file gold comparison with per-entry log
assertions that verify the expected cache and prefetch records
independently of ordering, then removes the unused gold file.
Fixes: #13311
(cherry picked from commit c179b6b9bbc0853d2a184296ec38a37b5032e4c4)
---
.../pluginTest/slice/gold/slice_prefetch.gold | 27 ------------------
.../pluginTest/slice/slice_prefetch.test.py | 33 +++++++++++++++++++++-
2 files changed, 32 insertions(+), 28 deletions(-)
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_prefetch.gold b/tests/gold_tests/pluginTest/slice/gold/slice_prefetch.gold
deleted file mode 100644
index 60a398b009..0000000000
--- a/tests/gold_tests/pluginTest/slice/gold/slice_prefetch.gold
+++ /dev/null
@@ -1,27 +0,0 @@
-bytes 0-6/18 miss
-bytes ``/18 miss
-bytes ``/18 miss
-bytes 14-17/18 hit-fresh
-- miss, none
-bytes 0-6/18 hit-fresh
-bytes 7-13/18 hit-fresh
-bytes 14-17/18 hit-fresh
-- hit-fresh, none
-bytes 0-6/18 hit-stale
-bytes ``/18 hit-stale
-bytes ``/18 hit-stale
-bytes 14-17/18 hit-fresh
-- hit-stale, none
-bytes 0-6/18 hit-fresh
-bytes 7-13/18 hit-fresh
-bytes 14-17/18 hit-fresh
-bytes 0-17/18 hit-fresh, none
-bytes 0-4/18 miss
-bytes ``/18 miss
-bytes ``/18 miss
-bytes ``/18 miss
-bytes 10-14/18 hit-fresh
-bytes 15-17/18 hit-fresh
-bytes 5-16/18 miss, none
-bytes 0-6/18 hit-fresh
-*/18 hit-fresh, none
diff --git a/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py b/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py
index baf9cbb1ec..14767b41c0 100644
--- a/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py
+++ b/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py
@@ -16,6 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
+import re
Test.Summary = '''
slice plugin prefetch feature test
@@ -180,4 +181,34 @@ test_run = Test.AddAwaitFileContainsTestRun(
cache_file,
r'\*/18 hit-fresh, none$',
)
-ts.Disk.File(cache_file).Content = "gold/slice_prefetch.gold"
+cache_log = ts.Disk.File(cache_file)
+expected_cache_entries = [
+ "bytes 0-6/18 miss",
+ "bytes 7-13/18 miss",
+ "bytes 14-17/18 miss",
+ "bytes 14-17/18 hit-fresh",
+ "- miss, none",
+ "bytes 0-6/18 hit-fresh",
+ "bytes 7-13/18 hit-fresh",
+ "bytes 14-17/18 hit-fresh",
+ "- hit-fresh, none",
+ "bytes 0-6/18 hit-stale",
+ "bytes 7-13/18 hit-stale",
+ "bytes 14-17/18 hit-stale",
+ "- hit-stale, none",
+ "bytes 0-17/18 hit-fresh, none",
+ "bytes 0-4/18 miss",
+ "bytes 5-9/18 miss",
+ "bytes 10-14/18 miss",
+ "bytes 15-17/18 miss",
+ "bytes 10-14/18 hit-fresh",
+ "bytes 15-17/18 hit-fresh",
+ "bytes 5-16/18 miss, none",
+ "*/18 hit-fresh, none",
+]
+for index, entry in enumerate(expected_cache_entries):
+ tester = Testers.ContainsExpression(f'(?m)^{re.escape(entry)}$', f'Verify cache log contains: {entry}')
+ if index == 0:
+ cache_log.Content = tester
+ else:
+ cache_log.Content += tester
(trafficserver) 19/33: traffic_crashlog: fix false-positive crash logs on clean shutdown (#13296)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit b02d0640f26f032d9ee7c83f71446d0d8fb215f7
Author: Masaori Koshiba <ma...@apache.org>
AuthorDate: Fri Jun 19 07:23:52 2026 +0900
traffic_crashlog: fix false-positive crash logs on clean shutdown (#13296)
* traffic_crashlog: fix false-positive crash logs on clean shutdown
The crash log helper is parked with SIGSTOP and armed with
PR_SET_PDEATHSIG=SIGCONT, so the kernel resumes it whenever
traffic_server exits, not only on a crash. It used getppid() to tell a
crash from a plain exit, but that is racy: PR_SET_PDEATHSIG fires on the
forking thread's death, which can occur while the parent pid is still
alive, so a normal SIGTERM shutdown could emit an empty crash log.
Replace the getppid() guess with a socket handshake: a real crash writes
the signal number to the helper, while any other exit closes the socket
so the helper reads EOF and exits quietly. FD_CLOEXEC keeps that EOF
reliable against later fork+exec children.
* Address Copilot's comment
(cherry picked from commit 3c000b304dea10dac2b7fb66c08e09a61c72e8dc)
---
src/traffic_crashlog/traffic_crashlog.cc | 21 +++++--
src/traffic_server/Crash.cc | 9 +++
.../shutdown/crashlog_no_false_positive.test.py | 73 ++++++++++++++++++++++
3 files changed, 97 insertions(+), 6 deletions(-)
diff --git a/src/traffic_crashlog/traffic_crashlog.cc b/src/traffic_crashlog/traffic_crashlog.cc
index e31c0d40bf..dd77893ffa 100644
--- a/src/traffic_crashlog/traffic_crashlog.cc
+++ b/src/traffic_crashlog/traffic_crashlog.cc
@@ -201,7 +201,6 @@ main(int /* argc ATS_UNUSED */, const char **argv)
FILE *fp;
char *logname;
crashlog_target target;
- pid_t parent = getppid();
DiagsPtr::set(new Diags("traffic_crashlog", "" /* tags */, "" /* actions */, new BaseLogFile("stderr")));
@@ -213,12 +212,22 @@ main(int /* argc ATS_UNUSED */, const char **argv)
if (wait_mode) {
EnableDeathSignal(SIGCONT);
kill(getpid(), SIGSTOP);
- }
- // If our parent changed, then we were woken after traffic_server exited. There's no point trying to
- // emit a crashlog because traffic_server is gone.
- if (getppid() != parent) {
- return 0;
+ // A real crash sends us a notification on this socket; any other parent exit just closes
+ // it, so EOF (or a short read) means there is nothing to log. getppid() is unreliable here:
+ // PR_SET_PDEATHSIG can fire while our parent pid is still alive.
+ //
+ //
+ //
+ int crash_signo = 0; // value unused; an int keeps the socket framing aligned with the writer
+ ssize_t nbytes;
+ do {
+ nbytes = read(STDIN_FILENO, &crash_signo, sizeof(crash_signo));
+ } while (nbytes < 0 && errno == EINTR);
+
+ if (nbytes != static_cast<ssize_t>(sizeof(crash_signo))) {
+ return 0;
+ }
}
runroot_handler(argv);
diff --git a/src/traffic_server/Crash.cc b/src/traffic_server/Crash.cc
index ff5b5e29b9..bb5f703106 100644
--- a/src/traffic_server/Crash.cc
+++ b/src/traffic_server/Crash.cc
@@ -30,6 +30,7 @@
#include <string>
#include <unistd.h>
+#include <fcntl.h>
#if defined(__linux__)
#include <sys/prctl.h>
#include <sys/syscall.h>
@@ -146,6 +147,10 @@ crash_logger_init(const char *user)
crash_logger_pid = child;
crash_logger_fd = pipe[0];
+ // Don't leak this socket into later fork+exec children; the logger relies on seeing EOF
+ // here when we (its parent) exit.
+ fcntl(crash_logger_fd, F_SETFD, FD_CLOEXEC);
+
#if defined(__linux__)
// Allow the crash logger to ptrace us. Without this, Yama's ptrace_scope=1
// (the default on many distros) prevents a child process from tracing its parent.
@@ -174,6 +179,10 @@ crash_logger_invoke(int signo, siginfo_t *info, void *ctx)
// Let the crash logger free ...
kill(crash_logger_pid, SIGCONT);
+ // Notify the logger that this is a real crash; a plain exit closes the socket (EOF)
+ // instead. Written on every platform, ahead of the Linux-only thread payload below.
+ ATS_UNUSED_RETURN(write(crash_logger_fd, &signo, sizeof(signo)));
+
#if defined(__linux__)
// Write the crashing thread information to the crash logger. While the siginfo_t is blesses by POSIX, the
// ucontext_t can contain pointers, so it's highly platform dependent. On Linux with glibc, however, it is
diff --git a/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py b/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py
new file mode 100644
index 0000000000..0feb322f5e
--- /dev/null
+++ b/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py
@@ -0,0 +1,73 @@
+'''
+Verify that a normal (SIGTERM) shutdown does not spuriously fire traffic_crashlog.
+'''
+# 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.
+
+import os
+
+Test.Summary = '''
+Verify that gracefully stopping traffic_server (SIGTERM) does not cause the crash
+log helper (traffic_crashlog) to emit a bogus crash log.
+
+Regression test: the crash log helper is parked with SIGSTOP and arms a
+parent-death signal (PR_SET_PDEATHSIG=SIGCONT). When traffic_server exits
+normally -- e.g. a routine SIGTERM shutdown -- the kernel resumes the helper via
+that death signal. The helper used to decide "did I wake because of a real crash
+or a plain exit?" with a racy getppid() check, and would lose the race and write
+an empty crash log against the already-exiting process. The fix replaces that
+guess with a handshake: crash_logger_invoke() writes the signal number to the
+helper, and a plain exit closes the socket so the helper reads EOF and stays
+quiet.
+
+Note: PR_SET_PDEATHSIG is a no-op on Darwin, so this false positive only occurs
+on Linux. On other platforms this test still confirms that the helper is armed
+and does not emit a crash log on a clean shutdown.
+'''
+
+ts = Test.MakeATSProcess('ts')
+
+ts.Disk.records_config.update(
+ {
+ # Enable the "server" debug tag so traffic_server emits the "received exit signal,
+ # shutting down" DIAG line (the shutdown anchor checked below) to traffic.out.
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'server',
+ # Force the crash log helper on regardless of the build's TS_USE_REMOTE_UNWINDING
+ # default (it is NULL on platforms without remote unwinding, e.g. Darwin).
+ 'proxy.config.crash_log_helper': os.path.join(ts.Variables.BINDIR, 'traffic_crashlog'),
+ })
+
+tr = Test.AddTestRun('Start traffic_server with the crash log helper armed, then let AuTest SIGTERM it')
+tr.Processes.Default.Command = 'printf "crash log helper armed"'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(ts)
+tr.StillRunningAfter = ts
+
+# Anchor the scenario: confirm traffic_server actually ran the graceful (SIGTERM) shutdown
+# path -- the exact trigger that woke the crash log helper in production. Without this the
+# ExcludesExpression checks below could pass without ever exercising shutdown. (The crash
+# log helper itself is forced on via proxy.config.crash_log_helper above, and debug/DIAG
+# output is emitted to traffic.out.)
+ts.Disk.traffic_out.Content = Testers.ContainsExpression(
+ 'received exit signal, shutting down', 'traffic_server should have run the graceful shutdown path')
+
+# The crash log helper shares traffic_server's stderr (bound to traffic.out). On a clean
+# shutdown it must neither wake nor write a crash log.
+ts.Disk.traffic_out.Content += Testers.ExcludesExpression(
+ 'crashlog started', 'the crash log helper must not wake on a clean SIGTERM shutdown')
+ts.Disk.traffic_out.Content += Testers.ExcludesExpression(
+ 'wrote crash log', 'a clean SIGTERM shutdown must not produce a crash log')
(trafficserver) 06/33: header_rewrite: Improve URL Error messages (#13261)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 447dd6dcd42bafee61589143fadeede7f802c4a1
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Jun 23 12:56:43 2026 -0500
header_rewrite: Improve URL Error messages (#13261)
Old log:
ERROR: [header_rewrite] Rule not supported at this hook
New log:
ERROR: [header_rewrite] Rule not supported at hook=TS_HTTP_READ_RESPONSE_HDR_HOOK: %{TO-URL:URL} in /tmp/sb/header_rewrite_unsupported_url_hook/unsupported_url_hook.conf:18
(cherry picked from commit 46413e8303cd8467c4f8267fb529e00e0472b58e)
---
plugins/header_rewrite/conditions.cc | 58 ++++++++++++++++++++++++++++++++----
plugins/header_rewrite/conditions.h | 2 ++
plugins/header_rewrite/operators.cc | 2 ++
plugins/header_rewrite/resources.cc | 1 +
plugins/header_rewrite/resources.h | 1 +
plugins/header_rewrite/ruleset.cc | 2 ++
plugins/header_rewrite/statement.h | 29 +++++++++++++++++-
plugins/header_rewrite/value.cc | 6 ++++
8 files changed, 95 insertions(+), 6 deletions(-)
diff --git a/plugins/header_rewrite/conditions.cc b/plugins/header_rewrite/conditions.cc
index 3d32c3515d..1953864d79 100644
--- a/plugins/header_rewrite/conditions.cc
+++ b/plugins/header_rewrite/conditions.cc
@@ -39,6 +39,42 @@
static const sockaddr *getClientAddr(TSHttpTxn txnp, int txn_private_slot);
+static const char *
+get_hook_name(TSHttpHookID hook)
+{
+ if (hook == TS_REMAP_PSEUDO_HOOK) {
+ return "REMAP_PSEUDO_HOOK";
+ }
+
+ if (hook == TS_HTTP_LAST_HOOK) {
+ return "UNKNOWN_HOOK";
+ }
+
+ if (const char *name = TSHttpHookNameLookup(hook); name != nullptr) {
+ return name;
+ }
+
+ return "UNKNOWN_HOOK";
+}
+
+static const char *
+get_url_type_name(ConditionUrl::UrlType type)
+{
+ switch (type) {
+ case ConditionUrl::CLIENT:
+ return "CLIENT-URL";
+ case ConditionUrl::SERVER:
+ return "SERVER-URL";
+ case ConditionUrl::FROM:
+ return "FROM-URL";
+ case ConditionUrl::TO:
+ return "TO-URL";
+ case ConditionUrl::URL:
+ default:
+ return "URL";
+ }
+}
+
#if TS_HAS_CRIPTS
#include "cripts/Certs.hpp"
#endif
@@ -318,6 +354,18 @@ ConditionUrl::set_qualifier(const std::string &q)
}
}
+void
+ConditionUrl::log_error(const Resources &res, const char *message) const
+{
+ if (has_config_location()) {
+ TSError("[%s] %s at hook=%s: %%{%s%s%s} in %s:%d", PLUGIN_NAME, message, get_hook_name(res.hook), get_url_type_name(_type),
+ _qualifier.empty() ? "" : ":", _qualifier.c_str(), get_config_filename().c_str(), get_config_lineno());
+ } else {
+ TSError("[%s] %s at hook=%s: %%{%s%s%s}", PLUGIN_NAME, message, get_hook_name(res.hook), get_url_type_name(_type),
+ _qualifier.empty() ? "" : ":", _qualifier.c_str());
+ }
+}
+
void
ConditionUrl::append_value(std::string &s, const Resources &res)
{
@@ -328,7 +376,7 @@ ConditionUrl::append_value(std::string &s, const Resources &res)
// CLIENT always uses the pristine URL
Dbg(pi_dbg_ctl, " Using the pristine url");
if (TSHttpTxnPristineUrlGet(res.state.txnp, &bufp, &url) != TS_SUCCESS) {
- TSError("[%s] Error getting the pristine URL", PLUGIN_NAME);
+ log_error(res, "Error getting the pristine URL");
return;
}
} else if (_type == SERVER) {
@@ -336,7 +384,7 @@ ConditionUrl::append_value(std::string &s, const Resources &res)
bufp = res.server_bufp;
if (bufp && res.server_hdr_loc) {
if (TSHttpHdrUrlGet(bufp, res.server_hdr_loc, &url) != TS_SUCCESS) {
- TSError("[%s] Error getting the server request URL", PLUGIN_NAME);
+ log_error(res, "Error getting the server request URL");
return;
}
} else {
@@ -356,7 +404,7 @@ ConditionUrl::append_value(std::string &s, const Resources &res)
Dbg(pi_dbg_ctl, " Using the to url");
url = res._rri->mapToUrl;
} else {
- TSError("[%s] Invalid option value", PLUGIN_NAME);
+ log_error(res, "Invalid URL option value");
return;
}
} else {
@@ -364,11 +412,11 @@ ConditionUrl::append_value(std::string &s, const Resources &res)
bufp = res.bufp;
TSMLoc hdr_loc = res.hdr_loc;
if (TSHttpHdrUrlGet(bufp, hdr_loc, &url) != TS_SUCCESS) {
- TSError("[%s] Error getting the URL", PLUGIN_NAME);
+ log_error(res, "Error getting the URL");
return;
}
} else {
- TSError("[%s] Rule not supported at this hook", PLUGIN_NAME);
+ log_error(res, "Rule not supported");
return;
}
}
diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h
index 631854e8f4..65020646b8 100644
--- a/plugins/header_rewrite/conditions.h
+++ b/plugins/header_rewrite/conditions.h
@@ -300,6 +300,8 @@ protected:
bool eval(const Resources &res) override;
private:
+ void log_error(const Resources &res, const char *message) const;
+
UrlQualifiers _url_qual = URL_QUAL_NONE;
UrlType _type;
std::string _query_param; // Optional: specific query parameter name for QUERY sub-key
diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc
index b64b4ec469..53baa8456e 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -1707,6 +1707,7 @@ OperatorIf::add_operator(Parser &p, const char *filename, int lineno)
}
Dbg(pi_dbg_ctl, " Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(), p.get_arg().c_str(), p.get_value().c_str());
+ op->set_config_location(filename, lineno);
try {
op->initialize(p);
@@ -1739,6 +1740,7 @@ OperatorIf::make_condition(Parser &p, const char *filename, int lineno)
}
Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s", p.get_op().c_str(), p.get_arg().c_str());
+ cond->set_config_location(filename, lineno);
try {
cond->initialize(p);
diff --git a/plugins/header_rewrite/resources.cc b/plugins/header_rewrite/resources.cc
index 78450147b9..ed8d4ffe6a 100644
--- a/plugins/header_rewrite/resources.cc
+++ b/plugins/header_rewrite/resources.cc
@@ -32,6 +32,7 @@
void
Resources::gather(const ResourceIDs ids, TSHttpHookID hook)
{
+ this->hook = hook;
Dbg(pi_dbg_ctl, "Building resources, hook=%s", TSHttpHookNameLookup(hook));
Dbg(pi_dbg_ctl, "Gathering resources for hook %s with IDs %d", TSHttpHookNameLookup(hook), ids);
diff --git a/plugins/header_rewrite/resources.h b/plugins/header_rewrite/resources.h
index 80d1268106..d745dffdee 100644
--- a/plugins/header_rewrite/resources.h
+++ b/plugins/header_rewrite/resources.h
@@ -128,6 +128,7 @@ public:
#endif
TSHttpStatus resp_status = TS_HTTP_STATUS_NONE;
void *geo_handle = nullptr;
+ TSHttpHookID hook = TS_HTTP_LAST_HOOK;
struct LifetimeExtension {
std::string subject_storage;
diff --git a/plugins/header_rewrite/ruleset.cc b/plugins/header_rewrite/ruleset.cc
index ba17407f20..acbeafc72f 100644
--- a/plugins/header_rewrite/ruleset.cc
+++ b/plugins/header_rewrite/ruleset.cc
@@ -65,6 +65,7 @@ RuleSet::make_condition(Parser &p, const char *filename, int lineno)
}
Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s", p.get_op().c_str(), p.get_arg().c_str());
+ c->set_config_location(filename, lineno);
c->initialize(p);
if (!c->is_hook_valid(_hook)) {
delete c;
@@ -92,6 +93,7 @@ RuleSet::add_operator(Parser &p, const char *filename, int lineno)
if (nullptr != op) {
Dbg(pi_dbg_ctl, " Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(), p.get_arg().c_str(), p.get_value().c_str());
+ op->set_config_location(filename, lineno);
op->initialize(p);
if (!op->is_hook_valid(_hook)) {
delete op;
diff --git a/plugins/header_rewrite/statement.h b/plugins/header_rewrite/statement.h
index a45974b84b..0a354fc694 100644
--- a/plugins/header_rewrite/statement.h
+++ b/plugins/header_rewrite/statement.h
@@ -167,6 +167,31 @@ public:
ResourceIDs get_resource_ids() const;
+ void
+ set_config_location(const char *filename, int lineno)
+ {
+ _config_filename = filename ? filename : "";
+ _config_lineno = lineno;
+ }
+
+ bool
+ has_config_location() const
+ {
+ return !_config_filename.empty();
+ }
+
+ const std::string &
+ get_config_filename() const
+ {
+ return _config_filename;
+ }
+
+ int
+ get_config_lineno() const
+ {
+ return _config_lineno;
+ }
+
virtual void
initialize(Parser &)
{
@@ -271,7 +296,9 @@ protected:
private:
ResourceIDs _rsrc = RSRC_NONE;
std::vector<TSHttpHookID> _allowed_hooks;
- bool _initialized = false;
+ std::string _config_filename;
+ int _config_lineno = 0;
+ bool _initialized = false;
};
union PrivateSlotData {
diff --git a/plugins/header_rewrite/value.cc b/plugins/header_rewrite/value.cc
index 70fd17893a..38b00dd825 100644
--- a/plugins/header_rewrite/value.cc
+++ b/plugins/header_rewrite/value.cc
@@ -39,6 +39,9 @@ void
Value::set_value(const std::string &val, Statement *owner)
{
_value = val;
+ if (owner && owner->has_config_location()) {
+ set_config_location(owner->get_config_filename().c_str(), owner->get_config_lineno());
+ }
if (_value.find("%{") != std::string::npos) {
HRWSimpleTokenizer tokenizer(_value);
@@ -54,6 +57,9 @@ Value::set_value(const std::string &val, Statement *owner)
Parser parser;
if (parser.parse_line(cond_token)) {
+ if (has_config_location()) {
+ tcond_val->set_config_location(get_config_filename().c_str(), get_config_lineno());
+ }
tcond_val->initialize(parser);
require_resources(tcond_val->get_resource_ids());
} else {
(trafficserver) 33/33: Add per-plugin workload counters (#13278)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit bf218ea66a09218300e37f3ad0811213ba6ff9a5
Author: Mo Chen <mo...@apache.org>
AuthorDate: Mon Jun 22 17:46:30 2026 -0500
Add per-plugin workload counters (#13278)
Add per-plugin metrics that allow us to track how much work each plugin is doing
by counting their invocations, intercept bytes, and intercept transfers.
Add proxy.process.plugin.<name>.{invocations,bytes, transfers}, keyed by the
plugin DSO basename and bounded by the number of loaded plugins.
Global plugins load via raw dlopen and previously carried no identity, so they
are given a PluginThreadContext around TSPluginInit; the continuations they
create then carry plugin identity the same way remap plugins already do.
(cherry picked from commit 990bd049aa67b2293c3334f8a28b9f80c2eb650b)
---
include/proxy/PluginThreadContext.h | 61 +++++++++++
include/proxy/PluginVC.h | 6 ++
include/proxy/http/remap/PluginDso.h | 11 +-
src/api/InkContInternal.cc | 9 +-
src/proxy/CMakeLists.txt | 1 +
src/proxy/Plugin.cc | 38 +++++++
src/proxy/PluginThreadContext.cc | 65 ++++++++++++
src/proxy/PluginVC.cc | 16 +++
src/proxy/http/remap/PluginDso.cc | 7 ++
src/proxy/http/remap/RemapPlugins.cc | 2 +
src/proxy/unit_tests/stub.cc | 2 -
tests/gold_tests/pluginTest/lua/metrics.sh | 3 +-
.../per_plugin_metrics/per_plugin_metrics.test.py | 111 +++++++++++++++++++++
.../per_plugin_metrics/rules/global.conf | 20 ++++
.../pluginTest/regex_revalidate/metrics.sh | 3 +-
.../pluginTest/regex_revalidate/metrics_miss.sh | 3 +-
16 files changed, 344 insertions(+), 14 deletions(-)
diff --git a/include/proxy/PluginThreadContext.h b/include/proxy/PluginThreadContext.h
new file mode 100644
index 0000000000..88db0d825d
--- /dev/null
+++ b/include/proxy/PluginThreadContext.h
@@ -0,0 +1,61 @@
+/** @file
+
+ Per-plugin identity carried on the continuations a plugin creates.
+
+ @section license License
+
+ 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.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include "tscore/Ptr.h"
+#include "tsutil/Metrics.h"
+
+/** Carries a plugin's identity on the continuations it creates so that
+ * proxy.process.plugin.<name>.* workload counters can be attributed back to the originating
+ * plugin DSO.
+ *
+ * This lives in ts::proxy rather than ts::http_remap because it is shared by both remap plugins
+ * (PluginDso) and global plugins (GlobalPluginContext, in Plugin.cc). The library dependency only
+ * runs ts::http_remap -> ts::proxy, so putting it here lets both paths resolve these symbols. */
+class PluginThreadContext : public RefCountObjInHeap
+{
+public:
+ virtual void acquire() = 0;
+ virtual void release() = 0;
+
+ /** Register this plugin's proxy.process.plugin.<name>.* metrics. @a plugin_name is the DSO path;
+ * only its basename stem (extension removed) is used as <name>. */
+ void registerPluginMetrics(std::string_view plugin_name);
+
+ void countInvocation();
+
+ ts::Metrics::Counter::AtomicType *_invocations = nullptr;
+ ts::Metrics::Counter::AtomicType *_bytes = nullptr;
+ ts::Metrics::Counter::AtomicType *_transfers = nullptr;
+
+ static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */
+
+private:
+ /** Derive a metric-safe token from a plugin path: the basename with the extension removed, then any
+ * character outside [A-Za-z0-9_-] replaced by '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite"). */
+ static std::string _metric_token(std::string_view name);
+};
diff --git a/include/proxy/PluginVC.h b/include/proxy/PluginVC.h
index bfa8b1aa5b..ba016754e1 100644
--- a/include/proxy/PluginVC.h
+++ b/include/proxy/PluginVC.h
@@ -38,6 +38,7 @@
#include "proxy/Plugin.h"
#include "iocore/net/NetVConnection.h"
#include "tscore/ink_atomic.h"
+#include "tsutil/Metrics.h"
class PluginVCCore;
@@ -253,6 +254,11 @@ private:
Continuation *connect_to = nullptr;
bool connected = false;
+ // Transport counters of the plugin that created this intercept, captured at alloc(). Registry-owned
+ // (process-lifetime), so safe to hold raw. Null for core-internal PluginVCs.
+ ts::Metrics::Counter::AtomicType *_bytes = nullptr;
+ ts::Metrics::Counter::AtomicType *_transfers = nullptr;
+
IpEndpoint passive_addr_struct;
IpEndpoint active_addr_struct;
diff --git a/include/proxy/http/remap/PluginDso.h b/include/proxy/http/remap/PluginDso.h
index d3ea8087a2..68b01f9058 100644
--- a/include/proxy/http/remap/PluginDso.h
+++ b/include/proxy/http/remap/PluginDso.h
@@ -46,17 +46,14 @@
namespace fs = swoc::file;
#include "tscore/Ptr.h"
+#include "tsutil/Metrics.h"
#include "iocore/eventsystem/EventSystem.h"
#include "proxy/Plugin.h"
+#include "proxy/PluginThreadContext.h"
-class PluginThreadContext : public RefCountObjInHeap
-{
-public:
- virtual void acquire() = 0;
- virtual void release() = 0;
- static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */
-};
+#include <string>
+#include <string_view>
class PluginDso : public PluginThreadContext
{
diff --git a/src/api/InkContInternal.cc b/src/api/InkContInternal.cc
index 5d5ac2cdc6..6c2da59d7a 100644
--- a/src/api/InkContInternal.cc
+++ b/src/api/InkContInternal.cc
@@ -157,8 +157,13 @@ INKContInternal::handle_event(int event, void *edata)
/* set the plugin context */
auto *previousContext = pluginThreadContext;
pluginThreadContext = reinterpret_cast<PluginThreadContext *>(m_context);
- int retval = m_event_func((TSCont)this, (TSEvent)event, edata);
- pluginThreadContext = previousContext;
+ // Every TSCont (continuation) callback dispatch flows through here; count it against the owning
+ // plugin. (Remap doRemap dispatch does not, and is counted in RemapPlugins::run_plugin instead.)
+ if (pluginThreadContext != nullptr) {
+ pluginThreadContext->countInvocation();
+ }
+ int retval = m_event_func((TSCont)this, (TSEvent)event, edata);
+ pluginThreadContext = previousContext;
if (edata && event == EVENT_INTERVAL) {
Event *e = reinterpret_cast<Event *>(edata);
if (e->period != 0) {
diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt
index 93c1669784..c295837730 100644
--- a/src/proxy/CMakeLists.txt
+++ b/src/proxy/CMakeLists.txt
@@ -28,6 +28,7 @@ add_library(
ParentSelectionStrategy.cc
ParentSelection.cc
Plugin.cc
+ PluginThreadContext.cc
PluginVC.cc
ProtocolProbeSessionAccept.cc
ProxySession.cc
diff --git a/src/proxy/Plugin.cc b/src/proxy/Plugin.cc
index 5e2d1e6bb7..23a05a1b9d 100644
--- a/src/proxy/Plugin.cc
+++ b/src/proxy/Plugin.cc
@@ -22,12 +22,15 @@
*/
#include <cstdio>
+#include <string_view>
+#include <vector>
#include "tscore/ink_platform.h"
#include "tscore/ink_file.h"
#include "tscore/ParseRules.h"
#include "records/RecCore.h"
#include "tscore/Layout.h"
#include "proxy/Plugin.h"
+#include "proxy/http/remap/RemapPluginInfo.h"
#include "tscore/ink_cap.h"
#include "tscore/Filenames.h"
@@ -79,6 +82,31 @@ static const char *plugin_dir = ".";
using init_func_t = void (*)(int, char **);
+namespace
+{
+/** Plugin context for global plugins, which load via raw dlopen() rather than the
+ * PluginFactory/PluginDso path and so would otherwise have no PluginThreadContext to carry their
+ * identity. Installed as the thread-local pluginThreadContext around TSPluginInit so the plugin's
+ * continuations are stamped with it. Global plugins are never unloaded, so acquire()/release() are
+ * no-ops and instances live for the process lifetime. */
+class GlobalPluginContext : public PluginThreadContext
+{
+public:
+ explicit GlobalPluginContext(std::string_view name) { registerPluginMetrics(name); }
+ void
+ acquire() override
+ {
+ }
+ void
+ release() override
+ {
+ }
+};
+
+// Keeps global-plugin contexts reachable for the process lifetime; mutated single-threaded at startup.
+std::vector<GlobalPluginContext *> g_global_plugin_contexts;
+} // namespace
+
// Plugin registration vars
//
// plugin_reg_list has an entry for each plugin
@@ -192,7 +220,17 @@ single_plugin_init(int argc, char *argv[], bool validateOnly)
#endif
opterr = 0;
optarg = nullptr;
+
+ // Install this plugin's context around TSPluginInit so the continuations it creates carry its
+ // identity (see GlobalPluginContext).
+ auto *global_context = new GlobalPluginContext(path);
+ g_global_plugin_contexts.push_back(global_context);
+ auto *prev_plugin_context = pluginThreadContext;
+ pluginThreadContext = global_context;
+
init(argc, argv);
+
+ pluginThreadContext = prev_plugin_context;
} // done elevating access
if (plugin_reg_current->plugin_registered) {
diff --git a/src/proxy/PluginThreadContext.cc b/src/proxy/PluginThreadContext.cc
new file mode 100644
index 0000000000..23575025e1
--- /dev/null
+++ b/src/proxy/PluginThreadContext.cc
@@ -0,0 +1,65 @@
+/** @file
+
+ Per-plugin identity carried on the continuations a plugin creates.
+
+ @section license License
+
+ 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.
+ */
+
+#include "proxy/PluginThreadContext.h"
+
+#include <algorithm>
+#include <cctype>
+#include <string>
+#include <string_view>
+
+void
+PluginThreadContext::registerPluginMetrics(std::string_view plugin_name)
+{
+ std::string prefix = "proxy.process.plugin." + _metric_token(plugin_name) + ".";
+
+ _invocations = ts::Metrics::Counter::createPtr(prefix + "invocations");
+ _bytes = ts::Metrics::Counter::createPtr(prefix + "bytes");
+ _transfers = ts::Metrics::Counter::createPtr(prefix + "transfers");
+}
+
+void
+PluginThreadContext::countInvocation()
+{
+ if (_invocations != nullptr) {
+ _invocations->increment(1);
+ }
+}
+
+std::string
+PluginThreadContext::_metric_token(std::string_view name)
+{
+ if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) {
+ name.remove_prefix(slash + 1);
+ }
+ if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) {
+ name = name.substr(0, dot);
+ }
+
+ std::string token{name};
+ std::replace_if(token.begin(), token.end(), [](unsigned char c) { return !(std::isalnum(c) || c == '_' || c == '-'); }, '_');
+ if (token.empty()) {
+ token = "unknown";
+ }
+ return token;
+}
diff --git a/src/proxy/PluginVC.cc b/src/proxy/PluginVC.cc
index 3caa8dc673..38cecb3f0b 100644
--- a/src/proxy/PluginVC.cc
+++ b/src/proxy/PluginVC.cc
@@ -72,6 +72,7 @@
****************************************************************************/
#include "proxy/PluginVC.h"
+#include "proxy/http/remap/RemapPluginInfo.h"
#include "../iocore/net/P_Net.h"
#include "tscore/Regression.h"
#if TS_HAS_TESTS
@@ -466,6 +467,11 @@ PluginVC::transfer_bytes(MIOBuffer *transfer_to, IOBufferReader *transfer_from,
total_added += moved;
}
+ // Attribute the bytes moved across this intercept to the owning plugin.
+ if (core_obj->_bytes != nullptr && total_added > 0) {
+ core_obj->_bytes->increment(total_added);
+ }
+
return total_added;
}
@@ -564,6 +570,11 @@ PluginVC::process_write_side()
return;
}
+ // Count a write-side pass for the owning plugin only when data actually moved to the peer.
+ if (core_obj->_transfers != nullptr && added > 0) {
+ core_obj->_transfers->increment(1);
+ }
+
write_state.vio.ndone += added;
other_side->read_state.vio.ndone += added;
@@ -1002,6 +1013,11 @@ PluginVCCore::alloc(Continuation *acceptor, int64_t buffer_index, int64_t buffer
PluginVCCore *pvc = new PluginVCCore;
pvc->init(buffer_index, buffer_water_mark);
pvc->connect_to = acceptor;
+ // Capture the creating plugin's transport counters (registry-owned) for per-plugin accounting.
+ if (pluginThreadContext != nullptr) {
+ pvc->_bytes = pluginThreadContext->_bytes;
+ pvc->_transfers = pluginThreadContext->_transfers;
+ }
return pvc;
}
diff --git a/src/proxy/http/remap/PluginDso.cc b/src/proxy/http/remap/PluginDso.cc
index 4b75f4e475..7c3982358f 100644
--- a/src/proxy/http/remap/PluginDso.cc
+++ b/src/proxy/http/remap/PluginDso.cc
@@ -29,6 +29,7 @@
#include "proxy/http/remap/PluginDso.h"
#include "iocore/eventsystem/Freer.h"
+#include "tsutil/Metrics.h"
#ifdef PLUGIN_DSO_TESTS
#include "unit-tests/plugin_testing_common.h"
#else
@@ -38,6 +39,8 @@
#endif
#include <cstdlib>
+#include <string>
+#include <string_view>
#include <utility>
namespace
@@ -139,6 +142,10 @@ PluginDso::load(std::string &error, const fs::path &compilerPath)
}
PluginDbg(_dbg_ctl(), "plugin '%s' finished loading DSO", _configPath.c_str());
+ if (result) {
+ registerPluginMetrics(_effectivePath.string());
+ }
+
return result;
}
diff --git a/src/proxy/http/remap/RemapPlugins.cc b/src/proxy/http/remap/RemapPlugins.cc
index 9c55ee2665..54c52c03be 100644
--- a/src/proxy/http/remap/RemapPlugins.cc
+++ b/src/proxy/http/remap/RemapPlugins.cc
@@ -59,6 +59,8 @@ RemapPlugins::run_plugin(RemapPluginInst *plugin)
_s->os_response_plugin_inst = plugin;
}
+ plugin->_plugin.countInvocation();
+
HttpTransact::milestone_start_api_time(_s);
plugin_retcode = plugin->doRemap(reinterpret_cast<TSHttpTxn>(_s->state_machine), &rri);
HttpTransact::milestone_update_api_time(_s);
diff --git a/src/proxy/unit_tests/stub.cc b/src/proxy/unit_tests/stub.cc
index a1a95fe8cc..ba279051a3 100644
--- a/src/proxy/unit_tests/stub.cc
+++ b/src/proxy/unit_tests/stub.cc
@@ -22,5 +22,3 @@
*/
#include "proxy/IPAllow.h"
-
-uint8_t IpAllow::subjects[IpAllow::Subject::MAX_SUBJECTS];
diff --git a/tests/gold_tests/pluginTest/lua/metrics.sh b/tests/gold_tests/pluginTest/lua/metrics.sh
index 6aec5bc03b..0d4c8d8617 100755
--- a/tests/gold_tests/pluginTest/lua/metrics.sh
+++ b/tests/gold_tests/pluginTest/lua/metrics.sh
@@ -18,7 +18,8 @@ N=60
while (( N > 0 ))
do
rm -f metrics.out metrics.txt
- traffic_ctl metric match lua > metrics.out
+ # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters.
+ traffic_ctl metric match '^plugin\.lua\.' > metrics.out
sleep 1
sed 's/ [0-9][0-9]*//' metrics.out > metrics.txt
if diff metrics.txt ${AUTEST_TEST_DIR}/gold/metrics.gold
diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py
new file mode 100644
index 0000000000..202412e621
--- /dev/null
+++ b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py
@@ -0,0 +1,111 @@
+'''
+Verify the per-plugin workload counters proxy.process.plugin.<name>.{invocations,bytes,transfers}.
+'''
+# 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.
+
+import re
+
+Test.Summary = '''
+Verify the per-plugin workload counters proxy.process.plugin.<name>.invocations (global and remap
+dispatch), .bytes and .transfers (PluginVC intercept transport).
+'''
+
+Test.SkipUnless(
+ Condition.PluginExists('header_rewrite.so'), Condition.PluginExists('conf_remap.so'), Condition.PluginExists('generator.so'))
+
+Test.ContinueOnFail = True
+
+
+class TestPerPluginWorkloadCounters:
+
+ def __init__(self):
+ self.setUpOriginServer()
+ self.setUpTS()
+
+ def setUpOriginServer(self):
+ self.server = Test.MakeOriginServer("server")
+ request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ self.server.addResponse("sessionfile.log", request_header, response_header)
+
+ def setUpTS(self):
+ self.ts = Test.MakeATSProcess("ts")
+
+ # header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites;
+ # generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters.
+ self.ts.Setup.CopyAs('rules/global.conf', Test.RunDirectory)
+ self.ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/global.conf')
+ self.ts.Disk.remap_config.AddLine(
+ f'map http://test.example http://127.0.0.1:{self.server.Variables.Port} '
+ f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1')
+ self.ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so')
+
+ self.ts.Disk.records_config.update({
+ 'proxy.config.diags.debug.enabled': 0,
+ 'proxy.config.diags.debug.tags': 'plugin',
+ })
+
+ def driveTraffic(self):
+ # curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap)
+ # matches; the 200 confirms it was served rather than 404'd.
+ tr = Test.AddTestRun("Drive traffic through global + remap plugins")
+ tr.Processes.Default.StartBefore(self.server)
+ tr.Processes.Default.StartBefore(self.ts)
+ tr.MakeCurlCommand(f'-s -D - -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://test.example/"', ts=self.ts)
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(
+ '200 OK', 'request should match the remap rule and be served')
+ tr.StillRunningAfter = self.server
+ tr.StillRunningAfter = self.ts
+
+ # Drive a generator request: it serves a 4096-byte body through a PluginVC intercept.
+ tr = Test.AddTestRun("Drive traffic through a PluginVC intercept plugin")
+ tr.MakeCurlCommand(
+ f'-s -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://gen.example/nocache/4096"', ts=self.ts)
+ tr.Processes.Default.ReturnCode = 0
+ tr.StillRunningAfter = self.ts
+ tr.StillRunningAfter = self.server
+
+ def checkMetric(self, description, metric, message):
+ tr = Test.AddTestRun(description)
+ tr.Processes.Default.Env = self.ts.Env
+ tr.Processes.Default.Command = f'traffic_ctl metric get {metric}'
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(f'{re.escape(metric)} [1-9]', message)
+ tr.StillRunningAfter = self.ts
+ tr.StillRunningAfter = self.server
+
+ def checkMetrics(self):
+ self.checkMetric(
+ "Global plugin invocation counter is non-zero", 'proxy.process.plugin.header_rewrite.invocations',
+ 'global header_rewrite invocations should be counted')
+ self.checkMetric(
+ "Remap plugin invocation counter is non-zero", 'proxy.process.plugin.conf_remap.invocations',
+ 'remap conf_remap invocations should be counted')
+ self.checkMetric(
+ "PluginVC intercept bytes counter is non-zero", 'proxy.process.plugin.generator.bytes',
+ 'generator PluginVC transport bytes should be counted')
+ self.checkMetric(
+ "PluginVC intercept transfers counter is non-zero", 'proxy.process.plugin.generator.transfers',
+ 'generator PluginVC transfer events should be counted')
+
+ def run(self):
+ self.driveTraffic()
+ self.checkMetrics()
+
+
+TestPerPluginWorkloadCounters().run()
diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf b/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf
new file mode 100644
index 0000000000..b715700328
--- /dev/null
+++ b/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf
@@ -0,0 +1,20 @@
+# 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.
+
+# Fire on every response so the global header_rewrite continuation is dispatched (through
+# INKContInternal::handle_event) for each transaction, exercising the per-plugin invocation counter.
+cond %{SEND_RESPONSE_HDR_HOOK}
+set-header X-Per-Plugin-Metrics "1"
diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh b/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh
index 4365a0e6e0..4b5afd2f99 100755
--- a/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh
+++ b/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh
@@ -18,7 +18,8 @@ N=60
while (( N > 0 ))
do
rm -f metrics.out
- traffic_ctl metric match regex_revalidate > metrics.out
+ # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters.
+ traffic_ctl metric match '^plugin\.regex_revalidate\.' > metrics.out
sleep 1
if diff metrics.out ${AUTEST_TEST_DIR}/gold/metrics.gold
then
diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh b/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh
index 164a4e4527..c2b4db8634 100755
--- a/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh
+++ b/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh
@@ -18,7 +18,8 @@ N=60
while (( N > 0 ))
do
rm -f metrics.out
- traffic_ctl metric match regex_revalidate > metrics.out
+ # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters.
+ traffic_ctl metric match '^plugin\.regex_revalidate\.' > metrics.out
sleep 1
if diff metrics.out ${AUTEST_TEST_DIR}/gold/metrics_miss.gold
then
(trafficserver) 10/33: Test TLS async without ENGINE (#13264)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit de3841dab740a014b9a36c77f03ca13317915fee
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Jun 23 12:43:51 2026 -0500
Test TLS async without ENGINE (#13264)
Some OpenSSL builds expose the async APIs ATS needs while disabling
the legacy ENGINE interface. The previous async handshake coverage
depended on a test-only dynamic ENGINE just to make OpenSSL suspend
during the TLS handshake, leaving those builds without the async-path
AuTest coverage.
This replaces the ENGINE plugin with a test plugin that pauses the
current OpenSSL async job from a TLS certificate hook. This keeps the
test focused on ATS's SSL_ERROR_WANT_ASYNC handling while avoiding the
deprecated ENGINE API and dynamic ENGINE availability checks.
(cherry picked from commit 422d770f3fe29f47f21695c2a1346835ea9a93a3)
---
..._engine.test.py => tls_async_handshake.test.py} | 43 +--
tests/tools/plugins/CMakeLists.txt | 6 +-
tests/tools/plugins/async_engine.c | 325 ---------------------
tests/tools/plugins/async_handshake.cc | 182 ++++++++++++
4 files changed, 194 insertions(+), 362 deletions(-)
diff --git a/tests/gold_tests/tls/tls_engine.test.py b/tests/gold_tests/tls/tls_async_handshake.test.py
similarity index 66%
rename from tests/gold_tests/tls/tls_engine.test.py
rename to tests/gold_tests/tls/tls_async_handshake.test.py
index 623f430538..5ac4e28959 100644
--- a/tests/gold_tests/tls/tls_engine.test.py
+++ b/tests/gold_tests/tls/tls_async_handshake.test.py
@@ -18,25 +18,23 @@
import os
-# Someday should add client cert to origin to exercise the
-# engine interface on the other side
-
Test.Summary = '''
-Test tls via the async interface with the sample async_engine
+Test TLS handshakes through OpenSSL's async job interface.
'''
+async_handshake = os.path.join(Test.Variables.AtsTestPluginsDir, 'async_handshake.so')
+
Test.SkipUnless(
Condition.HasOpenSSLVersion('1.1.1'),
Condition.IsOpenSSL(),
+ Condition(lambda: os.path.isfile(async_handshake), async_handshake + " not found."),
)
-# Define default ATS
ts = Test.MakeATSProcess("ts", enable_tls=True)
server = Test.MakeOriginServer("server")
-ts.Setup.Copy(os.path.join(Test.Variables.AtsTestPluginsDir, 'async_engine.so'), Test.RunDirectory)
+Test.PrepareTestPlugin(async_handshake, ts)
-# Add info the origin server responses
server.addResponse(
"sessionlog.json", {
"headers": "GET / HTTP/1.1\r\nuuid: basic\r\n\r\n",
@@ -44,12 +42,12 @@ server.addResponse(
"body": ""
}, {
"headers":
- "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n",
+ "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n"
+ "Cache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n",
"timestamp": "1469733493.993",
"body": "ok"
})
-# add ssl materials like key, certificates for the server
ts.addSSLfile("ssl/server.pem")
ts.addSSLfile("ssl/server.key")
@@ -61,36 +59,11 @@ ts.Disk.records_config.update(
'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
'proxy.config.exec_thread.autoconfig.scale': 1.0,
- 'proxy.config.ssl.engine.conf_file': '{0}/ts/config/load_engine.cnf'.format(Test.RunDirectory),
'proxy.config.ssl.async.handshake.enabled': 1,
'proxy.config.diags.debug.enabled': 0,
'proxy.config.diags.debug.tags': 'ssl|http'
})
-ts.Disk.MakeConfigFile('load_engine.cnf').AddLines(
- [
- 'openssl_conf = openssl_init',
- '',
- '[openssl_init]',
- '',
- 'engines = engine_section',
- '',
- '[engine_section]',
- '',
- 'async = async_section',
- '',
- '[async_section]',
- '',
- 'dynamic_path = {0}/async_engine.so'.format(Test.RunDirectory),
- '',
- 'engine_id = async-test',
- '',
- 'default_algorithms = RSA',
- '',
- 'init = 1',
- ])
-
-# Make a basic request. Hopefully it goes through
tr = Test.AddTestRun("Run-Test")
tr.MakeCurlCommand("-k -v -H uuid:basic -H host:example.com https://127.0.0.1:{0}/".format(ts.Variables.ssl_port), ts=ts)
tr.ReturnCode = 0
@@ -99,4 +72,4 @@ tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Varia
tr.Processes.Default.Streams.All = Testers.ContainsExpression(r"HTTP/(2|1\.1) 200", "Request succeeds")
tr.StillRunningAfter = server
-ts.Disk.traffic_out.Content += Testers.ContainsExpression("Send signal to ", "The Async engine triggers")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression("resumed OpenSSL async job", "The OpenSSL async job resumes")
diff --git a/tests/tools/plugins/CMakeLists.txt b/tests/tools/plugins/CMakeLists.txt
index 05a4d3d6e1..bbdc5866cd 100644
--- a/tests/tools/plugins/CMakeLists.txt
+++ b/tests/tools/plugins/CMakeLists.txt
@@ -34,7 +34,6 @@ target_link_libraries(test_cppapi PRIVATE ts::tscppapi)
add_autest_plugin(test_hooks test_hooks.cc)
add_autest_plugin(test_log_interface test_log_interface.cc)
add_autest_plugin(user_args user_args.cc)
-add_autest_plugin(async_engine async_engine.c)
add_autest_plugin(hook_tunnel_plugin hook_tunnel_plugin.cc)
add_autest_plugin(tunnel_transform tunnel_transform.cc)
add_autest_plugin(http2_close_connection http2_close_connection.cc)
@@ -44,4 +43,7 @@ target_link_libraries(ssl_client_verify_test PRIVATE OpenSSL::SSL)
target_link_libraries(ssl_hook_test PRIVATE OpenSSL::SSL)
target_link_libraries(ssl_secret_load_test PRIVATE OpenSSL::SSL)
target_link_libraries(ssl_verify_test PRIVATE OpenSSL::SSL)
-target_link_libraries(async_engine PRIVATE OpenSSL::SSL)
+if(TS_USE_TLS_ASYNC)
+ add_autest_plugin(async_handshake async_handshake.cc)
+ target_link_libraries(async_handshake PRIVATE OpenSSL::SSL)
+endif()
diff --git a/tests/tools/plugins/async_engine.c b/tests/tools/plugins/async_engine.c
deleted file mode 100644
index 8032293449..0000000000
--- a/tests/tools/plugins/async_engine.c
+++ /dev/null
@@ -1,325 +0,0 @@
-/** @file
-
- Test crypto engine
-
- @section license License
-
- 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 engine to exercise the asynchronous job interface
- * It performs the standard RSA operations, but for private key
- * operations spawns a thread to sleep for 5 seconds before resuming
- * the asynchronous job
- */
-
-/* TLS_USE_TLS_ASYNC defined in ink_config.h */
-#include "tscore/ink_config.h"
-#include "tscore/ink_defs.h"
-#if TS_USE_TLS_ASYNC
-
-#include <stdio.h>
-#include <string.h>
-
-#include <openssl/engine.h>
-#include <openssl/rsa.h>
-#include <openssl/async.h>
-#include <openssl/crypto.h>
-#include <openssl/ssl.h>
-#include <openssl/modes.h>
-#include <pthread.h>
-#include <unistd.h>
-
-/* Engine Id and Name */
-static const char *engine_id = "async-test";
-static const char *engine_name = "Asynchronous test engine";
-
-/* Engine Lifetime functions */
-static int async_destroy(ENGINE *e);
-static int engine_async_init(ENGINE *e);
-static int async_finish(ENGINE *e);
-void engine_load_async_int(void);
-
-static void async_pause_job(void);
-
-/* RSA */
-
-static int async_pub_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
-static int async_pub_dec(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
-static int async_rsa_priv_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
-static int async_rsa_priv_dec(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
-static int async_rsa_mod_exp(BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx);
-
-static int async_rsa_init(RSA *rsa);
-static int async_rsa_finish(RSA *rsa);
-
-static RSA_METHOD *async_rsa_method = NULL;
-
-EVP_PKEY *
-async_load_privkey(ENGINE *e ATS_UNUSED, const char *s_key_id, UI_METHOD *ui_method ATS_UNUSED, void *callback_data ATS_UNUSED)
-{
- fprintf(stderr, "Loading key %s\n", s_key_id);
- FILE *f = fopen(s_key_id, "r");
- if (f) {
- EVP_PKEY *key = PEM_read_PrivateKey(f, NULL, NULL, NULL);
- fclose(f);
- return key;
- } else {
- return NULL;
- }
-}
-
-static int
-bind_async(ENGINE *e)
-{
- /* Setup RSA_METHOD */
- if ((async_rsa_method = RSA_meth_new("Async RSA method", 0)) == NULL ||
- RSA_meth_set_pub_enc(async_rsa_method, async_pub_enc) == 0 || RSA_meth_set_pub_dec(async_rsa_method, async_pub_dec) == 0 ||
- RSA_meth_set_priv_enc(async_rsa_method, async_rsa_priv_enc) == 0 ||
- RSA_meth_set_priv_dec(async_rsa_method, async_rsa_priv_dec) == 0 ||
- RSA_meth_set_mod_exp(async_rsa_method, async_rsa_mod_exp) == 0 ||
- RSA_meth_set_bn_mod_exp(async_rsa_method, BN_mod_exp_mont) == 0 || RSA_meth_set_init(async_rsa_method, async_rsa_init) == 0 ||
- RSA_meth_set_finish(async_rsa_method, async_rsa_finish) == 0) {
- fprintf(stderr, "Failed to initialize rsa method\n");
- return 0;
- }
-
- /* Ensure the dasync error handling is set up */
- ERR_load_ASYNC_strings();
-
- if (!ENGINE_set_id(e, engine_id) || !ENGINE_set_name(e, engine_name) || !ENGINE_set_RSA(e, async_rsa_method) ||
- !ENGINE_set_destroy_function(e, async_destroy) || !ENGINE_set_init_function(e, engine_async_init) ||
- !ENGINE_set_finish_function(e, async_finish) || !ENGINE_set_load_privkey_function(e, async_load_privkey)) {
- fprintf(stderr, "Failed to initialize\n");
- return 0;
- }
-
- return 1;
-}
-
-#ifndef OPENSSL_NO_DYNAMIC_ENGINE
-static int
-bind_helper(ENGINE *e, const char *id)
-{
- if (id && (strcmp(id, engine_id) != 0))
- return 0;
- if (!bind_async(e))
- return 0;
- return 1;
-}
-
-IMPLEMENT_DYNAMIC_CHECK_FN()
-IMPLEMENT_DYNAMIC_BIND_FN(bind_helper)
-#endif
-
-static ENGINE *
-engine_async(void)
-{
- ENGINE *ret = ENGINE_new();
- if (!ret)
- return NULL;
- if (!bind_async(ret)) {
- ENGINE_free(ret);
- return NULL;
- }
- return ret;
-}
-
-void
-engine_load_async_int(void)
-{
- ENGINE *toadd = engine_async();
- if (!toadd)
- return;
- ENGINE_add(toadd);
- ENGINE_free(toadd);
- ERR_clear_error();
-}
-
-static int
-engine_async_init(ENGINE *e ATS_UNUSED)
-{
- return 1;
-}
-
-static int
-async_finish(ENGINE *e ATS_UNUSED)
-{
- return 1;
-}
-
-static int
-async_destroy(ENGINE *e ATS_UNUSED)
-{
- RSA_meth_free(async_rsa_method);
- return 1;
-}
-
-static void
-wait_cleanup(ASYNC_WAIT_CTX *ctx ATS_UNUSED, const void *key ATS_UNUSED, OSSL_ASYNC_FD readfd, void *pvwritefd)
-{
- OSSL_ASYNC_FD *pwritefd = (OSSL_ASYNC_FD *)pvwritefd;
- close(readfd);
- close(*((OSSL_ASYNC_FD *)pwritefd));
- OPENSSL_free(pwritefd);
- fprintf(stderr, "Cleanup %d and %d\n", readfd, *pwritefd);
-}
-
-#define DUMMY_CHAR 'X'
-
-static void
-async_pause_job(void)
-{
- ASYNC_JOB *job;
- ASYNC_WAIT_CTX *waitctx;
- OSSL_ASYNC_FD pipefds[2] = {0, 0};
- OSSL_ASYNC_FD *writefd;
- char buf = DUMMY_CHAR;
-
- if ((job = ASYNC_get_current_job()) == NULL) {
- fprintf(stderr, "No job\n");
- return;
- }
-
- waitctx = ASYNC_get_wait_ctx(job);
-
- if (ASYNC_WAIT_CTX_get_fd(waitctx, engine_id, &pipefds[0], (void **)&writefd)) {
- fprintf(stderr, "Existing wait ctx %d\n", *writefd);
- } else {
- writefd = (OSSL_ASYNC_FD *)OPENSSL_malloc(sizeof(*writefd));
- if (writefd == NULL)
- return;
- if (pipe(pipefds) != 0) {
- OPENSSL_free(writefd);
- return;
- }
- *writefd = pipefds[1];
-
- fprintf(stderr, "New wait ctx %d %d\n", pipefds[0], pipefds[1]);
-
- if (!ASYNC_WAIT_CTX_set_wait_fd(waitctx, engine_id, pipefds[0], writefd, wait_cleanup)) {
- fprintf(stderr, "set_wait_fd failed\n");
- wait_cleanup(waitctx, engine_id, pipefds[0], writefd);
- return;
- }
- }
-
- /* Ignore errors - we carry on anyway */
- ASYNC_pause_job();
-
- /* Clear the wake signal */
- if (read(pipefds[0], &buf, 1) < 0)
- return;
-}
-
-void *
-delay_method(void *arg)
-{
- int signal_fd = (intptr_t)arg;
- sleep(2);
- char buf = DUMMY_CHAR;
- if (write(signal_fd, &buf, sizeof(buf)) < 0) {
- fprintf(stderr, "Failed to send signal to %d, errno=%d\n", signal_fd, errno);
- } else {
- fprintf(stderr, "Send signal to %d\n", signal_fd);
- }
- return NULL;
-}
-
-void
-spawn_delay_thread()
-{
- pthread_t thread_id;
- ASYNC_JOB *job;
- if ((job = ASYNC_get_current_job()) == NULL) {
- fprintf(stderr, "Spawn no job\n");
- return;
- }
-
- ASYNC_WAIT_CTX *waitctx = ASYNC_get_wait_ctx(job);
-
- size_t numfds;
- if (ASYNC_WAIT_CTX_get_all_fds(waitctx, NULL, &numfds) && numfds > 0) {
- fprintf(stderr, "Spawn, wait_ctx exists. Go away, something else is using this job\n");
- } else {
- OSSL_ASYNC_FD signal_fd;
- OSSL_ASYNC_FD pipefds[2] = {0, 0};
- OSSL_ASYNC_FD *writefd = OPENSSL_malloc(sizeof(*writefd));
- if (pipe(pipefds) < 0) {
- fprintf(stderr, "Spawn, failed to create pipe errno=%d\n", errno);
- return;
- }
- signal_fd = *writefd = pipefds[1];
- ASYNC_WAIT_CTX_set_wait_fd(waitctx, engine_id, pipefds[0], writefd, wait_cleanup);
- fprintf(stderr, "Spawn, create wait_ctx %d %d\n", pipefds[0], pipefds[1]);
- pthread_create(&thread_id, NULL, delay_method, (void *)((intptr_t)signal_fd));
- }
-}
-
-/*
- * RSA implementation
- */
-
-static int
-async_pub_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)
-{
- return RSA_meth_get_pub_enc(RSA_PKCS1_OpenSSL())(flen, from, to, rsa, padding);
-}
-
-static int
-async_pub_dec(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)
-{
- return RSA_meth_get_pub_dec(RSA_PKCS1_OpenSSL())(flen, from, to, rsa, padding);
-}
-
-static int
-async_rsa_priv_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)
-{
- fprintf(stderr, "async_priv_enc\n");
- spawn_delay_thread();
- async_pause_job();
- fprintf(stderr, "async_priv_enc resume\n");
- return RSA_meth_get_priv_enc(RSA_PKCS1_OpenSSL())(flen, from, to, rsa, padding);
-}
-
-static int
-async_rsa_priv_dec(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)
-{
- fprintf(stderr, "async_priv_dec\n");
- spawn_delay_thread();
- async_pause_job();
- return RSA_meth_get_priv_dec(RSA_PKCS1_OpenSSL())(flen, from, to, rsa, padding);
-}
-
-static int
-async_rsa_mod_exp(BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx)
-{
- return RSA_meth_get_mod_exp(RSA_PKCS1_OpenSSL())(r0, I, rsa, ctx);
-}
-
-static int
-async_rsa_init(RSA *rsa)
-{
- return RSA_meth_get_init(RSA_PKCS1_OpenSSL())(rsa);
-}
-static int
-async_rsa_finish(RSA *rsa)
-{
- return RSA_meth_get_finish(RSA_PKCS1_OpenSSL())(rsa);
-}
-#endif /* TS_USE_TLS_ASYNC */
diff --git a/tests/tools/plugins/async_handshake.cc b/tests/tools/plugins/async_handshake.cc
new file mode 100644
index 0000000000..3747ce17eb
--- /dev/null
+++ b/tests/tools/plugins/async_handshake.cc
@@ -0,0 +1,182 @@
+/** @file
+
+ Test TLS handshakes that pause inside an OpenSSL async job.
+
+ @section license License
+
+ 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.
+ */
+
+#include <ts/ts.h>
+
+#include <openssl/async.h>
+#include <openssl/crypto.h>
+
+#include <cerrno>
+#include <cstdint>
+#include <cstdio>
+#include <pthread.h>
+#include <unistd.h>
+
+#define PLUGIN_NAME "async_handshake"
+#define PCP "[" PLUGIN_NAME "] "
+
+namespace
+{
+DbgCtl dbg_ctl{PLUGIN_NAME};
+char async_hook_key;
+
+void
+wait_cleanup(ASYNC_WAIT_CTX * /* ctx ATS_UNUSED */, const void * /* key ATS_UNUSED */, OSSL_ASYNC_FD read_fd, void *write_fd_ptr)
+{
+ auto *write_fd = static_cast<OSSL_ASYNC_FD *>(write_fd_ptr);
+
+ close(read_fd);
+ close(*write_fd);
+ OPENSSL_free(write_fd);
+}
+
+void *
+wake_async_job(void *arg)
+{
+ auto signal_fd = static_cast<OSSL_ASYNC_FD>(reinterpret_cast<intptr_t>(arg));
+ char buf = 'X';
+
+ usleep(100 * 1000);
+ if (write(signal_fd, &buf, sizeof(buf)) < 0) {
+ fprintf(stderr, PCP "failed to send async wake signal to %d, errno=%d\n", signal_fd, errno);
+ } else {
+ fprintf(stderr, PCP "sent async wake signal to %d\n", signal_fd);
+ }
+
+ return nullptr;
+}
+
+bool
+install_wait_fd(ASYNC_WAIT_CTX *wait_ctx, OSSL_ASYNC_FD &read_fd)
+{
+ OSSL_ASYNC_FD *write_fd = nullptr;
+ if (ASYNC_WAIT_CTX_get_fd(wait_ctx, &async_hook_key, &read_fd, reinterpret_cast<void **>(&write_fd))) {
+ return true;
+ }
+
+ OSSL_ASYNC_FD pipe_fds[2] = {-1, -1};
+ write_fd = static_cast<OSSL_ASYNC_FD *>(OPENSSL_malloc(sizeof(*write_fd)));
+ if (write_fd == nullptr) {
+ TSError(PCP "failed to allocate async wait fd storage");
+ return false;
+ }
+
+ if (pipe(pipe_fds) < 0) {
+ TSError(PCP "failed to create async wait pipe: errno=%d", errno);
+ OPENSSL_free(write_fd);
+ return false;
+ }
+ *write_fd = pipe_fds[1];
+
+ if (!ASYNC_WAIT_CTX_set_wait_fd(wait_ctx, &async_hook_key, pipe_fds[0], write_fd, wait_cleanup)) {
+ TSError(PCP "failed to install async wait fd");
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ OPENSSL_free(write_fd);
+ return false;
+ }
+
+ pthread_t thread_id;
+ if (pthread_create(&thread_id, nullptr, wake_async_job, reinterpret_cast<void *>(static_cast<intptr_t>(*write_fd))) == 0) {
+ pthread_detach(thread_id);
+ } else if (write(*write_fd, "X", 1) < 0) {
+ TSError(PCP "failed to start async wake thread and immediate wake failed: errno=%d", errno);
+ return false;
+ }
+
+ read_fd = pipe_fds[0];
+ return true;
+}
+
+void
+pause_current_async_job(TSVConn ssl_vc)
+{
+ ASYNC_JOB *job = ASYNC_get_current_job();
+ if (job == nullptr) {
+ TSError(PCP "SSL cert hook is not running in an OpenSSL async job");
+ TSVConnReenable(ssl_vc);
+ return;
+ }
+
+ ASYNC_WAIT_CTX *wait_ctx = ASYNC_get_wait_ctx(job);
+ if (wait_ctx == nullptr) {
+ TSError(PCP "OpenSSL async job has no wait context");
+ TSVConnReenable(ssl_vc);
+ return;
+ }
+
+ OSSL_ASYNC_FD read_fd = -1;
+ if (!install_wait_fd(wait_ctx, read_fd)) {
+ TSVConnReenable(ssl_vc);
+ return;
+ }
+
+ TSVConnReenable(ssl_vc);
+
+ fprintf(stderr, PCP "pausing OpenSSL async job\n");
+ if (!ASYNC_pause_job()) {
+ TSError(PCP "ASYNC_pause_job failed");
+ return;
+ }
+
+ fprintf(stderr, PCP "resumed OpenSSL async job\n");
+}
+
+int
+handle_ssl_cert(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata)
+{
+ TSVConn ssl_vc = static_cast<TSVConn>(edata);
+
+ if (event == TS_EVENT_SSL_CERT) {
+ pause_current_async_job(ssl_vc);
+ } else {
+ TSVConnReenable(ssl_vc);
+ }
+ return TS_SUCCESS;
+}
+
+} // namespace
+
+void
+TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
+{
+ TSPluginRegistrationInfo info;
+
+ info.plugin_name = PLUGIN_NAME;
+ info.vendor_name = "Apache Software Foundation";
+ info.support_email = "dev@trafficserver.apache.org";
+
+ if (TS_SUCCESS != TSPluginRegister(&info)) {
+ TSError(PCP "registration failed");
+ return;
+ }
+
+ TSCont cb_cert = TSContCreate(handle_ssl_cert, TSMutexCreate());
+ if (cb_cert == nullptr) {
+ TSError(PCP "failed to create SSL cert hook");
+ return;
+ }
+
+ TSHttpHookAdd(TS_SSL_CERT_HOOK, cb_cert);
+ Dbg(dbg_ctl, "plugin online");
+}
(trafficserver) 26/33: Fix server entry cleanup after request tunnel setup (#13295)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 70a24482442395d7dea8f408b61d38e614b92dae
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Jun 23 12:56:05 2026 -0500
Fix server entry cleanup after request tunnel setup (#13295)
A crash was observed while an HTTP/2 stream finished sending an
origin request header and started the client request body tunnel:
#0 HttpSM::state_send_server_request_header(...)
at src/proxy/http/HttpSM.cc:2176
#1 HttpSM::main_handler(...)
at src/proxy/http/HttpSM.cc:2731
#5 Http2Stream::signal_write_event(...)
at src/proxy/http2/Http2Stream.cc:954
The core had server_entry == nullptr at the vc_type check after the
request-body tunnel setup. The tunnel can synchronously abort the chain
and clean up the server VC before returning.
This checks whether the server entry still exists after setting up the
request body tunnel or transform tunnel. When setup already cleaned it
up, the state handler stops before installing a response-read handler
on a null entry.
(cherry picked from commit a8daf9994faba16c3b4cbfe21e724d048124731f)
---
src/proxy/http/HttpSM.cc | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 33480ae4a9..d884a639d5 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -2227,6 +2227,11 @@ HttpSM::state_send_server_request_header(int event, void *data)
}
}
}
+ // The request body tunnel setup can synchronously abort the chain, which
+ // cleans up server_entry before returning here.
+ if (server_entry == nullptr) {
+ break;
+ }
// Any other events to these read response
if (server_entry->vc_type == HttpVC_t::SERVER_VC) {
server_entry->vc_read_handler = &HttpSM::state_read_server_response_header;
(trafficserver) 25/33: dns: destruct HostEnt on free to fix SRV vector leak (#13307)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 1020680f40ab21c68371f9adee5e0f2a0468c11d
Author: Mo Chen <mo...@apache.org>
AuthorDate: Mon Jun 22 17:43:27 2026 -0500
dns: destruct HostEnt on free to fix SRV vector leak (#13307)
dnsBufAllocator used ClassAllocator<HostEnt, false>, so freeing a
HostEnt skipped its destructor. HostEnt embeds an SRVHosts whose
std::vector<SRV> owns heap storage, and alloc() placement-news a
fresh empty vector over the recycled block, orphaning the previous
buffer. Each recycle of a HostEnt that held SRV records leaked one
vector buffer.
Use the default Destruct_on_free=true so ~HostEnt runs on free.
(cherry picked from commit 9b098746d75c6c88a35c519f48b45fb901b0f348)
---
src/iocore/dns/DNS.cc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/iocore/dns/DNS.cc b/src/iocore/dns/DNS.cc
index 4d107238e9..09c0e0f8cf 100644
--- a/src/iocore/dns/DNS.cc
+++ b/src/iocore/dns/DNS.cc
@@ -94,7 +94,7 @@ DNSProcessor dnsProcessor;
ClassAllocator<DNSEntry, false> dnsEntryAllocator("dnsEntryAllocator");
// Users are expected to free these entries in short order!
// We could page align this buffer to enable page flipping for recv...
-ClassAllocator<HostEnt, false> dnsBufAllocator("dnsBufAllocator", 2);
+ClassAllocator<HostEnt> dnsBufAllocator("dnsBufAllocator", 2);
//
// Function Prototypes
(trafficserver) 02/33: Fix set-status crash inside if/endif at remap time (#13052)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 0246aec026c564f794e54a0fc2ac8927346c781b
Author: Mo Chen <mo...@apache.org>
AuthorDate: Tue Jun 23 16:59:35 2026 -0500
Fix set-status crash inside if/endif at remap time (#13052)
(cherry picked from commit bdb24cfd93e1c6e9a27a4d03b126121d085596da)
---
include/proxy/hdrs/HTTP.h | 4 +--
plugins/header_rewrite/operators.cc | 18 +++++--------
plugins/header_rewrite/ruleset.cc | 4 +--
plugins/header_rewrite/statement.cc | 10 ++-----
plugins/header_rewrite/statement.h | 10 ++-----
src/proxy/hdrs/HTTP.cc | 4 +--
.../header_rewrite_bundle.replay.yaml | 31 ++++++++++++++++++++++
.../header_rewrite/rules/set_status_in_if.conf | 27 +++++++++++++++++++
8 files changed, 74 insertions(+), 34 deletions(-)
diff --git a/include/proxy/hdrs/HTTP.h b/include/proxy/hdrs/HTTP.h
index 104743e593..571bd0fdc5 100644
--- a/include/proxy/hdrs/HTTP.h
+++ b/include/proxy/hdrs/HTTP.h
@@ -1097,7 +1097,7 @@ inline void
HTTPHdr::status_set(HTTPStatus status)
{
ink_assert(valid());
- ink_assert(m_http->m_polarity == HTTPType::RESPONSE);
+ ink_release_assert(m_http->m_polarity == HTTPType::RESPONSE);
http_hdr_status_set(m_http, status);
}
@@ -1121,7 +1121,7 @@ inline void
HTTPHdr::reason_set(std::string_view value)
{
ink_assert(valid());
- ink_assert(m_http->m_polarity == HTTPType::RESPONSE);
+ ink_release_assert(m_http->m_polarity == HTTPType::RESPONSE);
http_hdr_reason_set(m_heap, m_http, value, true);
}
diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc
index 20207bd24b..b64b4ec469 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -198,19 +198,13 @@ OperatorSetStatus::initialize_hooks()
bool
OperatorSetStatus::exec(const Resources &res) const
{
- switch (get_hook()) {
- case TS_HTTP_READ_RESPONSE_HDR_HOOK:
- case TS_HTTP_SEND_RESPONSE_HDR_HOOK:
- if (res.bufp && res.hdr_loc) {
- TSHttpHdrStatusSet(res.bufp, res.hdr_loc, static_cast<TSHttpStatus>(_status.get_int_value()), res.state.txnp, PLUGIN_NAME);
- if (_reason && _reason_len > 0) {
- TSHttpHdrReasonSet(res.bufp, res.hdr_loc, _reason, _reason_len);
- }
+ if (res.bufp && res.hdr_loc && TSHttpHdrTypeGet(res.bufp, res.hdr_loc) == TS_HTTP_TYPE_RESPONSE) {
+ TSHttpHdrStatusSet(res.bufp, res.hdr_loc, static_cast<TSHttpStatus>(_status.get_int_value()), res.state.txnp, PLUGIN_NAME);
+ if (_reason && _reason_len > 0) {
+ TSHttpHdrReasonSet(res.bufp, res.hdr_loc, _reason, _reason_len);
}
- break;
- default:
+ } else {
TSHttpTxnStatusSet(res.state.txnp, static_cast<TSHttpStatus>(_status.get_int_value()), PLUGIN_NAME);
- break;
}
Dbg(pi_dbg_ctl, "OperatorSetStatus::exec() invoked with status=%d", _status.get_int_value());
@@ -601,7 +595,7 @@ OperatorSetRedirect::exec(const Resources &res) const
const_cast<Resources &>(res).changed_url = true;
res._rri->redirect = 1;
} else {
- Dbg(pi_dbg_ctl, "OperatorSetRedirect::exec() hook=%d", int(get_hook()));
+ Dbg(pi_dbg_ctl, "OperatorSetRedirect::exec() redirect to %s", value.c_str());
// Set the new status code and reason.
TSHttpStatus status = static_cast<TSHttpStatus>(_status.get_int_value());
TSHttpHdrStatusSet(res.bufp, res.hdr_loc, status, res.state.txnp, PLUGIN_NAME);
diff --git a/plugins/header_rewrite/ruleset.cc b/plugins/header_rewrite/ruleset.cc
index 28fda37a51..ba17407f20 100644
--- a/plugins/header_rewrite/ruleset.cc
+++ b/plugins/header_rewrite/ruleset.cc
@@ -66,7 +66,7 @@ RuleSet::make_condition(Parser &p, const char *filename, int lineno)
Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s", p.get_op().c_str(), p.get_arg().c_str());
c->initialize(p);
- if (!c->set_hook(_hook)) {
+ if (!c->is_hook_valid(_hook)) {
delete c;
TSError("[%s] in %s:%d: can't use this condition in hook=%s: %%{%s} with arg: %s", PLUGIN_NAME, filename, lineno,
TSHttpHookNameLookup(_hook), p.get_op().c_str(), p.get_arg().c_str());
@@ -93,7 +93,7 @@ RuleSet::add_operator(Parser &p, const char *filename, int lineno)
if (nullptr != op) {
Dbg(pi_dbg_ctl, " Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(), p.get_arg().c_str(), p.get_value().c_str());
op->initialize(p);
- if (!op->set_hook(_hook)) {
+ if (!op->is_hook_valid(_hook)) {
delete op;
Dbg(pi_dbg_ctl, "in %s:%d: can't use this operator in hook=%s: %s(%s)", filename, lineno, TSHttpHookNameLookup(_hook),
p.get_op().c_str(), p.get_arg().c_str());
diff --git a/plugins/header_rewrite/statement.cc b/plugins/header_rewrite/statement.cc
index 8bce404a2d..351f5d572e 100644
--- a/plugins/header_rewrite/statement.cc
+++ b/plugins/header_rewrite/statement.cc
@@ -66,15 +66,9 @@ Statement::get_resource_ids() const
}
bool
-Statement::set_hook(TSHttpHookID hook)
+Statement::is_hook_valid(TSHttpHookID hook) const
{
- bool ret = std::find(_allowed_hooks.begin(), _allowed_hooks.end(), hook) != _allowed_hooks.end();
-
- if (ret) {
- _hook = hook;
- }
-
- return ret;
+ return std::find(_allowed_hooks.begin(), _allowed_hooks.end(), hook) != _allowed_hooks.end();
}
// This should be overridden for any Statement which only supports some hooks
diff --git a/plugins/header_rewrite/statement.h b/plugins/header_rewrite/statement.h
index 1f16c24246..a45974b84b 100644
--- a/plugins/header_rewrite/statement.h
+++ b/plugins/header_rewrite/statement.h
@@ -152,13 +152,8 @@ public:
Statement(const Statement &) = delete;
void operator=(const Statement &) = delete;
- // Which hook are we adding this statement to?
- bool set_hook(TSHttpHookID hook);
- TSHttpHookID
- get_hook() const
- {
- return _hook;
- }
+ // Validate that this statement is allowed on the given hook. Used during parsing only.
+ bool is_hook_valid(TSHttpHookID hook) const;
// Which hooks are this "statement" applicable for? Used during parsing only.
void
@@ -275,7 +270,6 @@ protected:
private:
ResourceIDs _rsrc = RSRC_NONE;
- TSHttpHookID _hook = TS_HTTP_READ_RESPONSE_HDR_HOOK;
std::vector<TSHttpHookID> _allowed_hooks;
bool _initialized = false;
};
diff --git a/src/proxy/hdrs/HTTP.cc b/src/proxy/hdrs/HTTP.cc
index b67534d6de..62b6357421 100644
--- a/src/proxy/hdrs/HTTP.cc
+++ b/src/proxy/hdrs/HTTP.cc
@@ -693,7 +693,7 @@ http_hdr_url_set(HdrHeap *heap, HTTPHdrImpl *hh, URLImpl *url)
void
http_hdr_status_set(HTTPHdrImpl *hh, HTTPStatus status)
{
- ink_assert(hh->m_polarity == HTTPType::RESPONSE);
+ ink_release_assert(hh->m_polarity == HTTPType::RESPONSE);
hh->u.resp.m_status = static_cast<int16_t>(status);
}
@@ -713,7 +713,7 @@ http_hdr_reason_get(HTTPHdrImpl *hh)
void
http_hdr_reason_set(HdrHeap *heap, HTTPHdrImpl *hh, std::string_view value, bool must_copy)
{
- ink_assert(hh->m_polarity == HTTPType::RESPONSE);
+ ink_release_assert(hh->m_polarity == HTTPType::RESPONSE);
mime_str_u16_set(heap, value, &(hh->u.resp.m_ptr_reason), &(hh->u.resp.m_len_reason), must_copy);
}
diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
index b444acaae8..af07d37972 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
+++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
@@ -176,6 +176,13 @@ autest:
args:
- "rules/rule_session_vars.conf"
+ - from: "http://www.example.com/from_18/"
+ to: "http://backend.ex:{SERVER_HTTP_PORT}/to_18/"
+ plugins:
+ - name: "header_rewrite.so"
+ args:
+ - "rules/set_status_in_if.conf"
+
metric_checks:
- metric: "proxy.process.plugin.header_rewrite.operators"
min: 1
@@ -1942,3 +1949,27 @@ sessions:
headers:
fields:
- [ X-Session-Seen, { value: "yes", as: equal } ]
+
+# Test 67: set-status inside if/endif at REMAP_PSEUDO_HOOK time.
+# Before the fix, the set-status operator inside an if/endif block retained
+# its default _hook (TS_HTTP_READ_RESPONSE_HDR_HOOK), so exec() called
+# TSHttpHdrStatusSet on a request buffer, corrupting the union and crashing.
+- transactions:
+ - client-request:
+ method: "GET"
+ version: "1.1"
+ url: /from_18/test
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ uuid, 67 ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Connection, close ]
+
+ proxy-response:
+ status: 403
diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf
new file mode 100644
index 0000000000..f8b2b56cc0
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+# Minimal reproduction for set-status inside if/endif at remap time.
+# Before the fix, the operator's _hook defaulted to
+# TS_HTTP_READ_RESPONSE_HDR_HOOK, so exec() called TSHttpHdrStatusSet
+# on a request buffer, corrupting the header union and crashing.
+#
+cond %{REMAP_PSEUDO_HOOK}
+ if
+ cond %{CLIENT-URL:PATH} /from_18/
+ set-status 403
+ endif
(trafficserver) 23/33: ci: modernize the coverage helper script (#13305)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 245a0a78d87f74fd965accdb0dbc5b445f42ac71
Author: Mo Chen <mo...@apache.org>
AuthorDate: Mon Jun 22 22:17:49 2026 -0500
ci: modernize the coverage helper script (#13305)
Rework ci/coverage to drive an out-of-source CMake build and an optional
autest run (AUTEST='*/tls/*' or AUTEST=all) when gathering coverage, and
report via gcov/gcovr.
(cherry picked from commit 4c69f8582b3005fca44c79d5fc0f189f032ddb72)
---
ci/coverage | 178 +++++++++++++++++++++++++++++++++---------------------------
1 file changed, 98 insertions(+), 80 deletions(-)
diff --git a/ci/coverage b/ci/coverage
index 29b9aac0b4..97a83ffbba 100755
--- a/ci/coverage
+++ b/ci/coverage
@@ -16,93 +16,111 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-LCOV=${LCOV:-lcov}
-GENHTML=${GENHTML:-genhtml}
-
-TMPDIR=${TMPDIR:-/tmp}
-BUILDID="org.apache.trafficserver.$$"
-
-SRCROOT=${SRCROOT:-$(cd $(dirname $0)/.. && pwd)} # where the source lives
-OBJROOT=${OBJROOT:-"$TMPDIR/$BUILDID/obj"} # where we are building
-DSTROOT=${DSTROOT:-"$TMPDIR/$BUILDID/dst"} # where we are installing
-
-# Force low make parallelization so that the build can complete in a VM with
-# only a small amount of memory.
-NPROCS=${NPROCS:-2}
-
-mkdir -p $SRCROOT
-mkdir -p $OBJROOT
-mkdir -p $DSTROOT
-
-autogen() {
- (
- cd "$SRCROOT"
- [ configure -nt configure.ac -a Makefile.in -nt Makefile.am ] || autoreconf -fi
- )
-}
+# Build ATS with gcov instrumentation, run the unit tests (and optionally
+# autests), and produce a coverage report with gcovr.
+#
+# Usage:
+# ci/coverage [extra cmake args...]
+#
+# Environment overrides:
+# BUILDDIR build tree (default: $SRCROOT/build-coverage)
+# PREFIX install prefix (default: /tmp/ts-coverage)
+# NPROCS build parallelism (default: nproc)
+# GCOVR gcovr command (default: gcovr, falls back to uvx gcovr)
+# AUTEST run autests too if set to a -f/--filter basename glob,
+# e.g. AUTEST='tls_*' or AUTEST=all for the whole suite
+#
+# Requires: cmake, ninja or make, gcc/gcov, and gcovr (pip install gcovr).
+
+set -e
+
+SRCROOT=${SRCROOT:-$(cd "$(dirname "$0")"/.. && pwd)}
+BUILDDIR=${BUILDDIR:-$SRCROOT/build-coverage}
+PREFIX=${PREFIX:-/tmp/ts-coverage}
+NPROCS=${NPROCS:-$(nproc 2>/dev/null || echo 4)}
+
+GCOVR=${GCOVR:-gcovr}
+if ! command -v "${GCOVR%% *}" >/dev/null 2>&1; then
+ if command -v uvx >/dev/null 2>&1; then
+ GCOVR="uvx gcovr"
+ else
+ echo "gcovr not found; install it with 'pip install gcovr'" >&2
+ exit 1
+ fi
+fi
+
+# --coverage instruments compile and link. Atomic profile updates keep the
+# counters sane in ATS's heavily threaded runtime, and absolute paths in the
+# notes files let gcovr resolve sources from any working directory.
+COVERAGE_FLAGS="-g -O0 --coverage -fprofile-update=atomic -fprofile-abs-path"
configure() {
- (
- cd $OBJROOT
- $SRCROOT/configure \
- --prefix=$DSTROOT \
- --enable-debug \
- --enable-coverage \
- --enable-werror \
- --enable-example-plugins \
- --enable-test-tools \
- --enable-experimental-plugins \
- CC="$CC" \
- CXX="$CXX" \
- "$@"
- )
+ # Enabling autest pulls in Python3/uv/nc and the proxy-verifier toolchain at
+ # configure time, so only ask for it when AUTEST actually selects a run.
+ local autest_args=()
+ if [ -n "$AUTEST" ]; then
+ autest_args=(-DENABLE_AUTEST=ON)
+ fi
+
+ cmake -S "$SRCROOT" -B "$BUILDDIR" \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_INSTALL_PREFIX="$PREFIX" \
+ -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \
+ -DBUILD_TESTING=ON \
+ "${autest_args[@]}" \
+ -DBUILD_EXPERIMENTAL_PLUGINS=ON \
+ -DENABLE_EXAMPLE=ON \
+ -DCMAKE_CXX_FLAGS_DEBUG="$COVERAGE_FLAGS" \
+ -DCMAKE_C_FLAGS_DEBUG="$COVERAGE_FLAGS" \
+ "$@"
}
build() {
- ( cd $OBJROOT && $MAKE -j $NPROCS )
- ( cd $OBJROOT && $MAKE install )
+ cmake --build "$BUILDDIR" -j "$NPROCS"
+ cmake --install "$BUILDDIR"
}
-regress() {
- ( cd $OBJROOT && $MAKE check ) && \
- $DSTROOT/bin/traffic_server -k -K -R 1
+run_tests() {
+ # Reset counters so the report reflects exactly this run.
+ find "$BUILDDIR" -name '*.gcda' -delete
+
+ ctest --test-dir "$BUILDDIR" -j "$NPROCS"
+
+ if [ -n "$AUTEST" ]; then
+ local filter_args=()
+ if [ "$AUTEST" != all ]; then
+ # Quote the pattern: the parallel runner fnmatches it against each test's
+ # basename (e.g. 'tls_*'), so it must reach autest verbatim rather than be
+ # glob-expanded by the shell first.
+ filter_args=(-f "$AUTEST")
+ fi
+ # Match ctest's parallelism: -j routes autest.sh through autest-parallel.py,
+ # which already aims --sandbox at a per-build-tree path under /tmp, so
+ # concurrent coverage runs in separate build trees don't trample each other.
+ (cd "$BUILDDIR/tests" && ./autest.sh -j "$NPROCS" "${filter_args[@]}")
+ fi
}
-CC=${CC:-gcc}
-CXX=${CXX:-g++}
-MAKE=${MAKE:-make}
-export CC CXX MAKE
-
-case $VERBOSE in
- Y*) set -x ;;
- y*) set -x ;;
- 1) set -x ;;
- *) set +x ;;
-esac
-
-autogen || exit 1
-configure "$@" || exit 1
-build || exit 1
-
-$LCOV --quiet --capture --initial --directory $OBJROOT --output-file initial.info
-
-regress
-
-$LCOV --quiet --capture --directory $OBJROOT --output-file tests.info
-
-# The --add-tracefile option refuses to create an output file with
-# --output-file (contrary to documentation). Capture the combined
-# coverage from stdout instead.
-$LCOV \
- --add-tracefile initial.info \
- --add-tracefile tests.info \
-> combined.info
-
-# genhtml will puke because it can't find the original TSConfig files.
-# We don't need to bother generation anything for /usr/include.
-$LCOV --remove combined.info \
- 'TsConfigSyntax.*' \
- 'TsConfigGrammar.*' \
- '/usr/include/*' > coverage.info
+report() {
+ cd "$SRCROOT"
+ # no_working_dir_found: vendored/generated objects whose recorded cwd is
+ # gone. merge-use-line-min: sources compiled into both a library and a
+ # unit-test binary produce slightly different function records.
+ # suspicious_hits: hot-path counters legitimately reach billions of hits
+ # over a full test run, tripping gcovr's corruption heuristic (gcc #68080).
+ $GCOVR -r . "$BUILDDIR" -j "$NPROCS" \
+ --gcov-ignore-errors=no_working_dir_found \
+ --gcov-ignore-parse-errors=suspicious_hits.warn_once_per_file \
+ --merge-mode-functions=merge-use-line-min \
+ --exclude 'lib/' --exclude '.*/unit_tests/' --exclude 'build.*/' \
+ --print-summary \
+ --html-details coverage.html \
+ --json coverage.json \
+ --xml coverage.xml
+ echo "Reports written: coverage.html coverage.json coverage.xml"
+}
-$GENHTML --output-directory coverage.html coverage.info
+configure "$@"
+build
+run_tests
+report
(trafficserver) 16/33: TLS: count handshake signatures by key type (#13289)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit ddc283c280b99965cc9b44a611d8acc6b987f181
Author: Mo Chen <mo...@apache.org>
AuthorDate: Fri Jun 19 17:45:42 2026 -0500
TLS: count handshake signatures by key type (#13289)
A full TLS handshake runs an asymmetric signature whose cost depends on the
server key type -- RSA being far heavier than ECDSA. Count those signatures
by key type to make that work visible; resumed handshakes skip the signature.
Also add proxy.process.ssl.connections_closed, incremented once per SSL_free,
to track TLS connection teardown volume.
New counters:
* proxy.process.ssl.handshake_sign_rsa / _ecdsa / _other
* proxy.process.ssl.connections_closed
(cherry picked from commit 7766ce2cc9a8e58a247201d6a3ce7b1d3e895a59)
---
src/iocore/net/SSLNetVConnection.cc | 6 ++++++
src/iocore/net/SSLStats.cc | 4 ++++
src/iocore/net/SSLStats.h | 4 ++++
src/iocore/net/TLSBasicSupport.cc | 22 ++++++++++++++++++++++
4 files changed, 36 insertions(+)
diff --git a/src/iocore/net/SSLNetVConnection.cc b/src/iocore/net/SSLNetVConnection.cc
index f92d29528f..b14462a4da 100644
--- a/src/iocore/net/SSLNetVConnection.cc
+++ b/src/iocore/net/SSLNetVConnection.cc
@@ -990,6 +990,12 @@ SSLNetVConnection::clear()
client_sess.reset();
if (ssl != nullptr) {
+ // clear() runs from free() once per VC recycle, so this is the single chokepoint where a TLS
+ // connection's SSL object is torn down -- count it as one connection close here. Blind-tunnel
+ // conversions free their SSL earlier (before any data) and continue as a tunnel rather than a
+ // close, so by here ssl is already null for them and they are not counted (they are tracked by
+ // the tunnel metrics instead).
+ Metrics::Counter::increment(ssl_rsb.connections_closed);
SSL_free(ssl);
ssl = nullptr;
}
diff --git a/src/iocore/net/SSLStats.cc b/src/iocore/net/SSLStats.cc
index 0b9b94343d..20ddb97e85 100644
--- a/src/iocore/net/SSLStats.cc
+++ b/src/iocore/net/SSLStats.cc
@@ -262,6 +262,10 @@ SSLInitializeStatistics()
ssl_rsb.total_sslv3 = Metrics::Counter::createPtr("proxy.process.ssl.ssl_total_sslv3");
ssl_rsb.total_success_handshake_count_in = Metrics::Counter::createPtr("proxy.process.ssl.total_success_handshake_count_in");
ssl_rsb.total_success_handshake_count_out = Metrics::Counter::createPtr("proxy.process.ssl.total_success_handshake_count_out");
+ ssl_rsb.handshake_sign_rsa = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_rsa");
+ ssl_rsb.handshake_sign_ecdsa = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_ecdsa");
+ ssl_rsb.handshake_sign_other = Metrics::Counter::createPtr("proxy.process.ssl.handshake_sign_other");
+ ssl_rsb.connections_closed = Metrics::Counter::createPtr("proxy.process.ssl.connections_closed");
ssl_rsb.total_ticket_keys_renewed = Metrics::Counter::createPtr("proxy.process.ssl.total_ticket_keys_renewed");
ssl_rsb.total_tickets_created = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_created");
ssl_rsb.total_tickets_not_found = Metrics::Counter::createPtr("proxy.process.ssl.total_tickets_not_found");
diff --git a/src/iocore/net/SSLStats.h b/src/iocore/net/SSLStats.h
index 495092b7fb..c6a3b32bce 100644
--- a/src/iocore/net/SSLStats.h
+++ b/src/iocore/net/SSLStats.h
@@ -86,6 +86,10 @@ struct SSLStatsBlock {
Metrics::Counter::AtomicType *total_sslv3 = nullptr;
Metrics::Counter::AtomicType *total_success_handshake_count_in = nullptr;
Metrics::Counter::AtomicType *total_success_handshake_count_out = nullptr;
+ Metrics::Counter::AtomicType *handshake_sign_rsa = nullptr;
+ Metrics::Counter::AtomicType *handshake_sign_ecdsa = nullptr;
+ Metrics::Counter::AtomicType *handshake_sign_other = nullptr;
+ Metrics::Counter::AtomicType *connections_closed = nullptr;
Metrics::Counter::AtomicType *total_ticket_keys_renewed = nullptr;
Metrics::Counter::AtomicType *total_tickets_created = nullptr;
Metrics::Counter::AtomicType *total_tickets_not_found = nullptr;
diff --git a/src/iocore/net/TLSBasicSupport.cc b/src/iocore/net/TLSBasicSupport.cc
index 5d7187da4a..897e406741 100644
--- a/src/iocore/net/TLSBasicSupport.cc
+++ b/src/iocore/net/TLSBasicSupport.cc
@@ -319,6 +319,28 @@ TLSBasicSupport::_update_end_of_handshake_stats()
{
Metrics::Counter::increment(ssl_rsb.total_success_handshake_count_in);
+ // Only a full handshake runs the asymmetric signature (a resumed one skips it); count those
+ // signatures by the server private-key type.
+ {
+ const SSL *ssl = this->_get_ssl_object();
+ if (ssl != nullptr && !SSL_session_reused(ssl)) {
+ if (EVP_PKEY *pkey = SSL_get_privatekey(ssl); pkey != nullptr) {
+ switch (EVP_PKEY_id(pkey)) {
+ case EVP_PKEY_RSA:
+ case EVP_PKEY_RSA_PSS:
+ Metrics::Counter::increment(ssl_rsb.handshake_sign_rsa);
+ break;
+ case EVP_PKEY_EC:
+ Metrics::Counter::increment(ssl_rsb.handshake_sign_ecdsa);
+ break;
+ default:
+ Metrics::Counter::increment(ssl_rsb.handshake_sign_other);
+ break;
+ }
+ }
+ }
+ }
+
#if defined(OPENSSL_IS_BORINGSSL)
SSL *ssl = this->_get_ssl_object();
uint16_t group_id = SSL_get_group_id(ssl);
(trafficserver) 32/33: HttpSM.cc: fix cache read end milestone order (#13325)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 5336b5c43d1b3b052e709d2e311476276af77448
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Jun 24 20:39:52 2026 -0500
HttpSM.cc: fix cache read end milestone order (#13325)
The log-milestone-fields AuTest intermittently logged a valid
TCP_MEM_HIT with hit_proc rendered as unset. That field is computed from
TS_MILESTONE_UA_BEGIN_WRITE minus TS_MILESTONE_CACHE_OPEN_READ_END, so
the failing logs showed the client write-begin milestone could be
observed before the cache open-read end milestone on a fast cache-hit
path.
The cache had already delivered the open-read result in
state_cache_open_read(), so that ordering pointed at milestone stamping
rather than an actual cache sequencing problem.
TS_MILESTONE_CACHE_OPEN_READ_END was marked after cache-open-read state
handling, and that handling can advance into cache serving and stamp
TS_MILESTONE_UA_BEGIN_WRITE first.
This records TS_MILESTONE_CACHE_OPEN_READ_END in the terminal
cache-open-read handling paths before they can advance to later
transaction milestones. The compatibility-key retry path is left
unstamped so the retried lookup owns a coherent cache-read begin/end
pair.
(cherry picked from commit 8f33dbf76b81e82ff541af58457619ed95e26f89)
---
src/proxy/http/HttpSM.cc | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index b1afc073e1..749e271c1f 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -2689,6 +2689,11 @@ HttpSM::state_cache_open_read(int event, void *data)
Metrics::Counter::increment(http_rsb.cache_compat_key_reads);
}
+ // Record the cache open-read completion before state handling can advance
+ // into cache serving and stamp later transaction milestones.
+ ATS_PROBE1(milestone_cache_open_read_end, sm_id);
+ milestones[TS_MILESTONE_CACHE_OPEN_READ_END] = ink_get_hrtime();
+
ink_assert(t_state.cache_info.object_read != nullptr);
call_transact_and_set_next_state(HttpTransact::HandleCacheOpenRead);
break;
@@ -2715,6 +2720,11 @@ HttpSM::state_cache_open_read(int event, void *data)
t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::MISS;
}
+ // Record the final cache open-read completion after any compatibility-key
+ // retry has been ruled out.
+ ATS_PROBE1(milestone_cache_open_read_end, sm_id);
+ milestones[TS_MILESTONE_CACHE_OPEN_READ_END] = ink_get_hrtime();
+
ink_assert(t_state.transact_return_point == nullptr);
t_state.transact_return_point = HttpTransact::HandleCacheOpenRead;
@@ -2739,9 +2749,6 @@ HttpSM::state_cache_open_read(int event, void *data)
break;
}
- ATS_PROBE1(milestone_cache_open_read_end, sm_id);
- milestones[TS_MILESTONE_CACHE_OPEN_READ_END] = ink_get_hrtime();
-
return 0;
}
(trafficserver) 22/33: tests: give cripts ATS startup a longer readiness window (#13304)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 4ee1a8b9dd354881bdeb327e1531926545de2f99
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 08:46:43 2026 -0500
tests: give cripts ATS startup a longer readiness window (#13304)
(cherry picked from commit a17e04af3b5c7bf6803daecd579fa40fda038ab8)
---
tests/gold_tests/autest-site/trafficserver.test.ext | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext
index a41f8948ce..221aa16be1 100755
--- a/tests/gold_tests/autest-site/trafficserver.test.ext
+++ b/tests/gold_tests/autest-site/trafficserver.test.ext
@@ -419,6 +419,9 @@ def MakeATSProcess(
})
# ATS_ROOT is used by compiler.sh to find the ATS install directory.
p.Env['ATS_ROOT'] = p.Variables.PREFIX
+ # Cripts are compiled at startup, which can exceed the default 10s readiness window
+ # under CI load and flake the "fully initialized" wait. Give cripts startup more room.
+ p.StartupTimeout = 60
# The following message was added so that tests and users can know when
# Traffic Server is ready to both receive and optimize traffic.
(trafficserver) 07/33: Use ls-hpack's fast Huffman decoder for HPACK/QPACK strings (#13259)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit af98f1ba15f65a0f14fbf2780005edff80092734
Author: Phong Nguyen <ph...@gmail.com>
AuthorDate: Wed Jun 24 10:34:59 2026 -0700
Use ls-hpack's fast Huffman decoder for HPACK/QPACK strings (#13259)
When the home-grown Huffman codec was replaced with vendored LiteSpeed ls-hpack code (#12357), only the conservative 4-bit FSM decoder (lshpack_dec_huff_decode_full) was ported — although huff-tables.h has carried the 64K-entry table for upstream's fast decoder all along. This PR ports the fast decoder (lshpack_dec_huff_decode, from ls-hpack v2.3.5) and switches huffman_decode() to it.
The fast decoder consumes 16 bits of input per table lookup and emits up to 3 bytes, falling back to the FSM decoder for the rare codes longer than 16 bits. HPACK and QPACK share the wrapper through xpack_decode_string(), so both HTTP/2 and HTTP/3 header decoding benefit. No new memory footprint: the hdecs table has been compiled into the binary since the original vendoring.
(cherry picked from commit b8ada6fe06dd4e2161d8480a1a1fc75346a43153)
---
include/proxy/hdrs/HuffmanCodec.h | 10 ++
lib/ls-hpack/README.md | 16 ++-
lib/ls-hpack/lshpack.cc | 190 +++++++++++++++++++++++++-
lib/ls-hpack/lshpack.h | 6 +
src/proxy/hdrs/CMakeLists.txt | 2 +
src/proxy/hdrs/HuffmanCodec.cc | 4 +-
src/proxy/hdrs/unit_tests/test_Huffmancode.cc | 137 +++++++++++++++++++
tools/benchmark/CMakeLists.txt | 4 +
tools/benchmark/benchmark_HuffmanDecode.cc | 142 +++++++++++++++++++
9 files changed, 507 insertions(+), 4 deletions(-)
diff --git a/include/proxy/hdrs/HuffmanCodec.h b/include/proxy/hdrs/HuffmanCodec.h
index e979bdc315..9d1aa7316f 100644
--- a/include/proxy/hdrs/HuffmanCodec.h
+++ b/include/proxy/hdrs/HuffmanCodec.h
@@ -25,5 +25,15 @@
#include <cstdint>
+/** Decode a Huffman-encoded string per RFC 7541 section 5.2.
+
+ @return The decoded length, or a negative value on invalid input or
+ insufficient destination space.
+
+ @note dst_len must be strictly greater than the decoded length; with an
+ exactly-sized destination the decoder may report insufficient space.
+ Huffman expands to at most 8/5 of the encoded length, so sizing dst at
+ 2x src_len always suffices (see xpack_decode_string).
+ */
int64_t huffman_decode(char *dst, uint32_t dst_len, uint8_t const *src, uint32_t src_len);
int64_t huffman_encode(uint8_t *dst, uint32_t dst_len, uint8_t const *src, uint32_t src_len);
diff --git a/lib/ls-hpack/README.md b/lib/ls-hpack/README.md
index b6ba2c769f..2b7ccb1948 100644
--- a/lib/ls-hpack/README.md
+++ b/lib/ls-hpack/README.md
@@ -23,4 +23,18 @@ Huffman decoding acceleration.
The current implementation pulled into ATS is based upon what is currently the
latest version,
-[v2.3.4](https://github.com/litespeedtech/ls-hpack/releases/tag/v2.3.4).
+[v2.3.5](https://github.com/litespeedtech/ls-hpack/releases/tag/v2.3.5).
+
+# ATS Modifications
+
+The code is kept as close to upstream as practical, with one deliberate
+behavioral divergence that any future re-sync must preserve:
+
+- `lshpack_dec_huff_decode` (the fast decoder) rejects Huffman padding of 8 or
+ more bits, as required by RFC 7541 section 5.2. Upstream's fast decoder
+ accepts such padding when it follows the final symbol near the end of the
+ input; the 4-bit FSM decoder (`lshpack_dec_huff_decode_full`) has always
+ rejected it. See the commented tail check in `lshpack.cc` and the
+ `decode_overlong_padding` test in
+ `src/proxy/hdrs/unit_tests/test_Huffmancode.cc`, which fails if the check is
+ dropped.
diff --git a/lib/ls-hpack/lshpack.cc b/lib/ls-hpack/lshpack.cc
index 6e7f0ee0e6..b1d4167c65 100644
--- a/lib/ls-hpack/lshpack.cc
+++ b/lib/ls-hpack/lshpack.cc
@@ -52,6 +52,9 @@ SOFTWARE.
*/
#include "huff-tables.h"
+#include "lshpack.h"
+#include <cassert>
+#include <cstddef>
#include <cstdint>
#include <cstring>
@@ -61,7 +64,7 @@ namespace litespeed {
namespace
{
-constexpr int64_t LSHPACK_ERR_MORE_BUF = -3;
+constexpr unsigned SHORTEST_CODE = 5;
struct decode_status {
uint8_t state;
@@ -243,4 +246,189 @@ lshpack_dec_huff_decode_full(uint8_t const *src, uint32_t src_len, char *dst, ui
return p_dst - dst;
}
+// Implementation taken from LiteSpeed:
+// lshpack_dec_huff_decode
+//
+// The decoder is optimized for the common case. Most of the time, we decode
+// data whose encoding is 16 bits or shorter. This lets us use a 64 KB table
+// indexed by two bytes of input and outputs 1, 2, or 3 bytes at a time.
+//
+// In the case a longer code is encountered, we fall back to the original
+// Huffman decoder that supports all code lengths.
+int64_t
+lshpack_dec_huff_decode(uint8_t const *src, uint32_t src_len, char *dst, uint32_t dst_len)
+{
+ char *const orig_dst = dst;
+ uint8_t const *const src_end = src + src_len;
+ char *const dst_end = dst + dst_len;
+ uintptr_t buf = 0;
+ unsigned avail_bits, len;
+ struct hdec hdec = {0, {0, 0, 0}};
+ uint16_t idx;
+ int64_t r;
+
+ avail_bits = 0;
+ while (true) {
+ if (src + sizeof(buf) <= src_end) {
+ len = (sizeof(buf) * 8 - avail_bits) >> 3;
+ avail_bits += len << 3;
+ switch (len) {
+#if UINTPTR_MAX == 18446744073709551615ull
+ case 8:
+ buf <<= 8;
+ buf |= static_cast<uintptr_t>(*src++);
+ [[fallthrough]];
+ case 7:
+ buf <<= 8;
+ buf |= static_cast<uintptr_t>(*src++);
+ [[fallthrough]];
+ default:
+ buf <<= 48;
+ buf |= static_cast<uintptr_t>(*src++) << 40;
+ buf |= static_cast<uintptr_t>(*src++) << 32;
+ buf |= static_cast<uintptr_t>(*src++) << 24;
+ buf |= static_cast<uintptr_t>(*src++) << 16;
+#else
+ [[fallthrough]];
+ case 4:
+ buf <<= 8;
+ buf |= static_cast<uintptr_t>(*src++);
+ [[fallthrough]];
+ case 3:
+ buf <<= 8;
+ buf |= static_cast<uintptr_t>(*src++);
+ [[fallthrough]];
+ default:
+ buf <<= 16;
+#endif
+ buf |= static_cast<uintptr_t>(*src++) << 8;
+ buf |= static_cast<uintptr_t>(*src++) << 0;
+ }
+ } else if (src < src_end) {
+ do {
+ buf <<= 8;
+ buf |= static_cast<uintptr_t>(*src++);
+ avail_bits += 8;
+ } while (src < src_end && avail_bits <= sizeof(buf) * 8 - 8);
+ } else {
+ break; // Normal case terminating condition: out of input
+ }
+
+ if (dst_end - dst >= static_cast<ptrdiff_t>(8 * sizeof(buf) / SHORTEST_CODE) && avail_bits >= 16) {
+ // Fast path: don't check destination bounds
+ do {
+ idx = static_cast<uint16_t>(buf >> (avail_bits - 16));
+ hdec = hdecs[idx];
+ dst[0] = static_cast<char>(hdec.out[0]);
+ dst[1] = static_cast<char>(hdec.out[1]);
+ dst[2] = static_cast<char>(hdec.out[2]);
+ dst += hdec.lens & 3;
+ avail_bits -= hdec.lens >> 2;
+ } while (avail_bits >= 16 && hdec.lens);
+ if (avail_bits < 16) {
+ continue;
+ }
+ goto slow_path;
+ } else {
+ while (avail_bits >= 16) {
+ idx = static_cast<uint16_t>(buf >> (avail_bits - 16));
+ hdec = hdecs[idx];
+ len = hdec.lens & 3;
+ if (len && dst + len <= dst_end) {
+ switch (len) {
+ case 3:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ *dst++ = static_cast<char>(hdec.out[1]);
+ *dst++ = static_cast<char>(hdec.out[2]);
+ break;
+ case 2:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ *dst++ = static_cast<char>(hdec.out[1]);
+ break;
+ default:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ break;
+ }
+ avail_bits -= hdec.lens >> 2;
+ } else if (dst + len > dst_end) {
+ r = dst_end - dst - static_cast<ptrdiff_t>(len);
+ if (r > LSHPACK_ERR_MORE_BUF) {
+ r = LSHPACK_ERR_MORE_BUF;
+ }
+ return r;
+ } else {
+ goto slow_path;
+ }
+ }
+ }
+ }
+
+ if (avail_bits >= SHORTEST_CODE) {
+ idx = static_cast<uint16_t>(buf << (16 - avail_bits));
+ idx |= (1 << (16 - avail_bits)) - 1; // EOF
+ if (idx == 0xFFFF && avail_bits < 8) {
+ goto end;
+ }
+ // If a byte or more of input is left, this means there is a valid
+ // encoding, not just EOF.
+ hdec = hdecs[idx];
+ len = hdec.lens & 3;
+ if ((static_cast<unsigned>(hdec.lens) >> 2) > avail_bits) {
+ return -1;
+ }
+ if (len && dst + len <= dst_end) {
+ switch (len) {
+ case 3:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ *dst++ = static_cast<char>(hdec.out[1]);
+ *dst++ = static_cast<char>(hdec.out[2]);
+ break;
+ case 2:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ *dst++ = static_cast<char>(hdec.out[1]);
+ break;
+ default:
+ *dst++ = static_cast<char>(hdec.out[0]);
+ break;
+ }
+ avail_bits -= hdec.lens >> 2;
+ } else if (dst + len > dst_end) {
+ r = dst_end - dst - static_cast<ptrdiff_t>(len);
+ if (r > LSHPACK_ERR_MORE_BUF) {
+ r = LSHPACK_ERR_MORE_BUF;
+ }
+ return r;
+ } else {
+ // This must be an invalid code, otherwise it would have fit
+ return -1;
+ }
+ }
+
+ if (avail_bits > 0) {
+ // ATS: unlike upstream ls-hpack, also reject padding of 8 or more bits
+ // (possible here after the final symbol consumed only part of the tail).
+ // RFC 7541 5.2: "A padding strictly longer than 7 bits MUST be treated as
+ // a decoding error." This keeps the strictness of the 4-bit FSM decoder.
+ if (avail_bits >= 8 || ((1u << avail_bits) - 1) != (buf & ((1u << avail_bits) - 1))) {
+ return -1; // Not EOF as expected
+ }
+ }
+
+end:
+ return dst - orig_dst;
+
+slow_path:
+ // Find previous byte boundary and finish decoding thence.
+ while ((avail_bits & 7) && dst > orig_dst) {
+ avail_bits += encode_table[static_cast<uint8_t>(*--dst)].bits;
+ }
+ assert((avail_bits & 7) == 0);
+ src -= avail_bits >> 3;
+ r = lshpack_dec_huff_decode_full(src, static_cast<uint32_t>(src_end - src), dst, static_cast<uint32_t>(dst_end - dst));
+ if (r >= 0) {
+ return dst - orig_dst + r;
+ }
+ return r;
+}
+
} // namespace litespeed
diff --git a/lib/ls-hpack/lshpack.h b/lib/ls-hpack/lshpack.h
index 7069317b28..c64c1f19cc 100644
--- a/lib/ls-hpack/lshpack.h
+++ b/lib/ls-hpack/lshpack.h
@@ -59,10 +59,16 @@ SOFTWARE.
namespace litespeed {
+// Matches upstream lshpack.h's LSHPACK_ERR_MORE_BUF.
+constexpr int64_t LSHPACK_ERR_MORE_BUF = -3;
+
int64_t lshpack_enc_huff_encode(uint8_t const *src,
uint8_t const *const src_end, uint8_t *dst, uint32_t dst_len);
int64_t lshpack_dec_huff_decode_full(uint8_t const *src, uint32_t src_len,
char *dst, uint32_t dst_len);
+int64_t lshpack_dec_huff_decode(uint8_t const *src, uint32_t src_len,
+ char *dst, uint32_t dst_len);
+
} // namespace litespeed
diff --git a/src/proxy/hdrs/CMakeLists.txt b/src/proxy/hdrs/CMakeLists.txt
index 09dfc6f8b6..de62a23475 100644
--- a/src/proxy/hdrs/CMakeLists.txt
+++ b/src/proxy/hdrs/CMakeLists.txt
@@ -57,6 +57,8 @@ if(BUILD_TESTING)
target_link_libraries(
test_proxy_hdrs PRIVATE ts::hdrs ts::tscore ts::inkevent libswoc::libswoc Catch2::Catch2WithMain lshpack
)
+ # For the ls-hpack decoder parity tests.
+ target_include_directories(test_proxy_hdrs PRIVATE ${CMAKE_SOURCE_DIR}/lib)
add_catch2_test(NAME test_proxy_hdrs COMMAND test_proxy_hdrs)
add_executable(test_proxy_hdrs_xpack unit_tests/test_XPACK.cc)
diff --git a/src/proxy/hdrs/HuffmanCodec.cc b/src/proxy/hdrs/HuffmanCodec.cc
index e8a1427584..5f144d7294 100644
--- a/src/proxy/hdrs/HuffmanCodec.cc
+++ b/src/proxy/hdrs/HuffmanCodec.cc
@@ -26,11 +26,11 @@
#include <cstdint>
// Implementation taken from LiteSpeed:
-// lshpack_dec_huff_decode_full
+// lshpack_dec_huff_decode
int64_t
huffman_decode(char *dst, uint32_t dst_len, const uint8_t *src, uint32_t src_len)
{
- return litespeed::lshpack_dec_huff_decode_full(src, src_len, dst, dst_len);
+ return litespeed::lshpack_dec_huff_decode(src, src_len, dst, dst_len);
}
// Implementation taken from LiteSpeed:
diff --git a/src/proxy/hdrs/unit_tests/test_Huffmancode.cc b/src/proxy/hdrs/unit_tests/test_Huffmancode.cc
index 4b942d1f88..6a7c2cb22a 100644
--- a/src/proxy/hdrs/unit_tests/test_Huffmancode.cc
+++ b/src/proxy/hdrs/unit_tests/test_Huffmancode.cc
@@ -22,10 +22,14 @@
*/
#include "proxy/hdrs/HuffmanCodec.h"
+#include "ls-hpack/lshpack.h"
+#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <cassert>
#include <cstring>
+#include <random>
+#include <vector>
#include <catch2/catch_test_macros.hpp>
using namespace std;
@@ -207,3 +211,136 @@ TEST_CASE("decode_errors", "[proxy][huffman]")
free(dst);
}
}
+
+TEST_CASE("decode_known_vectors", "[proxy][huffman]")
+{
+ for (const auto &i : huffman_encode_test_data) {
+ char dst[64];
+ int64_t len = huffman_decode(dst, sizeof(dst), i.expect, i.expect_len);
+
+ REQUIRE(len == i.src_len);
+ REQUIRE(memcmp(dst, i.src, len) == 0);
+ }
+}
+
+// The fast table-based decoder must agree with the original 4-bit FSM decoder
+// on input validity and decoded content. The full decoder run with a roomy
+// destination serves as the oracle. The one permitted difference is an
+// exactly-sized destination (dst_len == decoded length): depending on how the
+// trailing padding falls on nibble boundaries, either decoder may report
+// LSHPACK_ERR_MORE_BUF where the other succeeds. One byte of headroom
+// guarantees success for both; ATS callers always allocate twice the encoded
+// length, which is strictly more than any decoded length. Sentinel bytes
+// verify nothing is written past dst_len.
+static void
+require_decoder_parity(const uint8_t *src, uint32_t src_len, uint32_t dst_len)
+{
+ constexpr size_t SENTINEL_LEN = 64;
+ std::vector<char> oracle_buf(2 * src_len + 8);
+ int64_t oracle = litespeed::lshpack_dec_huff_decode_full(src, src_len, oracle_buf.data(), oracle_buf.size());
+
+ std::vector<char> fast_buf(dst_len + SENTINEL_LEN, '\xa5');
+ int64_t fast = litespeed::lshpack_dec_huff_decode(src, src_len, fast_buf.data(), dst_len);
+
+ if (oracle < 0 || static_cast<uint64_t>(oracle) > dst_len) {
+ REQUIRE(fast < 0);
+ } else if (static_cast<uint64_t>(oracle) == dst_len && fast < 0) {
+ REQUIRE(fast == litespeed::LSHPACK_ERR_MORE_BUF); // exact-fit destination
+ } else {
+ REQUIRE(fast == oracle);
+ REQUIRE(memcmp(fast_buf.data(), oracle_buf.data(), oracle) == 0);
+ }
+ REQUIRE(std::all_of(fast_buf.begin() + dst_len, fast_buf.end(), [](char c) { return c == '\xa5'; }));
+}
+
+TEST_CASE("decoder_parity_exhaustive_short", "[proxy][huffman]")
+{
+ uint8_t src[2];
+
+ for (uint32_t hi = 0; hi < 256; ++hi) {
+ src[0] = static_cast<uint8_t>(hi);
+ for (uint32_t dst_len = 0; dst_len <= 4; ++dst_len) {
+ require_decoder_parity(src, 1, dst_len);
+ }
+ for (uint32_t lo = 0; lo < 256; ++lo) {
+ src[1] = static_cast<uint8_t>(lo);
+ require_decoder_parity(src, 2, 8);
+ }
+ }
+}
+
+TEST_CASE("decoder_parity_fuzz", "[proxy][huffman]")
+{
+ std::mt19937 rng(0x5eed);
+ std::uniform_int_distribution<int> len_dist(0, 64);
+ std::uniform_int_distribution<int> byte_dist(0, 255);
+ uint8_t src[64];
+
+ for (int i = 0; i < 100000; ++i) {
+ int src_len = len_dist(rng);
+ for (int j = 0; j < src_len; ++j) {
+ src[j] = static_cast<uint8_t>(byte_dist(rng));
+ }
+ // Bias some iterations toward trailing 0xff runs to exercise the
+ // EOS-prefix and padding paths.
+ if (i % 4 == 0 && src_len > 0) {
+ for (int j = byte_dist(rng) % src_len; j < src_len; ++j) {
+ src[j] = 0xff;
+ }
+ }
+ // Mostly roomy destination, sometimes a tight one to exercise the
+ // bounds-checked path and LSHPACK_ERR_MORE_BUF.
+ uint32_t dst_len = (i % 8 == 0) ? static_cast<uint32_t>(byte_dist(rng) % 16) : static_cast<uint32_t>(2 * src_len + 8);
+ require_decoder_parity(src, src_len, dst_len);
+ }
+}
+
+// Streams whose final symbol is followed by 8 or more bits of all-ones
+// padding. RFC 7541 5.2 requires treating padding longer than 7 bits as a
+// decoding error. Upstream ls-hpack's fast decoder accepts these; the ATS
+// copy deliberately rejects them (see the tail check in
+// lib/ls-hpack/lshpack.cc), and this test pins that divergence against a
+// future re-sync. The exhaustive parity test cannot cover this branch: it is
+// only reachable with inputs of four or more bytes.
+TEST_CASE("decode_overlong_padding", "[proxy][huffman]")
+{
+ const static struct {
+ const uint8_t *input;
+ uint32_t input_len;
+ } test_cases[] = {
+ {reinterpret_cast<const uint8_t *>("\xbf\xd0\x37\xd9\xff"), 5 },
+ {reinterpret_cast<const uint8_t *>("\xd9\x68\x63\x84\x4c\xfe\x77\xa0\xbc\xc9\xff"), 11},
+ };
+
+ for (const auto &tc : test_cases) {
+ char dst[64];
+ REQUIRE(litespeed::lshpack_dec_huff_decode(tc.input, tc.input_len, dst, sizeof(dst)) == -1);
+ REQUIRE(litespeed::lshpack_dec_huff_decode_full(tc.input, tc.input_len, dst, sizeof(dst)) == -1);
+ }
+}
+
+TEST_CASE("decoder_roundtrip_fuzz", "[proxy][huffman]")
+{
+ std::mt19937 rng(0xc0ffee);
+ std::uniform_int_distribution<int> len_dist(0, 300);
+ std::uniform_int_distribution<int> byte_dist(0, 255);
+
+ for (int i = 0; i < 20000; ++i) {
+ uint32_t src_len = static_cast<uint32_t>(len_dist(rng));
+ std::vector<uint8_t> src(src_len);
+ for (auto &c : src) {
+ c = static_cast<uint8_t>(byte_dist(rng));
+ }
+
+ // Worst-case Huffman expansion is 30 bits per input byte.
+ std::vector<uint8_t> encoded(src_len * 4 + 8);
+ int64_t enc_len = huffman_encode(encoded.data(), encoded.size(), src.data(), src_len);
+ REQUIRE(enc_len >= 0);
+
+ // One byte of headroom guarantees success (see require_decoder_parity).
+ std::vector<char> decoded(src_len + 1);
+ int64_t dec_len = huffman_decode(decoded.data(), src_len + 1, encoded.data(), enc_len);
+ REQUIRE(dec_len == static_cast<int64_t>(src_len));
+ REQUIRE(memcmp(decoded.data(), src.data(), src_len) == 0);
+ }
+}
diff --git a/tools/benchmark/CMakeLists.txt b/tools/benchmark/CMakeLists.txt
index 49f25fad1c..e58e8dcf26 100644
--- a/tools/benchmark/CMakeLists.txt
+++ b/tools/benchmark/CMakeLists.txt
@@ -47,3 +47,7 @@ target_link_libraries(
ts::inkcache
ts::inkhostdb
)
+
+add_executable(benchmark_HuffmanDecode benchmark_HuffmanDecode.cc)
+target_link_libraries(benchmark_HuffmanDecode PRIVATE Catch2::Catch2WithMain lshpack)
+target_include_directories(benchmark_HuffmanDecode PRIVATE ${CMAKE_SOURCE_DIR}/lib)
diff --git a/tools/benchmark/benchmark_HuffmanDecode.cc b/tools/benchmark/benchmark_HuffmanDecode.cc
new file mode 100644
index 0000000000..188501a5f0
--- /dev/null
+++ b/tools/benchmark/benchmark_HuffmanDecode.cc
@@ -0,0 +1,142 @@
+/** @file
+
+ Benchmark comparing the two ls-hpack Huffman decoders: the original 4-bit
+ FSM decoder (lshpack_dec_huff_decode_full) and the 16-bit table decoder
+ (lshpack_dec_huff_decode).
+
+ @section license License
+
+ 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.
+ */
+
+#include "ls-hpack/lshpack.h"
+
+#include <cstdint>
+#include <cstring>
+#include <string_view>
+#include <vector>
+
+#define CATCH_CONFIG_ENABLE_BENCHMARKING
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/benchmark/catch_benchmark.hpp>
+
+namespace
+{
+
+// Representative header field values seen on the wire.
+constexpr std::string_view SHORT_VALUE = "text/css";
+constexpr std::string_view MEDIUM_VALUE = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
+constexpr std::string_view LONG_VALUE =
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
+constexpr std::string_view CORPUS[] = {
+ SHORT_VALUE,
+ MEDIUM_VALUE,
+ LONG_VALUE,
+ "/",
+ "gzip, deflate, br, zstd",
+ "Mon, 21 Oct 2013 20:13:21 GMT",
+ "https://www.example.com/images/2026/06/some-article/hero.webp?w=1200&q=75",
+ "max-age=31536000, public, immutable",
+ "session=8f14e45fceea167a5a36dedd4bea2543; theme=dark; region=us-west-2; ab_bucket=37",
+ "ATS/10.1.0",
+};
+
+std::vector<uint8_t>
+encode(std::string_view text)
+{
+ std::vector<uint8_t> encoded(text.size() * 4 + 8);
+ int64_t len = litespeed::lshpack_enc_huff_encode(reinterpret_cast<const uint8_t *>(text.data()), //
+ reinterpret_cast<const uint8_t *>(text.data()) + text.size(), encoded.data(),
+ static_cast<uint32_t>(encoded.size()));
+ REQUIRE(len > 0);
+ encoded.resize(len);
+ return encoded;
+}
+
+} // namespace
+
+TEST_CASE("huffman decode: full vs fast", "[bench][huffman]")
+{
+ char dst[4096];
+
+ // Sanity: both decoders agree on the corpus.
+ for (auto text : CORPUS) {
+ auto encoded = encode(text);
+ int64_t full = litespeed::lshpack_dec_huff_decode_full(encoded.data(), encoded.size(), dst, sizeof(dst));
+ REQUIRE(full == static_cast<int64_t>(text.size()));
+ REQUIRE(memcmp(dst, text.data(), text.size()) == 0);
+ int64_t fast = litespeed::lshpack_dec_huff_decode(encoded.data(), encoded.size(), dst, sizeof(dst));
+ REQUIRE(fast == full);
+ REQUIRE(memcmp(dst, text.data(), text.size()) == 0);
+ }
+
+ auto short_enc = encode(SHORT_VALUE);
+ auto medium_enc = encode(MEDIUM_VALUE);
+ auto long_enc = encode(LONG_VALUE);
+
+ BENCHMARK("full: short (8B)")
+ {
+ return litespeed::lshpack_dec_huff_decode_full(short_enc.data(), short_enc.size(), dst, sizeof(dst));
+ };
+ BENCHMARK("fast: short (8B)")
+ {
+ return litespeed::lshpack_dec_huff_decode(short_enc.data(), short_enc.size(), dst, sizeof(dst));
+ };
+
+ BENCHMARK("full: medium (86B)")
+ {
+ return litespeed::lshpack_dec_huff_decode_full(medium_enc.data(), medium_enc.size(), dst, sizeof(dst));
+ };
+ BENCHMARK("fast: medium (86B)")
+ {
+ return litespeed::lshpack_dec_huff_decode(medium_enc.data(), medium_enc.size(), dst, sizeof(dst));
+ };
+
+ BENCHMARK("full: long (113B)")
+ {
+ return litespeed::lshpack_dec_huff_decode_full(long_enc.data(), long_enc.size(), dst, sizeof(dst));
+ };
+ BENCHMARK("fast: long (113B)")
+ {
+ return litespeed::lshpack_dec_huff_decode(long_enc.data(), long_enc.size(), dst, sizeof(dst));
+ };
+
+ std::vector<std::vector<uint8_t>> corpus_enc;
+ size_t corpus_bytes = 0;
+ for (auto text : CORPUS) {
+ corpus_enc.push_back(encode(text));
+ corpus_bytes += text.size();
+ }
+ WARN("corpus: " << corpus_bytes << " decoded bytes per iteration");
+
+ BENCHMARK("full: corpus")
+ {
+ int64_t acc = 0;
+ for (auto const &encoded : corpus_enc) {
+ acc += litespeed::lshpack_dec_huff_decode_full(encoded.data(), encoded.size(), dst, sizeof(dst));
+ }
+ return acc;
+ };
+ BENCHMARK("fast: corpus")
+ {
+ int64_t acc = 0;
+ for (auto const &encoded : corpus_enc) {
+ acc += litespeed::lshpack_dec_huff_decode(encoded.data(), encoded.size(), dst, sizeof(dst));
+ }
+ return acc;
+ };
+}
(trafficserver) 04/33: Fix incorrect errno short names on FreeBSD/macOS in bwf::Errno (#13240)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit fefa7fe7bdf297e3d748662b39813edb36f20626
Author: Bryan Call <bc...@apache.org>
AuthorDate: Wed Jun 17 08:23:16 2026 -0700
Fix incorrect errno short names on FreeBSD/macOS in bwf::Errno (#13240)
bwf::Errno used a single Linux-numbered table indexed by raw errno, so
other platforms printed the wrong name (e.g. ETIMEDOUT=60 logged as
ENOSTR on FreeBSD/macOS). Replace it with a per-platform table built
from each OS's <errno.h>, add guarded tests, and #error on unsupported
platforms.
(cherry picked from commit 4b6f79035141c3904c889ff83f0b68dff5209f53)
---
lib/swoc/src/bw_format.cc | 155 +++++++++++++++++-----------------
lib/swoc/unit_tests/test_bw_format.cc | 35 ++++++++
2 files changed, 111 insertions(+), 79 deletions(-)
diff --git a/lib/swoc/src/bw_format.cc b/lib/swoc/src/bw_format.cc
index 5b87fc9909..28a2348d4d 100644
--- a/lib/swoc/src/bw_format.cc
+++ b/lib/swoc/src/bw_format.cc
@@ -743,83 +743,79 @@ FixedBufferWriter::operator>>(std::ostream &s) const {
}
namespace {
-// Hand rolled, might not be totally compliant everywhere, but probably close
-// enough. The long string will be locally accurate. Clang requires the double
-// braces. Why, Turing only knows.
-static const std::array<std::string_view, 134> ERRNO_SHORT_NAME = {
- {
- "SUCCESS", "EPERM",
- "ENOENT", "ESRCH",
- "EINTR", "EIO",
- "ENXIO", "E2BIG ",
- "ENOEXEC", "EBADF",
- "ECHILD", "EAGAIN",
- "ENOMEM", "EACCES",
- "EFAULT", "ENOTBLK",
- "EBUSY", "EEXIST",
- "EXDEV", "ENODEV",
- "ENOTDIR", "EISDIR",
- "EINVAL", "ENFILE",
- "EMFILE", "ENOTTY",
- "ETXTBSY", "EFBIG",
- "ENOSPC", "ESPIPE",
- "EROFS", "EMLINK",
- "EPIPE", "EDOM",
- "ERANGE", "EDEADLK",
- "ENAMETOOLONG", "ENOLCK",
- "ENOSYS", "ENOTEMPTY",
- "ELOOP", "EWOULDBLOCK",
- "ENOMSG", "EIDRM",
- "ECHRNG", "EL2NSYNC",
- "EL3HLT", "EL3RST",
- "ELNRNG", "EUNATCH",
- "ENOCSI", "EL2HTL",
- "EBADE", "EBADR",
- "EXFULL", "ENOANO",
- "EBADRQC", "EBADSLT",
- "EDEADLOCK", "EBFONT",
- "ENOSTR", "ENODATA",
- "ETIME", "ENOSR",
- "ENONET", "ENOPKG",
- "EREMOTE", "ENOLINK",
- "EADV", "ESRMNT",
- "ECOMM", "EPROTO",
- "EMULTIHOP", "EDOTDOT",
- "EBADMSG", "EOVERFLOW",
- "ENOTUNIQ", "EBADFD",
- "EREMCHG", "ELIBACC",
- "ELIBBAD", "ELIBSCN",
- "ELIBMAX", "ELIBEXEC",
- "EILSEQ", "ERESTART",
- "ESTRPIPE", "EUSERS",
- "ENOTSOCK", "EDESTADDRREQ",
- "EMSGSIZE", "EPROTOTYPE",
- "ENOPROTOOPT", "EPROTONOSUPPORT",
- "ESOCKTNOSUPPORT", "EOPNOTSUPP",
- "EPFNOSUPPORT", "EAFNOSUPPORT",
- "EADDRINUSE", "EADDRNOTAVAIL",
- "ENETDOWN", "ENETUNREACH",
- "ENETRESET", "ECONNABORTED",
- "ECONNRESET", "ENOBUFS",
- "EISCONN", "ENOTCONN",
- "ESHUTDOWN", "ETOOMANYREFS",
- "ETIMEDOUT", "ECONNREFUSED",
- "EHOSTDOWN", "EHOSTUNREACH",
- "EALREADY", "EINPROGRESS",
- "ESTALE", "EUCLEAN",
- "ENOTNAM", "ENAVAIL",
- "EISNAM", "EREMOTEIO",
- "EDQUOT", "ENOMEDIUM",
- "EMEDIUMTYPE", "ECANCELED",
- "ENOKEY", "EKEYEXPIRED",
- "EKEYREVOKED", "EKEYREJECTED",
- "EOWNERDEAD", "ENOTRECOVERABLE",
- "ERFKILL", "EHWPOISON",
- }
-};
-static constexpr DiscreteRange<unsigned> ERRNO_RANGE{0, ERRNO_SHORT_NAME.size() - 1};
-// This provides convenient safe access to the errno short name array.
-auto errno_short_name = [](unsigned n) { return ERRNO_RANGE.contains(n) ? ERRNO_SHORT_NAME[n] : "Unknown"sv; };
+// errno value -> symbolic name, indexed directly by the errno number (O(1)).
+// The numbering differs between Linux and the BSD-derived platforms, and even
+// macOS and FreeBSD disagree on a few of their own high-numbered codes, so each
+// platform carries a table generated from its real <errno.h>. Gaps in the
+// numbering are empty and fall through to "Unknown"; a platform with no table
+// is a compile error. The long form (the 'l' spec, below) still uses strerror().
+// clang-format off
+#if defined(__linux__)
+static constexpr std::array<std::string_view, 134> ERRNO_NAMES = {{
+ /* 0 */ "SUCCESS", "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",
+ /* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EAGAIN", "ENOMEM", "EACCES", "EFAULT", "ENOTBLK",
+ /* 16 */ "EBUSY", "EEXIST", "EXDEV", "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE",
+ /* 24 */ "EMFILE", "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS", "EMLINK",
+ /* 32 */ "EPIPE", "EDOM", "ERANGE", "EDEADLK", "ENAMETOOLONG", "ENOLCK", "ENOSYS", "ENOTEMPTY",
+ /* 40 */ "ELOOP", "", "ENOMSG", "EIDRM", "ECHRNG", "EL2NSYNC", "EL3HLT", "EL3RST",
+ /* 48 */ "ELNRNG", "EUNATCH", "ENOCSI", "EL2HLT", "EBADE", "EBADR", "EXFULL", "ENOANO",
+ /* 56 */ "EBADRQC", "EBADSLT", "", "EBFONT", "ENOSTR", "ENODATA", "ETIME", "ENOSR",
+ /* 64 */ "ENONET", "ENOPKG", "EREMOTE", "ENOLINK", "EADV", "ESRMNT", "ECOMM", "EPROTO",
+ /* 72 */ "EMULTIHOP", "EDOTDOT", "EBADMSG", "EOVERFLOW", "ENOTUNIQ", "EBADFD", "EREMCHG", "ELIBACC",
+ /* 80 */ "ELIBBAD", "ELIBSCN", "ELIBMAX", "ELIBEXEC", "EILSEQ", "ERESTART", "ESTRPIPE", "EUSERS",
+ /* 88 */ "ENOTSOCK", "EDESTADDRREQ", "EMSGSIZE", "EPROTOTYPE", "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT", "EOPNOTSUPP",
+ /* 96 */ "EPFNOSUPPORT", "EAFNOSUPPORT", "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH", "ENETRESET", "ECONNABORTED",
+ /* 104 */ "ECONNRESET", "ENOBUFS", "EISCONN", "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT", "ECONNREFUSED",
+ /* 112 */ "EHOSTDOWN", "EHOSTUNREACH", "EALREADY", "EINPROGRESS", "ESTALE", "EUCLEAN", "ENOTNAM", "ENAVAIL",
+ /* 120 */ "EISNAM", "EREMOTEIO", "EDQUOT", "ENOMEDIUM", "EMEDIUMTYPE", "ECANCELED", "ENOKEY", "EKEYEXPIRED",
+ /* 128 */ "EKEYREVOKED", "EKEYREJECTED", "EOWNERDEAD", "ENOTRECOVERABLE", "ERFKILL", "EHWPOISON",
+}};
+#elif defined(__APPLE__)
+static constexpr std::array<std::string_view, 108> ERRNO_NAMES = {{
+ /* 0 */ "SUCCESS", "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",
+ /* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EDEADLK", "ENOMEM", "EACCES", "EFAULT", "ENOTBLK",
+ /* 16 */ "EBUSY", "EEXIST", "EXDEV", "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE",
+ /* 24 */ "EMFILE", "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS", "EMLINK",
+ /* 32 */ "EPIPE", "EDOM", "ERANGE", "EAGAIN", "EINPROGRESS", "EALREADY", "ENOTSOCK", "EDESTADDRREQ",
+ /* 40 */ "EMSGSIZE", "EPROTOTYPE", "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT", "ENOTSUP", "EPFNOSUPPORT", "EAFNOSUPPORT",
+ /* 48 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH", "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS",
+ /* 56 */ "EISCONN", "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT", "ECONNREFUSED", "ELOOP", "ENAMETOOLONG",
+ /* 64 */ "EHOSTDOWN", "EHOSTUNREACH", "ENOTEMPTY", "EPROCLIM", "EUSERS", "EDQUOT", "ESTALE", "EREMOTE",
+ /* 72 */ "EBADRPC", "ERPCMISMATCH", "EPROGUNAVAIL", "EPROGMISMATCH", "EPROCUNAVAIL", "ENOLCK", "ENOSYS", "EFTYPE",
+ /* 80 */ "EAUTH", "ENEEDAUTH", "EPWROFF", "EDEVERR", "EOVERFLOW", "EBADEXEC", "EBADARCH", "ESHLIBVERS",
+ /* 88 */ "EBADMACHO", "ECANCELED", "EIDRM", "ENOMSG", "EILSEQ", "ENOATTR", "EBADMSG", "EMULTIHOP",
+ /* 96 */ "ENODATA", "ENOLINK", "ENOSR", "ENOSTR", "EPROTO", "ETIME", "EOPNOTSUPP", "ENOPOLICY",
+ /* 104 */ "ENOTRECOVERABLE", "EOWNERDEAD", "EQFULL", "ENOTCAPABLE",
+}};
+#elif defined(__FreeBSD__)
+static constexpr std::array<std::string_view, 98> ERRNO_NAMES = {{
+ /* 0 */ "SUCCESS", "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",
+ /* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EDEADLK", "ENOMEM", "EACCES", "EFAULT", "ENOTBLK",
+ /* 16 */ "EBUSY", "EEXIST", "EXDEV", "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE",
+ /* 24 */ "EMFILE", "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS", "EMLINK",
+ /* 32 */ "EPIPE", "EDOM", "ERANGE", "EAGAIN", "EINPROGRESS", "EALREADY", "ENOTSOCK", "EDESTADDRREQ",
+ /* 40 */ "EMSGSIZE", "EPROTOTYPE", "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT", "EOPNOTSUPP", "EPFNOSUPPORT", "EAFNOSUPPORT",
+ /* 48 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH", "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS",
+ /* 56 */ "EISCONN", "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT", "ECONNREFUSED", "ELOOP", "ENAMETOOLONG",
+ /* 64 */ "EHOSTDOWN", "EHOSTUNREACH", "ENOTEMPTY", "EPROCLIM", "EUSERS", "EDQUOT", "ESTALE", "EREMOTE",
+ /* 72 */ "EBADRPC", "ERPCMISMATCH", "EPROGUNAVAIL", "EPROGMISMATCH", "EPROCUNAVAIL", "ENOLCK", "ENOSYS", "EFTYPE",
+ /* 80 */ "EAUTH", "ENEEDAUTH", "EIDRM", "ENOMSG", "EOVERFLOW", "ECANCELED", "EILSEQ", "ENOATTR",
+ /* 88 */ "EDOOFUS", "EBADMSG", "EMULTIHOP", "ENOLINK", "EPROTO", "ENOTCAPABLE", "ECAPMODE", "ENOTRECOVERABLE",
+ /* 96 */ "EOWNERDEAD", "EINTEGRITY",
+}};
+#else
+#error "errno name table not defined for this platform"
+#endif
+// clang-format on
+
+std::string_view
+errno_short_name(int e)
+{
+ if (e >= 0 && e < static_cast<int>(ERRNO_NAMES.size()) && !ERRNO_NAMES[e].empty()) {
+ return ERRNO_NAMES[e];
+ }
+ return "Unknown"sv;
+}
} // namespace
BufferWriter &
@@ -910,8 +906,9 @@ bwformat(BufferWriter &w, bwf::Spec const &spec, std::error_code const &ec) {
if (spec.has_numeric_type()) { // if numeric type, print just the numeric part.
bwformat(w, spec, ec.value());
} else {
- if ((&ec.category() == G_CAT || &ec.category() == S_CAT) && swoc::ERRNO_RANGE.contains(ec.value())) {
- bwformat(w, spec, swoc::ERRNO_SHORT_NAME[ec.value()]);
+ auto short_name = (&ec.category() == G_CAT || &ec.category() == S_CAT) ? errno_short_name(ec.value()) : "Unknown"sv;
+ if (short_name != "Unknown"sv) {
+ bwformat(w, spec, short_name);
} else {
w.write(ec.message());
}
diff --git a/lib/swoc/unit_tests/test_bw_format.cc b/lib/swoc/unit_tests/test_bw_format.cc
index 2cfb99ad2f..caf69a0499 100644
--- a/lib/swoc/unit_tests/test_bw_format.cc
+++ b/lib/swoc/unit_tests/test_bw_format.cc
@@ -8,6 +8,9 @@
#include <iostream>
#include <variant>
#include <cmath>
+#include <cerrno>
+#include <string>
+#include <system_error>
#include <netinet/in.h>
@@ -545,6 +548,38 @@ TEST_CASE("bwstring std formats", "[libswoc][bwprint]") {
w.clear().print("{::l}", swoc::bwf::Errno(13));
REQUIRE(w.view() == "Permission denied [13]"sv);
+ // The symbolic short name must come from the running platform's <errno.h>,
+ // not a fixed Linux-numbered table. Regression guard for the FreeBSD/macOS
+ // mislabel where ETIMEDOUT (errno 60 on those platforms) printed as "ENOSTR"
+ // (apache/trafficserver#13203). Use the macros so each platform checks its
+ // own numbering.
+ w.clear().print("{:s:s}", swoc::bwf::Errno(EPERM));
+ REQUIRE(w.view() == "EPERM"sv);
+ w.clear().print("{:s:s}", swoc::bwf::Errno(ETIMEDOUT));
+ REQUIRE(w.view() == "ETIMEDOUT"sv);
+ w.clear().print("{:s:s}", swoc::bwf::Errno(ECONNREFUSED));
+ REQUIRE(w.view() == "ECONNREFUSED"sv);
+
+ // Direct guards for the codes that were missing from the tables (each one
+ // formatted as "Unknown" before this fix). They are platform-specific, so
+ // each only compiles where it is defined: ENOATTR on macOS/FreeBSD, EL2HLT
+ // on Linux.
+#ifdef ENOATTR
+ w.clear().print("{:s:s}", swoc::bwf::Errno(ENOATTR));
+ REQUIRE(w.view() == "ENOATTR"sv);
+#endif
+#ifdef EL2HLT
+ w.clear().print("{:s:s}", swoc::bwf::Errno(EL2HLT));
+ REQUIRE(w.view() == "EL2HLT"sv);
+#endif
+
+ // The std::error_code formatter routes through the same per-platform table,
+ // rendering "<name> [<value>]". Build the expectation from the macro so the
+ // numeric part matches the running platform's numbering.
+ std::string ec_expected = "ECONNREFUSED [" + std::to_string(ECONNREFUSED) + "]";
+ w.clear().print("{}", std::error_code(ECONNREFUSED, std::generic_category()));
+ REQUIRE(w.view() == ec_expected);
+
time_t t = 1528484137;
// default is GMT
w.clear().print("{} is {}", t, swoc::bwf::Date(t));
(trafficserver) 03/33: Fix hdrHeap/hdrStrHeap allocator inuse metric underflow (#13218)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 24c34a1bb1ee74ef3221bc421540665f17cd3c5a
Author: Mo Chen <mo...@apache.org>
AuthorDate: Tue Jun 16 13:08:27 2026 -0500
Fix hdrHeap/hdrStrHeap allocator inuse metric underflow (#13218)
The hdrHeap and hdrStrHeap allocators are plain Allocator globals whose
objects are allocated with no constructor arguments, so THREAD_ALLOC
resolves to the non-templated thread_alloc(Allocator &, ProxyAllocator &)
overload. Unlike the templated overload, it did not call
increment_for_alloc() when reusing an object from the per-thread
freelist, while THREAD_FREE always decremented inuse on the way in.
The counted frees therefore outran the counted allocs and
proxy.process.allocator.inuse.{hdrHeap,hdrStrHeap} marched negative,
wrapping around as a huge uint64 value. Other allocators use
ClassAllocator (the templated path) and were unaffected.
Account for the freelist reuse in the non-templated overload so the two
paths stay symmetric, and add a regression test that cycles a block
through the freelist and asserts inuse stays balanced and never dips
below its starting value.
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
(cherry picked from commit 113f5fbb1667df56799e9878c71d6cd286695a49)
---
src/iocore/eventsystem/ProxyAllocator.cc | 8 +++
src/proxy/hdrs/unit_tests/test_HdrHeap.cc | 101 ++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+)
diff --git a/src/iocore/eventsystem/ProxyAllocator.cc b/src/iocore/eventsystem/ProxyAllocator.cc
index 7b6f4dd1d7..8f9d93cfc3 100644
--- a/src/iocore/eventsystem/ProxyAllocator.cc
+++ b/src/iocore/eventsystem/ProxyAllocator.cc
@@ -34,6 +34,14 @@ thread_alloc(Allocator &a, ProxyAllocator &l)
void *v = l.freelist;
l.freelist = *static_cast<void **>(l.freelist);
--(l.allocated);
+#if TS_USE_ALLOCATOR_METRICS
+ // Reusing an item from the per-thread freelist is an allocation as far as
+ // the metrics are concerned. THREAD_FREE decremented inuse when the item
+ // was pushed onto the freelist, so without this the counted frees exceed
+ // the counted allocs and inuse underflows. The templated thread_alloc()
+ // below already does this; the two paths must stay symmetric.
+ a.increment_for_alloc();
+#endif
return v;
}
return a.alloc_void();
diff --git a/src/proxy/hdrs/unit_tests/test_HdrHeap.cc b/src/proxy/hdrs/unit_tests/test_HdrHeap.cc
index 66e3bcacd0..c2017e714c 100644
--- a/src/proxy/hdrs/unit_tests/test_HdrHeap.cc
+++ b/src/proxy/hdrs/unit_tests/test_HdrHeap.cc
@@ -23,6 +23,17 @@
#include "proxy/hdrs/HdrHeap.h"
#include "proxy/hdrs/URL.h"
+#include "tscore/ink_config.h"
+
+#if TS_USE_ALLOCATOR_METRICS
+#include "tscore/Allocator.h"
+#include "tsutil/Metrics.h"
+#include "iocore/eventsystem/ProxyAllocator.h"
+
+#include <algorithm>
+#include <cstdint>
+#endif
+
/**
This test is designed to test numerous pieces of the HdrHeaps including allocations,
demotion of rw heaps to ronly heaps, and finally the coalesce and evacuate behaviours.
@@ -130,3 +141,93 @@ TEST_CASE("HdrHeap", "[proxy][hdrheap]")
// Clean up
heap->destroy();
}
+
+#if TS_USE_ALLOCATOR_METRICS
+
+extern Allocator hdrHeapAllocator;
+extern Allocator strHeapAllocator;
+
+namespace
+{
+// Mirror the per-thread-freelist branch of the THREAD_FREE macro: push the block
+// onto the freelist and account the free with the same metered call the macro
+// uses (UPDATE_FREE_METRICS).
+void
+freelist_push(Allocator &a, ProxyAllocator &l, void *p)
+{
+ *reinterpret_cast<char **>(p) = reinterpret_cast<char *>(l.freelist);
+ l.freelist = p;
+ l.allocated++;
+ a.increment_for_free();
+}
+
+// RAII guard that restores cmd_disable_pfreelist on scope exit, so a failing
+// REQUIRE()/CHECK() does not leak the flipped flag into later tests.
+struct PfreelistGuard {
+ int saved;
+ explicit PfreelistGuard(bool disable) : saved{cmd_disable_pfreelist} { cmd_disable_pfreelist = disable; }
+ ~PfreelistGuard() { cmd_disable_pfreelist = saved; }
+};
+} // namespace
+
+// Regression test for the hdrHeap/hdrStrHeap allocator inuse underflow. These two
+// allocators are plain Allocator globals whose objects are allocated with no
+// constructor arguments, so THREAD_ALLOC resolves to the non-templated
+// thread_alloc(Allocator &, ProxyAllocator &). That overload used to skip the
+// metric increment when reusing an object from the per-thread freelist, while
+// THREAD_FREE always decremented it on the way in. The counted frees therefore
+// outran the counted allocs and proxy.process.allocator.inuse.* marched negative,
+// wrapping around as a huge uint64 value.
+TEST_CASE("allocator inuse stays balanced across freelist reuse", "[proxy][hdrheap][metrics]")
+{
+ struct AllocCase {
+ const char *metric;
+ Allocator *allocator;
+ };
+
+ AllocCase const cases[] = {
+ {"proxy.process.allocator.inuse.hdrHeap", &hdrHeapAllocator},
+ {"proxy.process.allocator.inuse.hdrStrHeap", &strHeapAllocator},
+ };
+
+ // The unit test harness disables per-thread freelists; enable them so that
+ // thread_alloc() exercises the freelist-reuse path that regressed. The guard
+ // restores the harness setting even if an assertion below throws.
+ PfreelistGuard const pfreelist_guard{false};
+
+ for (auto const &c : cases) {
+ ts::Metrics::IdType id;
+ auto *inuse = ts::Metrics::Gauge::lookup(c.metric, &id);
+ REQUIRE(inuse != nullptr);
+
+ ProxyAllocator l;
+ int64_t const baseline = inuse->load();
+ int64_t low = baseline;
+
+ // One live block, allocated from the global pool (a counted allocation).
+ void *p = thread_alloc(*c.allocator, l);
+ REQUIRE(p != nullptr);
+
+ // Cycle the single block through the freelist. Each iteration frees it
+ // (inuse--) then reuses it (which must inuse++). Without the fix the reuse
+ // does not re-increment and inuse marches negative.
+ for (int i = 0; i < 1000; ++i) {
+ freelist_push(*c.allocator, l, p);
+ low = std::min(low, inuse->load());
+ p = thread_alloc(*c.allocator, l);
+ REQUIRE(p != nullptr);
+ low = std::min(low, inuse->load());
+ }
+
+ // Exactly one block is still outstanding, and inuse never dipped below the
+ // value we started from.
+ CHECK(inuse->load() == baseline + 1);
+ CHECK(low >= baseline);
+
+ // Release the outstanding block back to the global pool, restoring inuse.
+ c.allocator->free_void(p);
+ CHECK(inuse->load() == baseline);
+ }
+}
+
+#endif // TS_USE_ALLOCATOR_METRICS
(trafficserver) 28/33: Fix redirected cache write without write VC (#13309)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit a1aa2ed7dd38cbe90eceb665ec490853d10338b8
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Jun 24 12:28:49 2026 -0500
Fix redirected cache write without write VC (#13309)
Redirected transactions can retain CACHE_WL_SUCCESS after the concrete
cache write VC has been cleared. When a later response reaches cache
write setup, we crash dereferencing the missing write VC.
This only reuses a redirected cache write when the prepared write VC is
still available. Otherwise, this resets the write state so ATS prepares
a fresh cache write. The added test simply adds some coverage around the
scenario but is not a true regression test. That is, the test still
passes without the src/ code change.
This addresses a crash in the cache write setup path:
```
#0 HttpSM::setup_cache_write_transfer(...)
#1 HttpSM::perform_cache_write_action()
#2 HttpSM::handle_api_return()
#3 HttpSM::state_api_callout()
#4 HttpSM::state_api_callback()
#5 TSHttpTxnReenable()
#6 EscalateResponse()
```
(cherry picked from commit 3b04db3474c674aa286c9b3406e00a649ea50783)
---
src/proxy/http/HttpSM.cc | 2 ++
src/proxy/http/HttpTransact.cc | 11 +++--------
src/proxy/http/HttpTunnel.cc | 2 ++
tests/gold_tests/pluginTest/escalate/escalate.test.py | 10 +++++++---
4 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index d884a639d5..b1afc073e1 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -3932,6 +3932,8 @@ HttpSM::tunnel_handler_cache_write(int event, HttpTunnelConsumer *c)
server_response_body_bytes = c->bytes_written;
}
+ // The cache write VC is closed now, so any prepared write lock is gone.
+ t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT;
Metrics::Gauge::decrement(http_rsb.current_cache_connections);
return 0;
}
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index 9a3994f5a7..975eb4568a 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -3565,14 +3565,9 @@ HttpTransact::set_cache_prepare_write_action_for_new_request(State *s)
// This method must be called no more than one time per request. It should
// not be called for non-cacheable requests.
if (s->cache_info.write_lock_state == CacheWriteLock_t::SUCCESS) {
- // If and only if this is a redirected request, we may have already
- // prepared a cache write (during the handling of the previous request
- // which got the 3xx response) and can safely re-use it. Otherwise, we
- // risk storing the response under the wrong cache key. This is a release
- // assert because the correct behavior would be to prepare a new write,
- // but we can't do that because we failed to release the lock. To recover
- // we would have to tell the state machine to abort its write, and we
- // don't have a state for that.
+ // If and only if this is a redirected request, we may have already prepared
+ // a cache write during the handling of the previous request and can re-use
+ // it. Otherwise, we risk storing the response under the wrong cache key.
ink_release_assert(s->redirect_info.redirect_in_process);
s->cache_info.action = CacheAction_t::WRITE;
} else if (s->cache_info.write_lock_state == CacheWriteLock_t::READ_RETRY &&
diff --git a/src/proxy/http/HttpTunnel.cc b/src/proxy/http/HttpTunnel.cc
index 3695edf87a..4588dae087 100644
--- a/src/proxy/http/HttpTunnel.cc
+++ b/src/proxy/http/HttpTunnel.cc
@@ -1755,6 +1755,8 @@ HttpTunnel::chain_abort_cache_write(HttpTunnelProducer *p)
c->write_vio = nullptr;
c->vc->do_io_close(EHTTP_ERROR);
c->alive = false;
+ // The cache write VC is closed now, so any prepared write lock is gone.
+ sm->t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT;
Metrics::Gauge::decrement(http_rsb.current_cache_connections);
} else if (c->self_producer) {
chain_abort_cache_write(c->self_producer);
diff --git a/tests/gold_tests/pluginTest/escalate/escalate.test.py b/tests/gold_tests/pluginTest/escalate/escalate.test.py
index ea9f2953b6..46756e5a64 100644
--- a/tests/gold_tests/pluginTest/escalate/escalate.test.py
+++ b/tests/gold_tests/pluginTest/escalate/escalate.test.py
@@ -38,11 +38,14 @@ class EscalateTest:
_server_failover_file: str = 'escalate_failover_server_default.replay.yaml'
_process_counter: int = 0
- def __init__(self, disable_redirect_header: bool = False) -> None:
+ def __init__(self, disable_redirect_header: bool = False, enable_cache: bool = False) -> None:
'''Configure the test run.
:param disable_redirect_header: Whether to use --no-redirect-header.
+ :param enable_cache: Whether to exercise the cached redirect path.
'''
- tr = Test.AddTestRun(f'Test escalate plugin. disable_redirect_header={disable_redirect_header}')
+ tr = Test.AddTestRun(
+ f'Test escalate plugin. disable_redirect_header={disable_redirect_header}, enable_cache={enable_cache}')
+ self._enable_cache = enable_cache
self._setup_dns(tr)
self._setup_servers(tr, disable_redirect_header)
self._setup_ts(tr, disable_redirect_header)
@@ -118,7 +121,7 @@ class EscalateTest:
:param disable_redirect_header: Whether ATS should be configured with --no-redirect-header.
'''
process_name = f"ts_{EscalateTest._process_counter}"
- self._ts = tr.MakeATSProcess(process_name, enable_cache=False)
+ self._ts = tr.MakeATSProcess(process_name, enable_cache=self._enable_cache)
# Select a port that is guaranteed to not be used at the moment.
dead_port = get_port(self._ts, "dead_port")
self._ts.Disk.records_config.update(
@@ -287,4 +290,5 @@ class EscalateNonGetMethodsTest:
EscalateTest(disable_redirect_header=False)
EscalateTest(disable_redirect_header=True)
+EscalateTest(disable_redirect_header=False, enable_cache=True)
EscalateNonGetMethodsTest()
(trafficserver) 13/33: Fix connect attempt retries (#13102)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 0fd5a7ff3c1259667ca98ce4929557f71022dd9e
Author: Masaori Koshiba <ma...@apache.org>
AuthorDate: Wed Jun 17 06:52:12 2026 +0900
Fix connect attempt retries (#13102)
* Fix connect attempt retries
* Fix table format
(cherry picked from commit fb102991e75c89ec846d7c2f37667b4ba7f39650)
---
doc/admin-guide/files/records.yaml.en.rst | 21 +-
.../api/functions/TSHttpOverridableConfig.en.rst | 269 +++++++++++----------
.../api/types/TSOverridableConfigKey.en.rst | 1 +
doc/release-notes/upgrading.en.rst | 5 +
include/cripts/Configs.hpp | 2 +
include/iocore/hostdb/HostDBProcessor.h | 2 +-
include/proxy/http/HttpConfig.h | 10 +-
include/proxy/http/HttpTransact.h | 5 +-
include/proxy/http/OverridableConfigDefs.h | 3 +-
include/ts/apidefs.h.in | 1 +
src/iocore/hostdb/HostDB.cc | 14 +-
src/proxy/http/HttpConfig.cc | 40 ++-
src/proxy/http/HttpSM.cc | 68 +++---
src/proxy/http/HttpTransact.cc | 188 ++++++++------
src/records/RecordsConfig.cc | 8 +-
.../autest-site/verifier_client.test.ext | 15 +-
.../replay/connect_down_policy_4.replay.yaml | 2 +-
tests/gold_tests/dns/connect_attempts.test.py | 5 +
tests/gold_tests/dns/dns_host_down.test.py | 14 ++
.../connect_attempts_rr_max_retries_error_log.gold | 4 +-
.../connect_attempts_rr_retries_error_log.gold | 7 +-
...nect_attempts_single_max_retries_error_log.gold | 9 +
.../connect_attempts_rr_max_retries.replay.yaml | 2 +-
.../connect_attempts_rr_no_retry.replay.yaml | 2 +-
.../replay/connect_attempts_rr_retries.replay.yaml | 59 ++---
...onnect_attempts_single_max_retries.replay.yaml} | 79 ++----
26 files changed, 461 insertions(+), 374 deletions(-)
diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst
index 777f2b29a7..18d646a249 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -1818,15 +1818,28 @@ Origin Server Connect Attempts
The maximum number of connection retries |TS| can make when the origin server is not responding.
Each retry attempt lasts for `proxy.config.http.connect_attempts_timeout`_ seconds. Once the maximum number of retries is
reached, the origin is marked down (as controlled by `proxy.config.http.connect.down.policy`_. After this, the setting
- `proxy.config.http.connect_attempts_max_retries_down_server`_ is used to limit the number of retry attempts to the known down origin.
+ `proxy.config.http.connect_attempts_max_retries_suspect_server`_ is used to limit the number of retry attempts when the origin is
+ in the SUSPECT state (recovering after `proxy.config.http.down_server.cache_time`_ has elapsed).
+
+.. ts:cv:: CONFIG proxy.config.http.connect_attempts_max_retries_suspect_server INT 1
+ :reloadable:
+ :overridable:
+
+ Maximum number of connection retries |TS| can make while an origin is in the SUSPECT state (the first request after
+ `proxy.config.http.down_server.cache_time`_ has elapsed on a previously-down origin). The total attempt budget for a SUSPECT
+ origin is therefore ``connect_attempts_max_retries_suspect_server + 1`` (the initial probe plus each retry). If any attempt
+ succeeds, the origin transitions back to UP; if all attempts fail, the origin returns to DOWN for another
+ `proxy.config.http.down_server.cache_time`_ seconds. Typically smaller than `proxy.config.http.connect_attempts_max_retries`_
+ so the recovering origin is not flooded.
.. ts:cv:: CONFIG proxy.config.http.connect_attempts_max_retries_down_server INT 1
:reloadable:
:overridable:
+ :deprecated:
- Maximum number of connection attempts |TS| can make while an origin is marked down per request. Typically this value is smaller than
- `proxy.config.http.connect_attempts_max_retries`_ so an error is returned to the client faster and also to reduce the load on the down origin.
- The timeout interval `proxy.config.http.connect_attempts_timeout`_ in seconds is used with this setting.
+ This setting is deprecated in favor of :ts:cv:`proxy.config.http.connect_attempts_max_retries_suspect_server`. If the
+ deprecated setting is set explicitly and the replacement is not, the deprecated value is mirrored forward and a warning is
+ logged. If both are set explicitly, the new setting wins and the deprecated value is ignored.
.. ts:cv:: CONFIG proxy.config.http.connect_attempts_retry_backoff_base INT 0
:reloadable:
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index 623b40f3a1..43d98b40ad 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -65,140 +65,141 @@ Testing :enumerator:`TS_CONFIG_BODY_FACTORY_TEMPLATE_BASE`.
The following configurations (from ``records.yaml``) are overridable:
-====================================================================== ====================================================================
-TSOverridableConfigKey Value Configuration Value
-====================================================================== ====================================================================
-:enumerator:`TS_CONFIG_BODY_FACTORY_TEMPLATE_BASE` :ts:cv:`proxy.config.body_factory.template_base`
-:enumerator:`TS_CONFIG_HTTP_ALLOW_HALF_OPEN` :ts:cv:`proxy.config.http.allow_half_open`
-:enumerator:`TS_CONFIG_HTTP_ALLOW_MULTI_RANGE` :ts:cv:`proxy.config.http.allow_multi_range`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP` :ts:cv:`proxy.config.http.insert_client_ip`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_CLIENT_IP` :ts:cv:`proxy.config.http.anonymize_remove_client_ip`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_COOKIE` :ts:cv:`proxy.config.http.anonymize_remove_cookie`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_FROM` :ts:cv:`proxy.config.http.anonymize_remove_from`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_REFERER` :ts:cv:`proxy.config.http.anonymize_remove_referer`
-:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_USER_AGENT` :ts:cv:`proxy.config.http.anonymize_remove_user_agent`
-:enumerator:`TS_CONFIG_HTTP_ATTACH_SERVER_SESSION_TO_CLIENT` :ts:cv:`proxy.config.http.attach_server_session_to_client`
-:enumerator:`TS_CONFIG_HTTP_MAX_PROXY_CYCLES` :ts:cv:`proxy.config.http.max_proxy_cycles`
-:enumerator:`TS_CONFIG_HTTP_AUTH_SERVER_SESSION_PRIVATE` :ts:cv:`proxy.config.http.auth_server_session_private`
-:enumerator:`TS_CONFIG_HTTP_BACKGROUND_FILL_ACTIVE_TIMEOUT` :ts:cv:`proxy.config.http.background_fill_active_timeout`
-:enumerator:`TS_CONFIG_HTTP_BACKGROUND_FILL_COMPLETED_THRESHOLD` :ts:cv:`proxy.config.http.background_fill_completed_threshold`
-:enumerator:`TS_CONFIG_HTTP_CACHE_CACHE_RESPONSES_TO_COOKIES` :ts:cv:`proxy.config.http.cache.cache_responses_to_cookies`
-:enumerator:`TS_CONFIG_HTTP_CACHE_CACHE_URLS_THAT_LOOK_DYNAMIC` :ts:cv:`proxy.config.http.cache.cache_urls_that_look_dynamic`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_QUERY` :ts:cv:`proxy.config.http.cache.ignore_query`
-:enumerator:`TS_CONFIG_HTTP_CACHE_GENERATION` :ts:cv:`proxy.config.http.cache.generation`
-:enumerator:`TS_CONFIG_HTTP_CACHE_GUARANTEED_MAX_LIFETIME` :ts:cv:`proxy.config.http.cache.guaranteed_max_lifetime`
-:enumerator:`TS_CONFIG_HTTP_CACHE_GUARANTEED_MIN_LIFETIME` :ts:cv:`proxy.config.http.cache.guaranteed_min_lifetime`
-:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_LM_FACTOR` :ts:cv:`proxy.config.http.cache.heuristic_lm_factor`
-:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_MAX_LIFETIME` :ts:cv:`proxy.config.http.cache.heuristic_max_lifetime`
-:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_MIN_LIFETIME` :ts:cv:`proxy.config.http.cache.heuristic_min_lifetime`
-:enumerator:`TS_CONFIG_HTTP_CACHE_HTTP` :ts:cv:`proxy.config.http.cache.http`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_CHARSET_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_charset_mismatch`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_ENCODING_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_encoding_mismatch`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_LANGUAGE_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_language_mismatch`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_mismatch`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_AUTHENTICATION` :ts:cv:`proxy.config.http.cache.ignore_authentication`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_CLIENT_CC_MAX_AGE` :ts:cv:`proxy.config.http.cache.ignore_client_cc_max_age`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_CLIENT_NO_CACHE` :ts:cv:`proxy.config.http.cache.ignore_client_no_cache`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_SERVER_NO_CACHE` :ts:cv:`proxy.config.http.cache.ignore_server_no_cache`
-:enumerator:`TS_CONFIG_HTTP_CACHE_IMS_ON_CLIENT_NO_CACHE` :ts:cv:`proxy.config.http.cache.ims_on_client_no_cache`
-:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_OPEN_READ_RETRIES` :ts:cv:`proxy.config.http.cache.max_open_read_retries`
-:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_OPEN_WRITE_RETRIES` :ts:cv:`proxy.config.http.cache.max_open_write_retries`
-:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_STALE_AGE` :ts:cv:`proxy.config.http.cache.max_stale_age`
-:enumerator:`TS_CONFIG_HTTP_CACHE_OPEN_READ_RETRY_TIME` :ts:cv:`proxy.config.http.cache.open_read_retry_time`
-:enumerator:`TS_CONFIG_HTTP_CACHE_OPEN_WRITE_FAIL_ACTION` :ts:cv:`proxy.config.http.cache.open_write_fail_action`
-:enumerator:`TS_CONFIG_HTTP_CACHE_RANGE_LOOKUP` :ts:cv:`proxy.config.http.cache.range.lookup`
-:enumerator:`TS_CONFIG_HTTP_CACHE_RANGE_WRITE` :ts:cv:`proxy.config.http.cache.range.write`
-:enumerator:`TS_CONFIG_HTTP_CACHE_REQUIRED_HEADERS` :ts:cv:`proxy.config.http.cache.required_headers`
-:enumerator:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE` :ts:cv:`proxy.config.http.cache.when_to_revalidate`
-:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED` :ts:cv:`proxy.config.http.chunking_enabled`
-:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE` :ts:cv:`proxy.config.http.chunking.size`
-:enumerator:`TS_CONFIG_HTTP_STRICT_CHUNK_PARSING` :ts:cv:`proxy.config.http.strict_chunk_parsing`
-:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS` :ts:cv:`proxy.config.http.drop_chunked_trailers`
-:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER` :ts:cv:`proxy.config.http.connect_attempts_max_retries_down_server`
-:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES` :ts:cv:`proxy.config.http.connect_attempts_max_retries`
-:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES` :ts:cv:`proxy.config.http.connect_attempts_rr_retries`
-:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_TIMEOUT` :ts:cv:`proxy.config.http.connect_attempts_timeout`
-:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE` :ts:cv:`proxy.config.http.connect_attempts_retry_backoff_base`
-:enumerator:`TS_CONFIG_HTTP_DEFAULT_BUFFER_SIZE` :ts:cv:`proxy.config.http.default_buffer_size`
-:enumerator:`TS_CONFIG_HTTP_DEFAULT_BUFFER_WATER_MARK` :ts:cv:`proxy.config.http.default_buffer_water_mark`
-:enumerator:`TS_CONFIG_HTTP_DOC_IN_CACHE_SKIP_DNS` :ts:cv:`proxy.config.http.doc_in_cache_skip_dns`
-:enumerator:`TS_CONFIG_HTTP_DOWN_SERVER_CACHE_TIME` :ts:cv:`proxy.config.http.down_server.cache_time`
-:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED` :ts:cv:`proxy.config.http.flow_control.enabled`
-:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK` :ts:cv:`proxy.config.http.flow_control.high_water`
-:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK` :ts:cv:`proxy.config.http.flow_control.low_water`
-:enumerator:`TS_CONFIG_HTTP_FORWARD_CONNECT_METHOD` :ts:cv:`proxy.config.http.forward_connect_method`
-:enumerator:`TS_CONFIG_HTTP_FORWARD_PROXY_AUTH_TO_PARENT` :ts:cv:`proxy.config.http.forward.proxy_auth_to_parent`
-:enumerator:`TS_CONFIG_HTTP_GLOBAL_USER_AGENT_HEADER` :ts:cv:`proxy.config.http.global_user_agent_header`
-:enumerator:`TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE` :ts:cv:`proxy.config.http.insert_age_in_response`
-:enumerator:`TS_CONFIG_HTTP_INSERT_FORWARDED` :ts:cv:`proxy.config.http.insert_forwarded`
-:enumerator:`TS_CONFIG_HTTP_INSERT_REQUEST_VIA_STR` :ts:cv:`proxy.config.http.insert_request_via_str`
-:enumerator:`TS_CONFIG_HTTP_INSERT_RESPONSE_VIA_STR` :ts:cv:`proxy.config.http.insert_response_via_str`
-:enumerator:`TS_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR` :ts:cv:`proxy.config.http.insert_squid_x_forwarded_for`
-:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_IN` :ts:cv:`proxy.config.http.keep_alive_enabled_in`
-:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_OUT` :ts:cv:`proxy.config.http.keep_alive_enabled_out`
-:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_NO_ACTIVITY_TIMEOUT_IN` :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`
-:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_NO_ACTIVITY_TIMEOUT_OUT` :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_out`
-:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_POST_OUT` :ts:cv:`proxy.config.http.keep_alive_post_out`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_ENABLED` :ts:cv:`proxy.config.http.negative_caching_enabled`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_LIFETIME` :ts:cv:`proxy.config.http.negative_caching_lifetime`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_LIST` :ts:cv:`proxy.config.http.negative_caching_list`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_ENABLED` :ts:cv:`proxy.config.http.negative_revalidating_enabled`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIFETIME` :ts:cv:`proxy.config.http.negative_revalidating_lifetime`
-:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST` :ts:cv:`proxy.config.http.negative_revalidating_list`
-:enumerator:`TS_CONFIG_HTTP_NO_DNS_JUST_FORWARD_TO_PARENT` :ts:cv:`proxy.config.http.no_dns_just_forward_to_parent`
-:enumerator:`TS_CONFIG_HTTP_NORMALIZE_AE` :ts:cv:`proxy.config.http.normalize_ae`
-:enumerator:`TS_CONFIG_HTTP_NUMBER_OF_REDIRECTIONS` :ts:cv:`proxy.config.http.number_of_redirections`
-:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_FAIL_THRESHOLD` :ts:cv:`proxy.config.http.parent_proxy.fail_threshold`
-:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_RETRY_TIME` :ts:cv:`proxy.config.http.parent_proxy.retry_time`
-:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_TOTAL_CONNECT_ATTEMPTS` :ts:cv:`proxy.config.http.parent_proxy.total_connect_attempts`
-:enumerator:`TS_CONFIG_HTTP_PER_PARENT_CONNECT_ATTEMPTS` :ts:cv:`proxy.config.http.parent_proxy.per_parent_connect_attempts`
-:enumerator:`TS_CONFIG_HTTP_PER_SERVER_CONNECTION_MATCH` :ts:cv:`proxy.config.http.per_server.connection.match`
-:enumerator:`TS_CONFIG_HTTP_PER_SERVER_CONNECTION_MAX` :ts:cv:`proxy.config.http.per_server.connection.max`
-:enumerator:`TS_CONFIG_HTTP_POST_CHECK_CONTENT_LENGTH_ENABLED` :ts:cv:`proxy.config.http.post.check.content_length.enabled`
-:enumerator:`TS_CONFIG_HTTP_REDIRECT_USE_ORIG_CACHE_KEY` :ts:cv:`proxy.config.http.redirect_use_orig_cache_key`
-:enumerator:`TS_CONFIG_HTTP_REQUEST_BUFFER_ENABLED` :ts:cv:`proxy.config.http.request_buffer_enabled`
-:enumerator:`TS_CONFIG_HTTP_REQUEST_HEADER_MAX_SIZE` :ts:cv:`proxy.config.http.request_header_max_size`
-:enumerator:`TS_CONFIG_HTTP_RESPONSE_HEADER_MAX_SIZE` :ts:cv:`proxy.config.http.response_header_max_size`
-:enumerator:`TS_CONFIG_HTTP_RESPONSE_SERVER_ENABLED` :ts:cv:`proxy.config.http.response_server_enabled`
-:enumerator:`TS_CONFIG_HTTP_RESPONSE_SERVER_STR` :ts:cv:`proxy.config.http.response_server_str`
-:enumerator:`TS_CONFIG_HTTP_SEND_HTTP11_REQUESTS` :ts:cv:`proxy.config.http.send_http11_requests`
-:enumerator:`TS_CONFIG_HTTP_SERVER_SESSION_SHARING_MATCH` :ts:cv:`proxy.config.http.server_session_sharing.match`
-:enumerator:`TS_CONFIG_HTTP_SLOW_LOG_THRESHOLD` :ts:cv:`proxy.config.http.slow.log.threshold`
-:enumerator:`TS_CONFIG_HTTP_TRANSACTION_ACTIVE_TIMEOUT_IN` :ts:cv:`proxy.config.http.transaction_active_timeout_in`
-:enumerator:`TS_CONFIG_HTTP_TRANSACTION_ACTIVE_TIMEOUT_OUT` :ts:cv:`proxy.config.http.transaction_active_timeout_out`
-:enumerator:`TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_IN` :ts:cv:`proxy.config.http.transaction_no_activity_timeout_in`
-:enumerator:`TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT` :ts:cv:`proxy.config.http.transaction_no_activity_timeout_out`
-:enumerator:`TS_CONFIG_HTTP_UNCACHEABLE_REQUESTS_BYPASS_PARENT` :ts:cv:`proxy.config.http.uncacheable_requests_bypass_parent`
-:enumerator:`TS_CONFIG_NET_SOCK_OPTION_FLAG_OUT` :ts:cv:`proxy.config.net.sock_option_flag_out`
-:enumerator:`TS_CONFIG_NET_SOCK_PACKET_MARK_OUT` :ts:cv:`proxy.config.net.sock_packet_mark_out`
-:enumerator:`TS_CONFIG_NET_SOCK_PACKET_TOS_OUT` :ts:cv:`proxy.config.net.sock_packet_tos_out`
-:enumerator:`TS_CONFIG_NET_SOCK_RECV_BUFFER_SIZE_OUT` :ts:cv:`proxy.config.net.sock_recv_buffer_size_out`
-:enumerator:`TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT` :ts:cv:`proxy.config.net.default_inactivity_timeout`
-:enumerator:`TS_CONFIG_NET_SOCK_SEND_BUFFER_SIZE_OUT` :ts:cv:`proxy.config.net.sock_send_buffer_size_out`
-:enumerator:`TS_CONFIG_PARENT_FAILURES_UPDATE_HOSTDB` :ts:cv:`proxy.config.http.parent_proxy.mark_down_hostdb`
-:enumerator:`TS_CONFIG_SRV_ENABLED` :ts:cv:`proxy.config.srv_enabled`
-:enumerator:`TS_CONFIG_SSL_CLIENT_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.cert.filename`
-:enumerator:`TS_CONFIG_SSL_CERT_FILEPATH` :ts:cv:`proxy.config.ssl.client.cert.path`
-:enumerator:`TS_CONFIG_SSL_CLIENT_VERIFY_SERVER_PROPERTIES` :ts:cv:`proxy.config.ssl.client.verify.server.properties`
-:enumerator:`TS_CONFIG_SSL_CLIENT_VERIFY_SERVER_POLICY` :ts:cv:`proxy.config.ssl.client.verify.server.policy`
-:enumerator:`TS_CONFIG_SSL_CLIENT_SNI_POLICY` :ts:cv:`proxy.config.ssl.client.sni_policy`
-:enumerator:`TS_CONFIG_SSL_HSTS_INCLUDE_SUBDOMAINS` :ts:cv:`proxy.config.ssl.hsts_include_subdomains`
-:enumerator:`TS_CONFIG_SSL_HSTS_MAX_AGE` :ts:cv:`proxy.config.ssl.hsts_max_age`
-:enumerator:`TS_CONFIG_URL_REMAP_PRISTINE_HOST_HDR` :ts:cv:`proxy.config.url_remap.pristine_host_hdr`
-:enumerator:`TS_CONFIG_WEBSOCKET_ACTIVE_TIMEOUT` :ts:cv:`proxy.config.websocket.active_timeout`
-:enumerator:`TS_CONFIG_WEBSOCKET_NO_ACTIVITY_TIMEOUT` :ts:cv:`proxy.config.websocket.no_activity_timeout`
-:enumerator:`TS_CONFIG_SSL_CLIENT_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.cert.filename`
-:enumerator:`TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME` :ts:cv:`proxy.config.ssl.client.private_key.filename`
-:enumerator:`TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.CA.cert.filename`
-:enumerator:`TS_CONFIG_SSL_CLIENT_CA_CERT_PATH` :ts:cv:`proxy.config.ssl.client.CA.cert.path`
-:enumerator:`TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE` :ts:cv:`proxy.config.hostdb.ip_resolve`
-:enumerator:`TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_INDEX` :ts:cv:`proxy.config.plugin.vc.default_buffer_index`
-:enumerator:`TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK` :ts:cv:`proxy.config.plugin.vc.default_buffer_water_mark`
-:enumerator:`TS_CONFIG_NET_SOCK_NOTSENT_LOWAT` :ts:cv:`proxy.config.net.sock_notsent_lowat`
-:enumerator:`TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE` :ts:cv:`proxy.config.body_factory.response_suppression_mode`
-:enumerator:`TS_CONFIG_HTTP_CACHE_POST_METHOD` :ts:cv:`proxy.config.http.cache.post_method`
-:enumerator:`TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS` :ts:cv:`proxy.config.http.cache.targeted_cache_control_headers`
-====================================================================== ====================================================================
+======================================================================== ====================================================================
+TSOverridableConfigKey Value Configuration Value
+======================================================================== ====================================================================
+:enumerator:`TS_CONFIG_BODY_FACTORY_TEMPLATE_BASE` :ts:cv:`proxy.config.body_factory.template_base`
+:enumerator:`TS_CONFIG_HTTP_ALLOW_HALF_OPEN` :ts:cv:`proxy.config.http.allow_half_open`
+:enumerator:`TS_CONFIG_HTTP_ALLOW_MULTI_RANGE` :ts:cv:`proxy.config.http.allow_multi_range`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP` :ts:cv:`proxy.config.http.insert_client_ip`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_CLIENT_IP` :ts:cv:`proxy.config.http.anonymize_remove_client_ip`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_COOKIE` :ts:cv:`proxy.config.http.anonymize_remove_cookie`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_FROM` :ts:cv:`proxy.config.http.anonymize_remove_from`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_REFERER` :ts:cv:`proxy.config.http.anonymize_remove_referer`
+:enumerator:`TS_CONFIG_HTTP_ANONYMIZE_REMOVE_USER_AGENT` :ts:cv:`proxy.config.http.anonymize_remove_user_agent`
+:enumerator:`TS_CONFIG_HTTP_ATTACH_SERVER_SESSION_TO_CLIENT` :ts:cv:`proxy.config.http.attach_server_session_to_client`
+:enumerator:`TS_CONFIG_HTTP_MAX_PROXY_CYCLES` :ts:cv:`proxy.config.http.max_proxy_cycles`
+:enumerator:`TS_CONFIG_HTTP_AUTH_SERVER_SESSION_PRIVATE` :ts:cv:`proxy.config.http.auth_server_session_private`
+:enumerator:`TS_CONFIG_HTTP_BACKGROUND_FILL_ACTIVE_TIMEOUT` :ts:cv:`proxy.config.http.background_fill_active_timeout`
+:enumerator:`TS_CONFIG_HTTP_BACKGROUND_FILL_COMPLETED_THRESHOLD` :ts:cv:`proxy.config.http.background_fill_completed_threshold`
+:enumerator:`TS_CONFIG_HTTP_CACHE_CACHE_RESPONSES_TO_COOKIES` :ts:cv:`proxy.config.http.cache.cache_responses_to_cookies`
+:enumerator:`TS_CONFIG_HTTP_CACHE_CACHE_URLS_THAT_LOOK_DYNAMIC` :ts:cv:`proxy.config.http.cache.cache_urls_that_look_dynamic`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_QUERY` :ts:cv:`proxy.config.http.cache.ignore_query`
+:enumerator:`TS_CONFIG_HTTP_CACHE_GENERATION` :ts:cv:`proxy.config.http.cache.generation`
+:enumerator:`TS_CONFIG_HTTP_CACHE_GUARANTEED_MAX_LIFETIME` :ts:cv:`proxy.config.http.cache.guaranteed_max_lifetime`
+:enumerator:`TS_CONFIG_HTTP_CACHE_GUARANTEED_MIN_LIFETIME` :ts:cv:`proxy.config.http.cache.guaranteed_min_lifetime`
+:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_LM_FACTOR` :ts:cv:`proxy.config.http.cache.heuristic_lm_factor`
+:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_MAX_LIFETIME` :ts:cv:`proxy.config.http.cache.heuristic_max_lifetime`
+:enumerator:`TS_CONFIG_HTTP_CACHE_HEURISTIC_MIN_LIFETIME` :ts:cv:`proxy.config.http.cache.heuristic_min_lifetime`
+:enumerator:`TS_CONFIG_HTTP_CACHE_HTTP` :ts:cv:`proxy.config.http.cache.http`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_CHARSET_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_charset_mismatch`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_ENCODING_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_encoding_mismatch`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_LANGUAGE_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_language_mismatch`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_ACCEPT_MISMATCH` :ts:cv:`proxy.config.http.cache.ignore_accept_mismatch`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_AUTHENTICATION` :ts:cv:`proxy.config.http.cache.ignore_authentication`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_CLIENT_CC_MAX_AGE` :ts:cv:`proxy.config.http.cache.ignore_client_cc_max_age`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_CLIENT_NO_CACHE` :ts:cv:`proxy.config.http.cache.ignore_client_no_cache`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IGNORE_SERVER_NO_CACHE` :ts:cv:`proxy.config.http.cache.ignore_server_no_cache`
+:enumerator:`TS_CONFIG_HTTP_CACHE_IMS_ON_CLIENT_NO_CACHE` :ts:cv:`proxy.config.http.cache.ims_on_client_no_cache`
+:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_OPEN_READ_RETRIES` :ts:cv:`proxy.config.http.cache.max_open_read_retries`
+:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_OPEN_WRITE_RETRIES` :ts:cv:`proxy.config.http.cache.max_open_write_retries`
+:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_STALE_AGE` :ts:cv:`proxy.config.http.cache.max_stale_age`
+:enumerator:`TS_CONFIG_HTTP_CACHE_OPEN_READ_RETRY_TIME` :ts:cv:`proxy.config.http.cache.open_read_retry_time`
+:enumerator:`TS_CONFIG_HTTP_CACHE_OPEN_WRITE_FAIL_ACTION` :ts:cv:`proxy.config.http.cache.open_write_fail_action`
+:enumerator:`TS_CONFIG_HTTP_CACHE_RANGE_LOOKUP` :ts:cv:`proxy.config.http.cache.range.lookup`
+:enumerator:`TS_CONFIG_HTTP_CACHE_RANGE_WRITE` :ts:cv:`proxy.config.http.cache.range.write`
+:enumerator:`TS_CONFIG_HTTP_CACHE_REQUIRED_HEADERS` :ts:cv:`proxy.config.http.cache.required_headers`
+:enumerator:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE` :ts:cv:`proxy.config.http.cache.when_to_revalidate`
+:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED` :ts:cv:`proxy.config.http.chunking_enabled`
+:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE` :ts:cv:`proxy.config.http.chunking.size`
+:enumerator:`TS_CONFIG_HTTP_STRICT_CHUNK_PARSING` :ts:cv:`proxy.config.http.strict_chunk_parsing`
+:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS` :ts:cv:`proxy.config.http.drop_chunked_trailers`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER` :ts:cv:`proxy.config.http.connect_attempts_max_retries_down_server`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_SUSPECT_SERVER` :ts:cv:`proxy.config.http.connect_attempts_max_retries_suspect_server`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES` :ts:cv:`proxy.config.http.connect_attempts_max_retries`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES` :ts:cv:`proxy.config.http.connect_attempts_rr_retries`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_TIMEOUT` :ts:cv:`proxy.config.http.connect_attempts_timeout`
+:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE` :ts:cv:`proxy.config.http.connect_attempts_retry_backoff_base`
+:enumerator:`TS_CONFIG_HTTP_DEFAULT_BUFFER_SIZE` :ts:cv:`proxy.config.http.default_buffer_size`
+:enumerator:`TS_CONFIG_HTTP_DEFAULT_BUFFER_WATER_MARK` :ts:cv:`proxy.config.http.default_buffer_water_mark`
+:enumerator:`TS_CONFIG_HTTP_DOC_IN_CACHE_SKIP_DNS` :ts:cv:`proxy.config.http.doc_in_cache_skip_dns`
+:enumerator:`TS_CONFIG_HTTP_DOWN_SERVER_CACHE_TIME` :ts:cv:`proxy.config.http.down_server.cache_time`
+:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED` :ts:cv:`proxy.config.http.flow_control.enabled`
+:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK` :ts:cv:`proxy.config.http.flow_control.high_water`
+:enumerator:`TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK` :ts:cv:`proxy.config.http.flow_control.low_water`
+:enumerator:`TS_CONFIG_HTTP_FORWARD_CONNECT_METHOD` :ts:cv:`proxy.config.http.forward_connect_method`
+:enumerator:`TS_CONFIG_HTTP_FORWARD_PROXY_AUTH_TO_PARENT` :ts:cv:`proxy.config.http.forward.proxy_auth_to_parent`
+:enumerator:`TS_CONFIG_HTTP_GLOBAL_USER_AGENT_HEADER` :ts:cv:`proxy.config.http.global_user_agent_header`
+:enumerator:`TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE` :ts:cv:`proxy.config.http.insert_age_in_response`
+:enumerator:`TS_CONFIG_HTTP_INSERT_FORWARDED` :ts:cv:`proxy.config.http.insert_forwarded`
+:enumerator:`TS_CONFIG_HTTP_INSERT_REQUEST_VIA_STR` :ts:cv:`proxy.config.http.insert_request_via_str`
+:enumerator:`TS_CONFIG_HTTP_INSERT_RESPONSE_VIA_STR` :ts:cv:`proxy.config.http.insert_response_via_str`
+:enumerator:`TS_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR` :ts:cv:`proxy.config.http.insert_squid_x_forwarded_for`
+:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_IN` :ts:cv:`proxy.config.http.keep_alive_enabled_in`
+:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_OUT` :ts:cv:`proxy.config.http.keep_alive_enabled_out`
+:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_NO_ACTIVITY_TIMEOUT_IN` :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`
+:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_NO_ACTIVITY_TIMEOUT_OUT` :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_out`
+:enumerator:`TS_CONFIG_HTTP_KEEP_ALIVE_POST_OUT` :ts:cv:`proxy.config.http.keep_alive_post_out`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_ENABLED` :ts:cv:`proxy.config.http.negative_caching_enabled`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_LIFETIME` :ts:cv:`proxy.config.http.negative_caching_lifetime`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_CACHING_LIST` :ts:cv:`proxy.config.http.negative_caching_list`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_ENABLED` :ts:cv:`proxy.config.http.negative_revalidating_enabled`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIFETIME` :ts:cv:`proxy.config.http.negative_revalidating_lifetime`
+:enumerator:`TS_CONFIG_HTTP_NEGATIVE_REVALIDATING_LIST` :ts:cv:`proxy.config.http.negative_revalidating_list`
+:enumerator:`TS_CONFIG_HTTP_NO_DNS_JUST_FORWARD_TO_PARENT` :ts:cv:`proxy.config.http.no_dns_just_forward_to_parent`
+:enumerator:`TS_CONFIG_HTTP_NORMALIZE_AE` :ts:cv:`proxy.config.http.normalize_ae`
+:enumerator:`TS_CONFIG_HTTP_NUMBER_OF_REDIRECTIONS` :ts:cv:`proxy.config.http.number_of_redirections`
+:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_FAIL_THRESHOLD` :ts:cv:`proxy.config.http.parent_proxy.fail_threshold`
+:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_RETRY_TIME` :ts:cv:`proxy.config.http.parent_proxy.retry_time`
+:enumerator:`TS_CONFIG_HTTP_PARENT_PROXY_TOTAL_CONNECT_ATTEMPTS` :ts:cv:`proxy.config.http.parent_proxy.total_connect_attempts`
+:enumerator:`TS_CONFIG_HTTP_PER_PARENT_CONNECT_ATTEMPTS` :ts:cv:`proxy.config.http.parent_proxy.per_parent_connect_attempts`
+:enumerator:`TS_CONFIG_HTTP_PER_SERVER_CONNECTION_MATCH` :ts:cv:`proxy.config.http.per_server.connection.match`
+:enumerator:`TS_CONFIG_HTTP_PER_SERVER_CONNECTION_MAX` :ts:cv:`proxy.config.http.per_server.connection.max`
+:enumerator:`TS_CONFIG_HTTP_POST_CHECK_CONTENT_LENGTH_ENABLED` :ts:cv:`proxy.config.http.post.check.content_length.enabled`
+:enumerator:`TS_CONFIG_HTTP_REDIRECT_USE_ORIG_CACHE_KEY` :ts:cv:`proxy.config.http.redirect_use_orig_cache_key`
+:enumerator:`TS_CONFIG_HTTP_REQUEST_BUFFER_ENABLED` :ts:cv:`proxy.config.http.request_buffer_enabled`
+:enumerator:`TS_CONFIG_HTTP_REQUEST_HEADER_MAX_SIZE` :ts:cv:`proxy.config.http.request_header_max_size`
+:enumerator:`TS_CONFIG_HTTP_RESPONSE_HEADER_MAX_SIZE` :ts:cv:`proxy.config.http.response_header_max_size`
+:enumerator:`TS_CONFIG_HTTP_RESPONSE_SERVER_ENABLED` :ts:cv:`proxy.config.http.response_server_enabled`
+:enumerator:`TS_CONFIG_HTTP_RESPONSE_SERVER_STR` :ts:cv:`proxy.config.http.response_server_str`
+:enumerator:`TS_CONFIG_HTTP_SEND_HTTP11_REQUESTS` :ts:cv:`proxy.config.http.send_http11_requests`
+:enumerator:`TS_CONFIG_HTTP_SERVER_SESSION_SHARING_MATCH` :ts:cv:`proxy.config.http.server_session_sharing.match`
+:enumerator:`TS_CONFIG_HTTP_SLOW_LOG_THRESHOLD` :ts:cv:`proxy.config.http.slow.log.threshold`
+:enumerator:`TS_CONFIG_HTTP_TRANSACTION_ACTIVE_TIMEOUT_IN` :ts:cv:`proxy.config.http.transaction_active_timeout_in`
+:enumerator:`TS_CONFIG_HTTP_TRANSACTION_ACTIVE_TIMEOUT_OUT` :ts:cv:`proxy.config.http.transaction_active_timeout_out`
+:enumerator:`TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_IN` :ts:cv:`proxy.config.http.transaction_no_activity_timeout_in`
+:enumerator:`TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT` :ts:cv:`proxy.config.http.transaction_no_activity_timeout_out`
+:enumerator:`TS_CONFIG_HTTP_UNCACHEABLE_REQUESTS_BYPASS_PARENT` :ts:cv:`proxy.config.http.uncacheable_requests_bypass_parent`
+:enumerator:`TS_CONFIG_NET_SOCK_OPTION_FLAG_OUT` :ts:cv:`proxy.config.net.sock_option_flag_out`
+:enumerator:`TS_CONFIG_NET_SOCK_PACKET_MARK_OUT` :ts:cv:`proxy.config.net.sock_packet_mark_out`
+:enumerator:`TS_CONFIG_NET_SOCK_PACKET_TOS_OUT` :ts:cv:`proxy.config.net.sock_packet_tos_out`
+:enumerator:`TS_CONFIG_NET_SOCK_RECV_BUFFER_SIZE_OUT` :ts:cv:`proxy.config.net.sock_recv_buffer_size_out`
+:enumerator:`TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT` :ts:cv:`proxy.config.net.default_inactivity_timeout`
+:enumerator:`TS_CONFIG_NET_SOCK_SEND_BUFFER_SIZE_OUT` :ts:cv:`proxy.config.net.sock_send_buffer_size_out`
+:enumerator:`TS_CONFIG_PARENT_FAILURES_UPDATE_HOSTDB` :ts:cv:`proxy.config.http.parent_proxy.mark_down_hostdb`
+:enumerator:`TS_CONFIG_SRV_ENABLED` :ts:cv:`proxy.config.srv_enabled`
+:enumerator:`TS_CONFIG_SSL_CLIENT_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.cert.filename`
+:enumerator:`TS_CONFIG_SSL_CERT_FILEPATH` :ts:cv:`proxy.config.ssl.client.cert.path`
+:enumerator:`TS_CONFIG_SSL_CLIENT_VERIFY_SERVER_PROPERTIES` :ts:cv:`proxy.config.ssl.client.verify.server.properties`
+:enumerator:`TS_CONFIG_SSL_CLIENT_VERIFY_SERVER_POLICY` :ts:cv:`proxy.config.ssl.client.verify.server.policy`
+:enumerator:`TS_CONFIG_SSL_CLIENT_SNI_POLICY` :ts:cv:`proxy.config.ssl.client.sni_policy`
+:enumerator:`TS_CONFIG_SSL_HSTS_INCLUDE_SUBDOMAINS` :ts:cv:`proxy.config.ssl.hsts_include_subdomains`
+:enumerator:`TS_CONFIG_SSL_HSTS_MAX_AGE` :ts:cv:`proxy.config.ssl.hsts_max_age`
+:enumerator:`TS_CONFIG_URL_REMAP_PRISTINE_HOST_HDR` :ts:cv:`proxy.config.url_remap.pristine_host_hdr`
+:enumerator:`TS_CONFIG_WEBSOCKET_ACTIVE_TIMEOUT` :ts:cv:`proxy.config.websocket.active_timeout`
+:enumerator:`TS_CONFIG_WEBSOCKET_NO_ACTIVITY_TIMEOUT` :ts:cv:`proxy.config.websocket.no_activity_timeout`
+:enumerator:`TS_CONFIG_SSL_CLIENT_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.cert.filename`
+:enumerator:`TS_CONFIG_SSL_CLIENT_PRIVATE_KEY_FILENAME` :ts:cv:`proxy.config.ssl.client.private_key.filename`
+:enumerator:`TS_CONFIG_SSL_CLIENT_CA_CERT_FILENAME` :ts:cv:`proxy.config.ssl.client.CA.cert.filename`
+:enumerator:`TS_CONFIG_SSL_CLIENT_CA_CERT_PATH` :ts:cv:`proxy.config.ssl.client.CA.cert.path`
+:enumerator:`TS_CONFIG_HTTP_HOST_RESOLUTION_PREFERENCE` :ts:cv:`proxy.config.hostdb.ip_resolve`
+:enumerator:`TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_INDEX` :ts:cv:`proxy.config.plugin.vc.default_buffer_index`
+:enumerator:`TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK` :ts:cv:`proxy.config.plugin.vc.default_buffer_water_mark`
+:enumerator:`TS_CONFIG_NET_SOCK_NOTSENT_LOWAT` :ts:cv:`proxy.config.net.sock_notsent_lowat`
+:enumerator:`TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE` :ts:cv:`proxy.config.body_factory.response_suppression_mode`
+:enumerator:`TS_CONFIG_HTTP_CACHE_POST_METHOD` :ts:cv:`proxy.config.http.cache.post_method`
+:enumerator:`TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS` :ts:cv:`proxy.config.http.cache.targeted_cache_control_headers`
+======================================================================== ====================================================================
Examples
========
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index 9d200845c6..4ce7c1712d 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -79,6 +79,7 @@ Enumeration Members
.. enumerator:: TS_CONFIG_HTTP_ORIGIN_MAX_CONNECTIONS
.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES
.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER
+.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_SUSPECT_SERVER
.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES
.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_TIMEOUT
.. enumerator:: TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RETRY_BACKOFF_BASE
diff --git a/doc/release-notes/upgrading.en.rst b/doc/release-notes/upgrading.en.rst
index 5fc96c5818..c1318af79c 100644
--- a/doc/release-notes/upgrading.en.rst
+++ b/doc/release-notes/upgrading.en.rst
@@ -146,6 +146,11 @@ The following :file:`records.yaml` changes have been made:
- The records.yaml entry ``proxy.config.http.down_server.abort_threshold`` has been removed.
- The records.yaml entry ``proxy.config.http.connect_attempts_max_retries_dead_server`` has been renamed to :ts:cv:`proxy.config.http.connect_attempts_max_retries_down_server`.
+- The records.yaml entry ``proxy.config.http.connect_attempts_max_retries_down_server`` is now deprecated in favor of
+ :ts:cv:`proxy.config.http.connect_attempts_max_retries_suspect_server`. The new name aligns with the
+ ``HostDBInfo::State::SUSPECT`` state it actually applies to (a recovering origin allowed a limited probe budget after
+ :ts:cv:`proxy.config.http.down_server.cache_time` elapses). When only the deprecated record is set, its value is mirrored
+ forward to the new record and a warning is logged. When both are set, the new record wins.
- The entry ``proxy.config.http.connect.dead.policy`` has been renamed to :ts:cv:`proxy.config.http.connect.down.policy`.
- The records.yaml entry ``proxy.config.http.parent_proxy.connect_attempts_timeout`` and
``proxy.config.http.post_connect_attempts_timeout`` have been removed. Instead use
diff --git a/include/cripts/Configs.hpp b/include/cripts/Configs.hpp
index c2f4597a6a..79460327e7 100644
--- a/include/cripts/Configs.hpp
+++ b/include/cripts/Configs.hpp
@@ -154,6 +154,8 @@ private:
cripts::IntConfig connect_attempts_max_retries{"proxy.config.http.connect_attempts_max_retries"};
cripts::IntConfig connect_attempts_max_retries_down_server{"proxy.config.http.connect_attempts_max_retries_down_server"};
+ cripts::IntConfig connect_attempts_max_retries_suspect_server{
+ "proxy.config.http.connect_attempts_max_retries_suspect_server"};
cripts::IntConfig connect_attempts_rr_retries{"proxy.config.http.connect_attempts_rr_retries"};
cripts::IntConfig connect_attempts_timeout{"proxy.config.http.connect_attempts_timeout"};
cripts::IntConfig default_buffer_size{"proxy.config.http.default_buffer_size"};
diff --git a/include/iocore/hostdb/HostDBProcessor.h b/include/iocore/hostdb/HostDBProcessor.h
index c369aace4f..1a274a10e0 100644
--- a/include/iocore/hostdb/HostDBProcessor.h
+++ b/include/iocore/hostdb/HostDBProcessor.h
@@ -582,7 +582,7 @@ struct ResolveInfo {
bool mark_active_server_up();
/// Select / resolve to the next RR entry for the record.
- bool select_next_rr();
+ bool select_next_rr(ts_time now, ts_seconds fail_window);
bool is_srv() const;
};
diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h
index c5989c3620..a0e7ed7d9b 100644
--- a/include/proxy/http/HttpConfig.h
+++ b/include/proxy/http/HttpConfig.h
@@ -706,11 +706,11 @@ struct OverridableHttpConfigParams {
////////////////////////////////////
// origin server connect attempts //
////////////////////////////////////
- MgmtInt connect_attempts_max_retries = 0;
- MgmtInt connect_attempts_max_retries_down_server = 3;
- MgmtInt connect_attempts_rr_retries = 3;
- MgmtInt connect_attempts_timeout = 30;
- MgmtInt connect_attempts_retry_backoff_base = 0;
+ MgmtInt connect_attempts_max_retries = 0;
+ MgmtInt connect_attempts_max_retries_suspect_server = 1;
+ MgmtInt connect_attempts_rr_retries = 3;
+ MgmtInt connect_attempts_timeout = 30;
+ MgmtInt connect_attempts_retry_backoff_base = 0;
MgmtInt connect_down_policy = 2;
diff --git a/include/proxy/http/HttpTransact.h b/include/proxy/http/HttpTransact.h
index a6d48a12f7..9215f5508e 100644
--- a/include/proxy/http/HttpTransact.h
+++ b/include/proxy/http/HttpTransact.h
@@ -1003,7 +1003,6 @@ public:
static void Forbidden(State *s);
static void SelfLoop(State *s);
static void TooEarly(State *s);
- static void OriginDown(State *s);
static void PostActiveTimeoutResponse(State *s);
static void PostInactiveTimeoutResponse(State *s);
static void DecideCacheLookup(State *s);
@@ -1034,7 +1033,7 @@ public:
static void handle_response_from_parent_plugin(State *s);
static void handle_response_from_server(State *s);
static void delete_server_rr_entry(State *s, int max_retries);
- static void retry_server_connection_not_open(State *s, ServerState_t conn_state, unsigned max_retries);
+ static void retry_server_connection_not_open(State *s, unsigned max_retries);
static void error_log_connection_failure(State *s, ServerState_t conn_state);
static void handle_server_connection_not_open(State *s);
static void handle_forward_server_connection_open(State *s);
@@ -1078,6 +1077,8 @@ public:
static bool handle_trace_and_options_requests(State *s, HTTPHdr *incoming_hdr);
static void bootstrap_state_variables_from_request(State *s, HTTPHdr *incoming_request);
+ static uint8_t origin_server_connect_attempts_max_retries(State *s);
+
// WARNING: this function may be called multiple times for the same transaction.
//
static void initialize_state_variables_from_request(State *s, HTTPHdr *obsolete_incoming_request);
diff --git a/include/proxy/http/OverridableConfigDefs.h b/include/proxy/http/OverridableConfigDefs.h
index a21e57d70a..bf00fc3def 100644
--- a/include/proxy/http/OverridableConfigDefs.h
+++ b/include/proxy/http/OverridableConfigDefs.h
@@ -160,7 +160,8 @@
X(HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT, transaction_no_activity_timeout_out, "proxy.config.http.transaction_no_activity_timeout_out", INT, GENERIC) \
X(HTTP_TRANSACTION_ACTIVE_TIMEOUT_OUT, transaction_active_timeout_out, "proxy.config.http.transaction_active_timeout_out", INT, GENERIC) \
X(HTTP_CONNECT_ATTEMPTS_MAX_RETRIES, connect_attempts_max_retries, "proxy.config.http.connect_attempts_max_retries", INT, GENERIC) \
- X(HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER, connect_attempts_max_retries_down_server, "proxy.config.http.connect_attempts_max_retries_down_server", INT, GENERIC) \
+ X(HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER, connect_attempts_max_retries_suspect_server, "proxy.config.http.connect_attempts_max_retries_down_server", INT, GENERIC) \
+ X(HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_SUSPECT_SERVER, connect_attempts_max_retries_suspect_server, "proxy.config.http.connect_attempts_max_retries_suspect_server", INT, GENERIC) \
X(HTTP_CONNECT_ATTEMPTS_RR_RETRIES, connect_attempts_rr_retries, "proxy.config.http.connect_attempts_rr_retries", INT, GENERIC) \
X(HTTP_CONNECT_ATTEMPTS_TIMEOUT, connect_attempts_timeout, "proxy.config.http.connect_attempts_timeout", INT, GENERIC) \
X(HTTP_DOWN_SERVER_CACHE_TIME, down_server_timeout, "proxy.config.http.down_server.cache_time", INT, HttpDownServerCacheTimeConv) \
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index bce145c8a4..145f1e83a8 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -825,6 +825,7 @@ enum TSOverridableConfigKey {
TS_CONFIG_HTTP_TRANSACTION_ACTIVE_TIMEOUT_OUT,
TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES,
TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DOWN_SERVER,
+ TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_SUSPECT_SERVER,
TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES,
TS_CONFIG_HTTP_CONNECT_ATTEMPTS_TIMEOUT,
TS_CONFIG_HTTP_DOWN_SERVER_CACHE_TIME,
diff --git a/src/iocore/hostdb/HostDB.cc b/src/iocore/hostdb/HostDB.cc
index f225aeae1c..fc7bc40fea 100644
--- a/src/iocore/hostdb/HostDB.cc
+++ b/src/iocore/hostdb/HostDB.cc
@@ -1706,13 +1706,19 @@ ResolveInfo::set_active(HostDBInfo *info)
}
bool
-ResolveInfo::select_next_rr()
+ResolveInfo::select_next_rr(ts_time now, ts_seconds fail_window)
{
if (active) {
if (auto rr_info{this->record->rr_info()}; rr_info.count() > 1) {
- unsigned limit = active - rr_info.data(), idx = (limit + 1) % rr_info.count();
- while ((idx = (idx + 1) % rr_info.count()) != limit && !rr_info[idx].is_up()) {}
- active = &rr_info[idx];
+ const unsigned limit = active - rr_info.data();
+ size_t idx = (limit + 1) % rr_info.count();
+ for (; idx != limit; idx = (idx + 1) % rr_info.count()) {
+ if (!rr_info[idx].is_down(now, fail_window)) {
+ active = &rr_info[idx];
+ break;
+ }
+ }
+
return idx != limit; // if the active record was actually changed.
}
}
diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc
index aa9775eda1..2bfb487b43 100644
--- a/src/proxy/http/HttpConfig.cc
+++ b/src/proxy/http/HttpConfig.cc
@@ -1043,8 +1043,8 @@ HttpConfig::startup()
HttpEstablishStaticConfigFloat(c.oride.background_fill_threshold, "proxy.config.http.background_fill_completed_threshold");
HttpEstablishStaticConfigLongLong(c.oride.connect_attempts_max_retries, "proxy.config.http.connect_attempts_max_retries");
- HttpEstablishStaticConfigLongLong(c.oride.connect_attempts_max_retries_down_server,
- "proxy.config.http.connect_attempts_max_retries_down_server");
+ HttpEstablishStaticConfigLongLong(c.oride.connect_attempts_max_retries_suspect_server,
+ "proxy.config.http.connect_attempts_max_retries_suspect_server");
HttpEstablishStaticConfigLongLong(c.oride.connect_attempts_retry_backoff_base,
"proxy.config.http.connect_attempts_retry_backoff_base");
@@ -1340,14 +1340,46 @@ HttpConfig::reconfigure()
params->oride.background_fill_active_timeout = m_master.oride.background_fill_active_timeout;
params->oride.background_fill_threshold = m_master.oride.background_fill_threshold;
- params->oride.connect_attempts_max_retries = m_master.oride.connect_attempts_max_retries;
- params->oride.connect_attempts_max_retries_down_server = m_master.oride.connect_attempts_max_retries_down_server;
+ params->oride.connect_attempts_max_retries = m_master.oride.connect_attempts_max_retries;
+ params->oride.connect_attempts_max_retries_suspect_server = m_master.oride.connect_attempts_max_retries_suspect_server;
+
+ // Deprecation handling for connect_attempts_max_retries_down_server: if the operator explicitly set the deprecated record
+ // but did not set the replacement, mirror the value forward so existing configs keep working. If both are set, the new
+ // record wins and the deprecated one is ignored. Always warn when the deprecated record is explicitly set. The deprecated
+ // record has no bound struct field; fetch its value on demand from RecCore.
+ {
+ RecSourceT old_src = REC_SOURCE_NULL;
+ RecSourceT new_src = REC_SOURCE_NULL;
+ RecGetRecordSource("proxy.config.http.connect_attempts_max_retries_down_server", &old_src);
+ RecGetRecordSource("proxy.config.http.connect_attempts_max_retries_suspect_server", &new_src);
+ const bool old_explicit = (old_src == REC_SOURCE_EXPLICIT || old_src == REC_SOURCE_ENV);
+ const bool new_explicit = (new_src == REC_SOURCE_EXPLICIT || new_src == REC_SOURCE_ENV);
+ if (old_explicit && !new_explicit) {
+ RecInt deprecated_val = RecGetRecordInt("proxy.config.http.connect_attempts_max_retries_down_server")
+ .value_or(params->oride.connect_attempts_max_retries_suspect_server);
+ Warning("proxy.config.http.connect_attempts_max_retries_down_server is deprecated; "
+ "use proxy.config.http.connect_attempts_max_retries_suspect_server instead. "
+ "Using deprecated value %" PRIu64 " for now.",
+ deprecated_val);
+ params->oride.connect_attempts_max_retries_suspect_server = deprecated_val;
+ } else if (old_explicit && new_explicit) {
+ Warning("proxy.config.http.connect_attempts_max_retries_down_server is deprecated and is being ignored "
+ "in favor of proxy.config.http.connect_attempts_max_retries_suspect_server (%" PRIu64 ").",
+ m_master.oride.connect_attempts_max_retries_suspect_server);
+ }
+ }
+
if (m_master.oride.connect_attempts_rr_retries > params->oride.connect_attempts_max_retries) {
Warning("connect_attempts_rr_retries (%" PRIu64 ") is greater than "
"connect_attempts_max_retries (%" PRIu64 "), this means requests "
"will never redispatch to another server",
m_master.oride.connect_attempts_rr_retries, params->oride.connect_attempts_max_retries);
}
+ if (m_master.oride.connect_attempts_rr_retries > 0 && params->oride.connect_attempts_max_retries_suspect_server == 0) {
+ Warning("connect_attempts_max_retries_suspect_server=0 with round-robin enabled leaves no retry budget for recovering "
+ "(SUSPECT) origins beyond the initial attempt; "
+ "setting proxy.config.http.connect_attempts_max_retries_suspect_server >= 1 is recommended");
+ }
params->oride.connect_attempts_retry_backoff_base = m_master.oride.connect_attempts_retry_backoff_base;
params->oride.connect_attempts_rr_retries = m_master.oride.connect_attempts_rr_retries;
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 1b9accd99f..33480ae4a9 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -24,6 +24,7 @@
#include "proxy/http/HttpConfig.h"
#include "tscore/ink_hrtime.h"
+#include "tscore/ink_time.h"
#include "tsutil/Metrics.h"
#include "tsutil/StringCompare.h"
#include "tsutil/ts_bw_format.h"
@@ -4771,9 +4772,12 @@ HttpSM::do_hostdb_update_if_necessary()
t_state.dns_info.active->http_version = t_state.updated_server_version;
}
+ char addrbuf[INET6_ADDRPORTSTRLEN];
+ SMDbg(dbg_ctl_http, "update hostdb info: %s", ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf)));
+
// Check to see if we need to report or clear a connection failure
if (track_connect_fail()) {
- this->mark_host_failure(&t_state.dns_info, ts_clock::from_time_t(t_state.client_request_time));
+ this->mark_host_failure(&t_state.dns_info, ts_clock::now());
} else {
if (t_state.dns_info.mark_active_server_up()) {
char addrbuf[INET6_ADDRPORTSTRLEN];
@@ -4788,8 +4792,6 @@ HttpSM::do_hostdb_update_if_necessary()
}
}
- char addrbuf[INET6_ADDRPORTSTRLEN];
- SMDbg(dbg_ctl_http, "server info = %s", ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf)));
return;
}
@@ -5597,12 +5599,6 @@ HttpSM::do_http_server_open(bool raw, bool only_direct)
return;
}
}
- if (HttpTransact::is_server_negative_cached(&t_state) == true &&
- t_state.txn_conf->connect_attempts_max_retries_down_server <= 0) {
- SMDbg(dbg_ctl_http_seq, "Not connecting to the server because it is marked down.");
- call_transact_and_set_next_state(HttpTransact::OriginDown);
- return;
- }
// Check for self loop.
if (!_ua.get_txn()->is_outbound_transparent() && HttpTransact::will_this_request_self_loop(&t_state)) {
@@ -6053,34 +6049,36 @@ HttpSM::do_transform_open()
void
HttpSM::mark_host_failure(ResolveInfo *info, ts_time time_down)
{
- char addrbuf[INET6_ADDRPORTSTRLEN];
+ ink_assert(time_down != TS_TIME_ZERO);
- if (info->active) {
- if (time_down != TS_TIME_ZERO) {
- ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf));
- // Increment the fail_count
- if (auto [down, fail_count] = info->active->increment_fail_count(time_down, t_state.txn_conf->connect_attempts_rr_retries,
- t_state.txn_conf->down_server_timeout);
- down) {
- char *url_str = t_state.hdr_info.client_request.url_string_get_ref(nullptr);
- std::string_view host_name{t_state.unmapped_url.host_get()};
- swoc::bwprint(error_bw_buffer, "CONNECT : {::s} connecting to {} for host='{}' url='{}' fail_count='{}' marking down",
- swoc::bwf::Errno(t_state.current.server->connect_result), t_state.current.server->dst_addr, host_name,
- swoc::bwf::FirstOf(url_str, "<none>"), fail_count);
- Log::error("%s", error_bw_buffer.c_str());
- SMDbg(dbg_ctl_http, "hostdb update marking IP: %s as down", addrbuf);
- ATS_PROBE2(hostdb_mark_ip_as_down, sm_id, addrbuf);
- } else {
- ATS_PROBE3(hostdb_inc_ip_failcount, sm_id, addrbuf, fail_count);
- SMDbg(dbg_ctl_http, "hostdb increment IP failcount %s to %d", addrbuf, fail_count);
- }
- } else { // Clear the failure
- info->active->mark_up();
- }
+ if (info->active == nullptr) {
+ return;
+ }
+
+ char addrbuf[INET6_ADDRPORTSTRLEN];
+ ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf));
+
+ const uint8_t max_connect_retries = HttpTransact::origin_server_connect_attempts_max_retries(&t_state);
+ const ts_seconds fail_window = t_state.txn_conf->down_server_timeout;
+
+ // Mark the host DOWN only after every attempt has failed. `max_connect_retries` counts only "retries", so the total attempt
+ // budget is `max_connect_retries + 1` (the initial connect plus each retry).
+ auto [down, fail_count] = info->active->increment_fail_count(time_down, max_connect_retries + 1, fail_window);
+
+ if (down) {
+ Metrics::Counter::increment(http_rsb.down_server_no_requests);
+ char *url_str = t_state.hdr_info.client_request.url_string_get_ref(nullptr);
+ std::string_view host_name{t_state.unmapped_url.host_get()};
+ swoc::bwprint(error_bw_buffer, "CONNECT : {::s} connecting to {} for host='{}' url='{}' fail_count='{}' marking down",
+ swoc::bwf::Errno(t_state.current.server->connect_result), t_state.current.server->dst_addr, host_name,
+ swoc::bwf::FirstOf(url_str, "<none>"), fail_count);
+ Log::error("%s", error_bw_buffer.c_str());
+ SMDbg(dbg_ctl_http, "hostdb update marking IP: %s as down", addrbuf);
+ ATS_PROBE2(hostdb_mark_ip_as_down, sm_id, addrbuf);
+ } else {
+ ATS_PROBE3(hostdb_inc_ip_failcount, sm_id, addrbuf, fail_count);
+ SMDbg(dbg_ctl_http, "hostdb increment IP failcount %s to %d", addrbuf, fail_count);
}
-#ifdef DEBUG
- ink_assert(std::chrono::system_clock::now() + t_state.txn_conf->down_server_timeout > time_down);
-#endif
}
void
diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc
index 107a8dc823..9a3994f5a7 100644
--- a/src/proxy/http/HttpTransact.cc
+++ b/src/proxy/http/HttpTransact.cc
@@ -562,6 +562,41 @@ HttpTransact::is_server_negative_cached(State *s)
}
}
+/**
+ ATS has two configuration options controlling how many times it retries a connection attempt against origin servers.
+
+ - proxy.config.http.connect_attempts_max_retries
+ - proxy.config.http.connect_attempts_max_retries_suspect_server
+
+ The choice is based on the state of the active HostDBInfo.
+
+ - HostDBInfo::State::UP: use proxy.config.http.connect_attempts_max_retries
+ - HostDBInfo::State::DOWN: no retry
+ - HostDBInfo::State::SUSPECT: use proxy.config.http.connect_attempts_max_retries_suspect_server
+
+*/
+uint8_t
+HttpTransact::origin_server_connect_attempts_max_retries(State *s)
+{
+ HostDBInfo *active = s->dns_info.active;
+ if (active == nullptr) {
+ return 0;
+ }
+
+ switch (active->state(ts_clock::now(), s->txn_conf->down_server_timeout)) {
+ case HostDBInfo::State::UP:
+ return s->txn_conf->connect_attempts_max_retries;
+ case HostDBInfo::State::DOWN:
+ return 0;
+ case HostDBInfo::State::SUSPECT:
+ return s->txn_conf->connect_attempts_max_retries_suspect_server;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
inline static void
update_current_info(HttpTransact::CurrentInfo *into, HttpTransact::ConnectionAttributes *from,
ResolveInfo::UpstreamResolveStyle who, bool clear_retry_attempts)
@@ -974,22 +1009,6 @@ HttpTransact::TooEarly(State *s)
TRANSACT_RETURN(StateMachineAction_t::SEND_ERROR_CACHE_NOOP, nullptr);
}
-void
-HttpTransact::OriginDown(State *s)
-{
- TxnDbg(dbg_ctl_http_trans, "origin server is marked down");
- bootstrap_state_variables_from_request(s, &s->hdr_info.client_request);
- build_error_response(s, HTTPStatus::BAD_GATEWAY, "Origin Server Marked Down", "connect#failed_connect");
- Metrics::Counter::increment(http_rsb.down_server_no_requests);
- char *url_str = s->hdr_info.client_request.url_string_get_ref(nullptr);
- std::string_view host_name{s->unmapped_url.host_get()};
- swoc::bwprint(error_bw_buffer, "CONNECT: down server no request to {} for host='{}' url='{}'", s->current.server->dst_addr,
- host_name, swoc::bwf::FirstOf(url_str, "<none>"));
- Log::error("%s", error_bw_buffer.c_str());
-
- TRANSACT_RETURN(StateMachineAction_t::SEND_ERROR_CACHE_NOOP, nullptr);
-}
-
void
HttpTransact::HandleBlindTunnel(State *s)
{
@@ -1969,6 +1988,9 @@ HttpTransact::OSDNSLookup(State *s)
build_error_response(s, HTTPStatus::INTERNAL_SERVER_ERROR, "Cannot find server.", Dns_error_body);
log_msg = "looking up";
} else {
+ // HostDB has the record but every address is DOWN within `down_server.cache_time`. This is a refusal-to-attempt
+ // rather than a true DNS failure; track it under down_server_no_requests.
+ Metrics::Counter::increment(http_rsb.down_server_no_requests);
build_error_response(s, HTTPStatus::INTERNAL_SERVER_ERROR, "No valid server.", "connect#all_down");
log_msg = "no valid server";
}
@@ -1998,7 +2020,8 @@ HttpTransact::OSDNSLookup(State *s)
// We've backed off from a client supplied address and found some
// HostDB addresses. We use those if they're different from the CTA.
// In all cases we now commit to client or HostDB for our source.
- if (s->dns_info.set_active(&s->current.server->dst_addr.sa) && s->dns_info.select_next_rr()) {
+ if (s->dns_info.set_active(&s->current.server->dst_addr.sa) &&
+ s->dns_info.select_next_rr(ts_clock::now(), s->txn_conf->down_server_timeout)) {
s->dns_info.os_addr_style = ResolveInfo::OS_Addr::USE_HOSTDB;
} else {
// nothing else there, continue with CTA.
@@ -2687,6 +2710,7 @@ HttpTransact::CallOSDNSLookup(State *s)
} else {
s->cache_info.action = CacheAction_t::NO_ACTION;
}
+ error_log_connection_failure(s, s->current.state);
handle_server_connection_not_open(s);
} else {
TRANSACT_RETURN(StateMachineAction_t::DNS_LOOKUP, OSDNSLookup);
@@ -3882,6 +3906,7 @@ HttpTransact::handle_response_from_parent(State *s)
case ResolveInfo::HOST_NONE:
// Check if content can be served from cache
s->current.request_to = ResolveInfo::PARENT_PROXY;
+ error_log_connection_failure(s, s->current.state);
handle_server_connection_not_open(s);
break;
default:
@@ -3914,7 +3939,6 @@ HttpTransact::handle_response_from_server(State *s)
{
TxnDbg(dbg_ctl_http_trans, "(hrfs)");
HTTP_RELEASE_ASSERT(s->current.server == &s->server_info);
- unsigned max_connect_retries = 0;
// plugin call
s->server_info.state = s->current.state;
@@ -3933,6 +3957,7 @@ HttpTransact::handle_response_from_server(State *s)
TxnDbg(dbg_ctl_http_trans, "Error. congestion control -- congested.");
SET_VIA_STRING(VIA_DETAIL_SERVER_CONNECT, VIA_DETAIL_SERVER_FAILURE);
s->set_connect_fail(EUSERS); // too many users
+ error_log_connection_failure(s, s->current.state);
handle_server_connection_not_open(s);
break;
case OPEN_RAW_ERROR:
@@ -3941,8 +3966,7 @@ HttpTransact::handle_response_from_server(State *s)
case INACTIVE_TIMEOUT:
case PARSE_ERROR:
case CONNECTION_CLOSED:
- case BAD_INCOMING_RESPONSE:
-
+ case BAD_INCOMING_RESPONSE: {
// Ensure cause_of_death_errno is set for all error states if not already set.
// This prevents the assertion failure in retry_server_connection_not_open.
if (s->cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) {
@@ -3957,63 +3981,92 @@ HttpTransact::handle_response_from_server(State *s)
}
}
- if (is_server_negative_cached(s)) {
- max_connect_retries = s->txn_conf->connect_attempts_max_retries_down_server - 1;
- } else {
- // server not yet negative cached - use default number of retries
- max_connect_retries = s->txn_conf->connect_attempts_max_retries;
- }
-
+ unsigned max_connect_retries = s->txn_conf->connect_attempts_max_retries;
TxnDbg(dbg_ctl_http_trans, "max_connect_retries: %d s->current.retry_attempts: %d", max_connect_retries,
s->current.retry_attempts.get());
- if (is_request_retryable(s) && s->current.retry_attempts.get() < max_connect_retries &&
- !HttpTransact::is_response_valid(s, &s->hdr_info.server_response)) {
- // If this is a round robin DNS entry & we're tried configured
- // number of times, we should try another node
- if (ResolveInfo::OS_Addr::TRY_CLIENT == s->dns_info.os_addr_style) {
- // attempt was based on client supplied server address. Try again using HostDB.
- // Allow DNS attempt
- s->dns_info.resolved_p = false;
- // See if we can get data from HostDB for this.
- s->dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_HOSTDB;
- // Force host resolution to have the same family as the client.
- // Because this is a transparent connection, we can't switch address
- // families - that is locked in by the client source address.
- ats_force_order_by_family(s->current.server->dst_addr.family(), s->my_txn_conf().host_res_data.order);
- return CallOSDNSLookup(s);
- } else if (ResolveInfo::OS_Addr::USE_API == s->dns_info.os_addr_style && !s->api_server_addr_set_retried) {
- // Plugin set the server address via TSHttpTxnServerAddrSet(). Clear resolution
- // state to allow the OS_DNS hook to be called again, giving the plugin a chance
- // to set a different server address for retry (issue #12611).
- // Only retry once to avoid infinite loops if the plugin keeps setting failing addresses.
- s->api_server_addr_set_retried = true;
- s->dns_info.resolved_p = false;
- s->dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_DEFAULT;
- // Clear the server request so it can be rebuilt for the new destination
- s->hdr_info.server_request.destroy();
- TxnDbg(dbg_ctl_http_trans, "Retrying with plugin-set address, returning to OS_DNS hook");
- return CallOSDNSLookup(s);
- } else {
- if ((s->txn_conf->connect_attempts_rr_retries > 0) &&
- ((s->current.retry_attempts.get() + 1) % s->txn_conf->connect_attempts_rr_retries == 0)) {
- s->dns_info.select_next_rr();
+ // Bail out if the request is not retryable, the global retry cap is reached, or we already have a usable response.
+ if (!is_request_retryable(s) || s->current.retry_attempts.get() >= max_connect_retries ||
+ HttpTransact::is_response_valid(s, &s->hdr_info.server_response)) {
+ TxnDbg(dbg_ctl_http_trans, "Error. No more retries. %d/%d", s->current.retry_attempts.get(), max_connect_retries);
+ SET_VIA_STRING(VIA_DETAIL_SERVER_CONNECT, VIA_DETAIL_SERVER_FAILURE);
+ error_log_connection_failure(s, s->current.state);
+ s->state_machine->do_hostdb_update_if_necessary();
+ handle_server_connection_not_open(s);
+ break;
+ }
+
+ // Attempt was based on a client-supplied address. Re-resolve via HostDB.
+ if (ResolveInfo::OS_Addr::TRY_CLIENT == s->dns_info.os_addr_style) {
+ // Allow DNS attempt
+ s->dns_info.resolved_p = false;
+ // See if we can get data from HostDB for this.
+ s->dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_HOSTDB;
+ // Force host resolution to have the same family as the client.
+ // Because this is a transparent connection, we can't switch address
+ // families - that is locked in by the client source address.
+ ats_force_order_by_family(s->current.server->dst_addr.family(), s->my_txn_conf().host_res_data.order);
+ return CallOSDNSLookup(s);
+ }
+
+ // Plugin set the server address via TSHttpTxnServerAddrSet(). Clear resolution
+ // state to allow the OS_DNS hook to be called again, giving the plugin a chance
+ // to set a different server address for retry (issue #12611).
+ // Only retry once to avoid infinite loops if the plugin keeps setting failing addresses.
+ if (ResolveInfo::OS_Addr::USE_API == s->dns_info.os_addr_style && !s->api_server_addr_set_retried) {
+ s->api_server_addr_set_retried = true;
+ s->dns_info.resolved_p = false;
+ s->dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_DEFAULT;
+ // Clear the server request so it can be rebuilt for the new destination
+ s->hdr_info.server_request.destroy();
+ TxnDbg(dbg_ctl_http_trans, "Retrying with plugin-set address, returning to OS_DNS hook");
+ return CallOSDNSLookup(s);
+ }
+
+ // Record the failure on the current active target.
+ error_log_connection_failure(s, s->current.state);
+ s->state_machine->do_hostdb_update_if_necessary();
+
+ // Decide between switching to the next round-robin member or staying on the same target.
+ if ((s->txn_conf->connect_attempts_rr_retries > 0) &&
+ ((s->current.retry_attempts.get() + 1) % s->txn_conf->connect_attempts_rr_retries == 0)) {
+ if (s->dns_info.select_next_rr(ts_clock::now(), s->txn_conf->down_server_timeout)) {
+ // select_next_rr() only updates dns_info.active; change the dst_addr too.
+ s->dns_info.addr.assign(s->dns_info.active->data.ip);
+ s->server_info.dst_addr.assign(s->dns_info.active->data.ip, s->server_info.dst_addr.network_order_port());
+ if (dbg_ctl_http_trans.on()) {
+ ip_port_text_buffer addrbuf;
+ TxnDbg(dbg_ctl_http_trans, "switched to next round-robin upstream addr=%s",
+ ats_ip_nptop(&s->server_info.dst_addr.sa, addrbuf, sizeof(addrbuf)));
}
- retry_server_connection_not_open(s, s->current.state, max_connect_retries);
- TxnDbg(dbg_ctl_http_trans, "Error. Retrying...");
- s->next_action = how_to_open_connection(s);
+ } else {
+ TxnDbg(dbg_ctl_http_trans, "No round-robin targets available, retrying current upstream if possible");
}
- } else {
- error_log_connection_failure(s, s->current.state);
- TxnDbg(dbg_ctl_http_trans, "Error. No more retries.");
+ }
+
+ // The active target (HostDB) may be SUSPECT state, so re-evaluate the retry limit.
+ // Skip when there is no HostDBInfo (e.g. USE_CLIENT / USE_API) and keep the configured baseline.
+ if (s->dns_info.active != nullptr) {
+ max_connect_retries = origin_server_connect_attempts_max_retries(s);
+ }
+ if (max_connect_retries <= s->current.retry_attempts.get()) {
+ TxnDbg(dbg_ctl_http_trans, "Per-host retries exhausted. Giving up. %d/%d", s->current.retry_attempts.get(),
+ max_connect_retries);
SET_VIA_STRING(VIA_DETAIL_SERVER_CONNECT, VIA_DETAIL_SERVER_FAILURE);
handle_server_connection_not_open(s);
+ break;
}
+
+ TxnDbg(dbg_ctl_http_trans, "Error. Retrying...");
+ retry_server_connection_not_open(s, max_connect_retries);
+ s->next_action = how_to_open_connection(s);
break;
+ }
case ACTIVE_TIMEOUT:
TxnDbg(dbg_ctl_http_trans, "[hrfs] connection not alive");
SET_VIA_STRING(VIA_DETAIL_SERVER_CONNECT, VIA_DETAIL_SERVER_FAILURE);
s->set_connect_fail(ETIMEDOUT);
+ s->state_machine->do_hostdb_update_if_necessary();
handle_server_connection_not_open(s);
break;
default:
@@ -4062,15 +4115,13 @@ HttpTransact::error_log_connection_failure(State *s, ServerState_t conn_state)
//
///////////////////////////////////////////////////////////////////////////////
void
-HttpTransact::retry_server_connection_not_open(State *s, ServerState_t conn_state, unsigned max_retries)
+HttpTransact::retry_server_connection_not_open(State *s, unsigned max_retries)
{
ink_assert(s->current.state != CONNECTION_ALIVE);
ink_assert(s->current.state != ACTIVE_TIMEOUT);
ink_assert(s->current.retry_attempts.get() < max_retries);
ink_assert(s->cause_of_death_errno != -UNKNOWN_INTERNAL_ERROR);
- error_log_connection_failure(s, conn_state);
-
//////////////////////////////////////////////
// disable keep-alive for request and retry //
//////////////////////////////////////////////
@@ -4104,9 +4155,6 @@ HttpTransact::handle_server_connection_not_open(State *s)
SET_VIA_STRING(VIA_SERVER_RESULT, VIA_SERVER_ERROR);
Metrics::Counter::increment(http_rsb.broken_server_connections);
- // Fire off a hostdb update to mark the server as down
- s->state_machine->do_hostdb_update_if_necessary();
-
switch (s->cache_info.action) {
case CacheAction_t::UPDATE:
case CacheAction_t::SERVE:
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index e22948bd38..0ab6d1626a 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -490,11 +490,13 @@ static constexpr RecordElement RecordsConfig[] =
// ##################################
// # origin server connect attempts #
// ##################################
- {RECT_CONFIG, "proxy.config.http.connect_attempts_max_retries", RECD_INT, "3", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ {RECT_CONFIG, "proxy.config.http.connect_attempts_max_retries", RECD_INT, "3", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-254]", RECA_NULL}
,
- {RECT_CONFIG, "proxy.config.http.connect_attempts_max_retries_down_server", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ {RECT_CONFIG, "proxy.config.http.connect_attempts_max_retries_down_server", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-254]", RECA_NULL}
,
- {RECT_CONFIG, "proxy.config.http.connect_attempts_rr_retries", RECD_INT, "3", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ {RECT_CONFIG, "proxy.config.http.connect_attempts_max_retries_suspect_server", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-254]", RECA_NULL}
+ ,
+ {RECT_CONFIG, "proxy.config.http.connect_attempts_rr_retries", RECD_INT, "3", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-254]", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.http.connect_attempts_timeout", RECD_INT, "30", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
,
diff --git a/tests/gold_tests/autest-site/verifier_client.test.ext b/tests/gold_tests/autest-site/verifier_client.test.ext
index 53f0f5f269..60ca394227 100755
--- a/tests/gold_tests/autest-site/verifier_client.test.ext
+++ b/tests/gold_tests/autest-site/verifier_client.test.ext
@@ -35,7 +35,8 @@ def _configure_client(
verbose=True,
other_args='',
run_parallel=False,
- context=None):
+ context=None,
+ poll_timeout=None):
"""
Configure the process for running the verifier-client.
@@ -136,6 +137,9 @@ def _configure_client(
if keys is not None:
command += f" --keys {keys}"
+ if poll_timeout is not None:
+ command += f" --poll-timeout {poll_timeout}"
+
# Generally we prefer the deterministic behavior of a single thread where
# each connection is processed sequentially.
if not run_parallel and 'thread-limit' not in command:
@@ -161,7 +165,8 @@ def AddVerifierClientProcess(
verbose=True,
other_args='',
run_parallel=False,
- context=None):
+ context=None,
+ poll_timeout=None):
"""
Set the Default process of the test run to a verifier-client Process.
@@ -198,6 +203,10 @@ def AddVerifierClientProcess(
Template strings support $-based substitutions in the replay file.
You can refer to https://docs.python.org/3/library/string.html#template-strings
for more information how to add template strings to the replay file.
+
+ poll_timeout: (int) The poll timeout in milliseconds passed to
+ verifier-client via --poll-timeout. Defaults to None which leaves the
+ verifier-client default (3000 ms) in place.
Returns:
The newly constructed verifier-client for the test run, which is also the
Default Process of the test run.
@@ -213,7 +222,7 @@ def AddVerifierClientProcess(
p = run.Processes.Default
_configure_client(
run, p, name, replay_path, http_ports, https_ports, http3_ports, keys, ssl_cert, ca_cert, verbose, other_args, run_parallel,
- context)
+ context, poll_timeout)
return p
diff --git a/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml b/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml
index 554e90d138..45fe1d8e5d 100644
--- a/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml
+++ b/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml
@@ -48,7 +48,7 @@ autest:
proxy.config.http.connect.down.policy: 4
proxy.config.http.connect_attempts_rr_retries: 0
proxy.config.http.connect_attempts_max_retries: 0
- proxy.config.http.connect_attempts_max_retries_down_server: 0
+ proxy.config.http.connect_attempts_max_retries_suspect_server: 0
proxy.config.http.connect_attempts_timeout: 1
proxy.config.http.down_server.cache_time: 5
diff --git a/tests/gold_tests/dns/connect_attempts.test.py b/tests/gold_tests/dns/connect_attempts.test.py
index d46cb32d3d..e5b572d366 100644
--- a/tests/gold_tests/dns/connect_attempts.test.py
+++ b/tests/gold_tests/dns/connect_attempts.test.py
@@ -21,6 +21,11 @@ Test.Summary = '''
Verify Origin Server Connect Attempts Behavior
'''
+# Single DNS Record
+# max_retries & down_server
+Test.ATSReplayTest(replay_file="replay/connect_attempts_single_max_retries.replay.yaml")
+
+# Multiple DNS Records
# No retry
Test.ATSReplayTest(replay_file="replay/connect_attempts_rr_no_retry.replay.yaml")
diff --git a/tests/gold_tests/dns/dns_host_down.test.py b/tests/gold_tests/dns/dns_host_down.test.py
index 80b59e0fb8..295100c2fd 100644
--- a/tests/gold_tests/dns/dns_host_down.test.py
+++ b/tests/gold_tests/dns/dns_host_down.test.py
@@ -73,9 +73,23 @@ class DownCachedOriginServerTest:
self._ts.Disk.error_log.Content = Testers.ContainsExpression(
"/dns/mark/down' fail_count='1' marking down", "host should be marked down")
+ # Verify down_server_no_requests metric is incremented:
+ # - once when the first request marks the origin DOWN (502)
+ # - once when the second request is rejected because HostDB has no live address (500)
+ def _test_down_server_no_requests_metric(self):
+ tr = Test.AddTestRun('Check proxy.process.http.down_server.no_requests metric')
+ tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.http.down_server.no_requests'
+ tr.Processes.Default.Env = self._ts.Env
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+ 'proxy.process.http.down_server.no_requests 2',
+ 'down_server.no_requests should be 2 (mark-down on first txn, no-live-address on second)')
+ tr.StillRunningAfter = self._ts
+
def run(self):
self._test_host_mark_down()
self._test_error_log()
+ self._test_down_server_no_requests_metric()
DownCachedOriginServerTest().run()
diff --git a/tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold b/tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold
index 2172f69d46..bc2cbc1c80 100644
--- a/tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold
+++ b/tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold
@@ -1,8 +1,8 @@
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 `` retry_attempts=0 url='http://backend.example.com:``/path/'
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 `` retry_attempts=1 url='http://backend.example.com:``/path/'
-`` CONNECT : `` connecting to 0.0.0.1:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='1' marking down
+`` CONNECT : `` connecting to 0.0.0.1:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='2' marking down
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=1 `` retry_attempts=0 url='http://backend.example.com:``/path/'
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=1 `` retry_attempts=1 url='http://backend.example.com:``/path/'
-`` CONNECT : `` connecting to 0.0.0.2:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='1' marking down
+`` CONNECT : `` connecting to 0.0.0.2:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='2' marking down
`` DNS Error: no valid server http://backend.example.com:``/path/
``
diff --git a/tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold b/tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold
index c835cd3fa5..a4e061a67d 100644
--- a/tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold
+++ b/tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold
@@ -1,8 +1,9 @@
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 connection_result=`` error=`` retry_attempts=0 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=0 connection_result=`` error=`` retry_attempts=1 url='http://backend.example.com:``/path/'
`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=1 connection_result=`` error=`` retry_attempts=0 url='http://backend.example.com:``/path/'
`` CONNECT : `` connecting to 0.0.0.1:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='2' marking down
-`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=2 connection_result=`` error=`` retry_attempts=0 url='http://backend.example.com:``/path/'
-`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=3 connection_result=`` error=`` retry_attempts=0 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=1 connection_result=`` error=`` retry_attempts=1 url='http://backend.example.com:``/path/'
`` CONNECT : `` connecting to 0.0.0.2:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='2' marking down
`` DNS Error: no valid server http://backend.example.com:``/path/
-``
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=3 connection_result=`` error=`` retry_attempts=0 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.2:`` for host='example.com' sm_id=3 connection_result=`` error=`` retry_attempts=1 url='http://backend.example.com:``/path/'
diff --git a/tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold b/tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold
new file mode 100644
index 0000000000..5aef6b9655
--- /dev/null
+++ b/tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold
@@ -0,0 +1,9 @@
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 `` retry_attempts=0 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 `` retry_attempts=1 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=0 `` retry_attempts=2 url='http://backend.example.com:``/path/'
+`` CONNECT : `` connecting to 0.0.0.1:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='3' marking down
+`` DNS Error: no valid server http://backend.example.com:``/path/
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=2 `` retry_attempts=0 url='http://backend.example.com:``/path/'
+`` CONNECT: attempt fail [CONNECTION_ERROR] to 0.0.0.1:`` for host='example.com' sm_id=2 `` retry_attempts=1 url='http://backend.example.com:``/path/'
+`` CONNECT : `` connecting to 0.0.0.1:`` for host='example.com' url='http://backend.example.com:``/path/' fail_count='2' marking down
+`` DNS Error: no valid server http://backend.example.com:``/path/
diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml
index b1c918ab2c..834c60fb91 100644
--- a/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml
+++ b/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml
@@ -41,7 +41,7 @@ autest:
proxy.config.diags.debug.tags: 'http|hostdb|dns'
proxy.config.http.connect_attempts_rr_retries: 0
proxy.config.http.connect_attempts_max_retries: 1
- proxy.config.http.connect_attempts_max_retries_down_server: 0
+ proxy.config.http.connect_attempts_max_retries_suspect_server: 0
proxy.config.http.connect_attempts_timeout: 1
proxy.config.http.down_server.cache_time: 10
diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml
index b0ec1819eb..258653fbb8 100644
--- a/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml
+++ b/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml
@@ -41,7 +41,7 @@ autest:
proxy.config.diags.debug.tags: 'http|hostdb|dns'
proxy.config.http.connect_attempts_rr_retries: 0
proxy.config.http.connect_attempts_max_retries: 0
- proxy.config.http.connect_attempts_max_retries_down_server: 0
+ proxy.config.http.connect_attempts_max_retries_suspect_server: 0
proxy.config.http.connect_attempts_timeout: 1
proxy.config.http.down_server.cache_time: 5
diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml
index 0d040345ff..971e623ff3 100644
--- a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml
+++ b/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml
@@ -39,11 +39,11 @@ autest:
records_config:
proxy.config.diags.debug.enabled: 1
proxy.config.diags.debug.tags: 'http|hostdb|dns'
- proxy.config.http.connect_attempts_rr_retries: 2
- proxy.config.http.connect_attempts_max_retries: 0
- proxy.config.http.connect_attempts_max_retries_down_server: 0
+ proxy.config.http.connect_attempts_rr_retries: 1
+ proxy.config.http.connect_attempts_max_retries: 1
+ proxy.config.http.connect_attempts_max_retries_suspect_server: 1
proxy.config.http.connect_attempts_timeout: 1
- proxy.config.http.down_server.cache_time: 10
+ proxy.config.http.down_server.cache_time: 3
remap_config:
- from: "http://example.com/"
@@ -55,7 +55,9 @@ autest:
sessions:
- transactions:
- # try 0.0.0.1
+ # Both 0.0.0.1 and 0.0.0.2 are UP. ATS tries one host, then rr_retries to the
+ # other (connect_attempts_rr_retries=1). Both attempts fail, so the client
+ # gets 502. Each host's fail_count is now 1.
- client-request:
method: GET
url: /path/
@@ -73,7 +75,8 @@ sessions:
proxy-response:
status: 502
- # try 0.0.0.1 again
+ # Same round-robin path as above. After this attempt each host's fail_count
+ # reaches connect_attempts_max_retries + 1 (=2), so both are marked DOWN.
- client-request:
method: GET
url: /path/
@@ -91,7 +94,8 @@ sessions:
proxy-response:
status: 502
- # try 0.0.0.2
+ # Within down_server.cache_time (3s): both hosts are still cached as DOWN, so
+ # ATS skips the connect attempts entirely and returns 500.
- client-request:
method: GET
url: /path/
@@ -106,46 +110,11 @@ sessions:
status: 200
reason: OK
- proxy-response:
- status: 502
-
- # try 0.0.0.2 again
- - client-request:
- method: GET
- url: /path/
- version: '1.1'
- headers:
- fields:
- - [Host, example.com]
- - [uuid, 4]
-
- # should not hit
- server-response:
- status: 200
- reason: OK
-
- proxy-response:
- status: 502
-
- # The request is expected to hit the down_server cache and immediately receive a 500 response.
- - client-request:
- method: GET
- url: /path/
- version: '1.1'
- headers:
- fields:
- - [Host, example.com]
- - [uuid, 10]
-
- # should not hit
- server-response:
- status: 200
- reason: OK
-
proxy-response:
status: 500
- # when down_server.cache_time is expired, try connect attempts
+ # After the 5s delay, down_server.cache_time (3s) has expired. ATS tries both
+ # hosts again via round-robin; both still fail, so the client gets 502.
- client-request:
method: GET
url: /path/
@@ -154,7 +123,7 @@ sessions:
fields:
- [Host, example.com]
- [uuid, 20]
- delay: 10s
+ delay: 5s
# should not hit
server-response:
diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.replay.yaml
similarity index 63%
copy from tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml
copy to tests/gold_tests/dns/replay/connect_attempts_single_max_retries.replay.yaml
index 0d040345ff..85285c551a 100644
--- a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml
+++ b/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.replay.yaml
@@ -19,31 +19,33 @@ meta:
# Configuration section for autest integration
autest:
- description: 'Verify connect attempts behavior - round robin'
+ description: 'Verify connect attempts behavior - no round-robin (single host)'
dns:
- name: 'dns-rrr'
- records: {"backend.example.com": ["0.0.0.1", "0.0.0.2"]}
+ name: 'dns-single-retries'
+ records: {"backend.example.com": ["0.0.0.1"]}
server:
- name: 'server-rrr'
+ name: 'server-single-retries'
client:
- name: 'client-rrr'
+ name: 'client-single-retries'
+ process_config:
+ poll_timeout: 10000 # ms
ats:
- name: 'ts-rrr'
+ name: 'ts-single-retries'
process_config:
enable_cache: false
records_config:
proxy.config.diags.debug.enabled: 1
proxy.config.diags.debug.tags: 'http|hostdb|dns'
- proxy.config.http.connect_attempts_rr_retries: 2
- proxy.config.http.connect_attempts_max_retries: 0
- proxy.config.http.connect_attempts_max_retries_down_server: 0
+ proxy.config.http.connect_attempts_rr_retries: 99 # make sure this doesn't affect this case
+ proxy.config.http.connect_attempts_max_retries: 2
+ proxy.config.http.connect_attempts_max_retries_suspect_server: 1
proxy.config.http.connect_attempts_timeout: 1
- proxy.config.http.down_server.cache_time: 10
+ proxy.config.http.down_server.cache_time: 3
remap_config:
- from: "http://example.com/"
@@ -51,11 +53,12 @@ autest:
log_validation:
error_log:
- gold_file: "gold/connect_attempts_rr_retries_error_log.gold"
+ gold_file: "gold/connect_attempts_single_max_retries_error_log.gold"
sessions:
- transactions:
- # try 0.0.0.1
+ # First request: ATS attempts 3 times (1 initial + 2 retries) to connect to 0.0.0.1.
+ # The server is marked as down and ATS returns 502.
- client-request:
method: GET
url: /path/
@@ -73,7 +76,8 @@ sessions:
proxy-response:
status: 502
- # try 0.0.0.1 again
+ # Second request: within down_server.cache_time=3s so the server is still marked down.
+ # ATS skips the connection attempt entirely and returns 500.
- client-request:
method: GET
url: /path/
@@ -89,9 +93,11 @@ sessions:
reason: OK
proxy-response:
- status: 502
+ status: 500
- # try 0.0.0.2
+ # Third request: delayed 5s so the down_server.cache_time=3s has expired.
+ # ATS attempts 2 times (1 initial + 1 retries) to connect to 0.0.0.1.
+ # The server is marked as down again and ATS returns 502.
- client-request:
method: GET
url: /path/
@@ -100,6 +106,7 @@ sessions:
fields:
- [Host, example.com]
- [uuid, 3]
+ delay: 5s
# should not hit
server-response:
@@ -109,7 +116,8 @@ sessions:
proxy-response:
status: 502
- # try 0.0.0.2 again
+ # Fourth request: within down_server.cache_time=3s so the server is still marked down.
+ # ATS skips the connection attempt entirely and returns 500.
- client-request:
method: GET
url: /path/
@@ -119,47 +127,8 @@ sessions:
- [Host, example.com]
- [uuid, 4]
- # should not hit
server-response:
status: 200
- reason: OK
-
- proxy-response:
- status: 502
-
- # The request is expected to hit the down_server cache and immediately receive a 500 response.
- - client-request:
- method: GET
- url: /path/
- version: '1.1'
- headers:
- fields:
- - [Host, example.com]
- - [uuid, 10]
-
- # should not hit
- server-response:
- status: 200
- reason: OK
proxy-response:
status: 500
-
- # when down_server.cache_time is expired, try connect attempts
- - client-request:
- method: GET
- url: /path/
- version: '1.1'
- headers:
- fields:
- - [Host, example.com]
- - [uuid, 20]
- delay: 10s
-
- # should not hit
- server-response:
- status: 200
- reason: OK
-
- proxy-response:
- status: 502
(trafficserver) 01/33: header_rewrite: count operators and conditions run (#13286)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 3a5904aa193af345e0fac887cc11968280e817f8
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 16:00:53 2026 -0500
header_rewrite: count operators and conditions run (#13286)
The per-hook invocation count cannot tell a small ruleset from a large one,
so it does not reflect how much work header_rewrite does per transaction.
Add proxy.process.plugin.header_rewrite.{operators,conditions}, incremented
in the inline Operator::do_exec and Condition::do_eval chain walkers, to make
that workload visible. do_eval short-circuits, so only conditions actually
evaluated are counted. The counters are created once, from both TSPluginInit
and TSRemapInit.
(cherry picked from commit c6ec3adbb3e44ee47068112b16908e54cc6b22d7)
---
plugins/header_rewrite/condition.h | 4 +++
plugins/header_rewrite/header_rewrite.cc | 4 +++
plugins/header_rewrite/operator.h | 3 +++
plugins/header_rewrite/statement.cc | 18 +++++++++++++
plugins/header_rewrite/statement.h | 8 ++++++
tests/gold_tests/autest-site/ats_replay.test.ext | 30 ++++++++++++++++------
.../header_rewrite_bundle.replay.yaml | 6 +++++
7 files changed, 65 insertions(+), 8 deletions(-)
diff --git a/plugins/header_rewrite/condition.h b/plugins/header_rewrite/condition.h
index 1400f3433a..63f2da6773 100644
--- a/plugins/header_rewrite/condition.h
+++ b/plugins/header_rewrite/condition.h
@@ -49,6 +49,10 @@ public:
bool
do_eval(const Resources &res)
{
+ // In do_eval, so AND/OR short-circuited conditions aren't counted.
+ if (hrw_stat_conditions != nullptr) {
+ ts::Metrics::Counter::increment(hrw_stat_conditions);
+ }
bool rt = eval(res);
if (has_modifier(_mods, CondModifiers::NOT)) {
diff --git a/plugins/header_rewrite/header_rewrite.cc b/plugins/header_rewrite/header_rewrite.cc
index 16f8b0c617..9a43a73813 100644
--- a/plugins/header_rewrite/header_rewrite.cc
+++ b/plugins/header_rewrite/header_rewrite.cc
@@ -569,6 +569,8 @@ TSPluginInit(int argc, const char *argv[])
return;
}
+ init_hrw_work_stats();
+
std::string geoDBpath;
int inboundIpSource = 0;
int timezone = 0;
@@ -663,6 +665,8 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
Dbg(pi_dbg_ctl, "Remap plugin is successfully initialized");
+ init_hrw_work_stats();
+
return TS_SUCCESS;
}
diff --git a/plugins/header_rewrite/operator.h b/plugins/header_rewrite/operator.h
index bc23c93aab..a44211ba97 100644
--- a/plugins/header_rewrite/operator.h
+++ b/plugins/header_rewrite/operator.h
@@ -78,6 +78,9 @@ public:
unsigned
do_exec(const Resources &res) const
{
+ if (hrw_stat_operators != nullptr) {
+ ts::Metrics::Counter::increment(hrw_stat_operators);
+ }
unsigned no_reenable{exec(res) ? 0U : 1U};
if (nullptr != _next) {
no_reenable += static_cast<Operator *>(_next)->do_exec(res);
diff --git a/plugins/header_rewrite/statement.cc b/plugins/header_rewrite/statement.cc
index 6e2607da63..8bce404a2d 100644
--- a/plugins/header_rewrite/statement.cc
+++ b/plugins/header_rewrite/statement.cc
@@ -21,6 +21,24 @@
//
#include "statement.h"
+ts::Metrics::Counter::AtomicType *hrw_stat_operators = nullptr;
+ts::Metrics::Counter::AtomicType *hrw_stat_conditions = nullptr;
+
+void
+init_hrw_work_stats()
+{
+ // createPtr() is idempotent and internally synchronized: it returns the existing counter when
+ // one already has the given name. header_rewrite.so can be loaded both globally (TSPluginInit)
+ // and as a remap plugin (TSRemapInit), so init runs more than once; every call resolves to the
+ // same process-wide counter regardless of ordering or which thread runs it.
+ if (nullptr == hrw_stat_operators) {
+ hrw_stat_operators = ts::Metrics::Counter::createPtr("proxy.process.plugin.header_rewrite.operators");
+ }
+ if (nullptr == hrw_stat_conditions) {
+ hrw_stat_conditions = ts::Metrics::Counter::createPtr("proxy.process.plugin.header_rewrite.conditions");
+ }
+}
+
void
Statement::append(Statement *stmt)
{
diff --git a/plugins/header_rewrite/statement.h b/plugins/header_rewrite/statement.h
index 2557d4482c..1f16c24246 100644
--- a/plugins/header_rewrite/statement.h
+++ b/plugins/header_rewrite/statement.h
@@ -27,11 +27,19 @@
#include <vector>
#include "ts/ts.h"
+#include "tsutil/Metrics.h"
#include "resources.h"
#include "parser.h"
#include "lulu.h"
+// Counters for the number of header_rewrite operators executed and conditions evaluated; the
+// bare hook invocation count cannot tell a small ruleset from a large one. Created once via
+// init_hrw_work_stats(); both are null until then, so increments are guarded.
+extern ts::Metrics::Counter::AtomicType *hrw_stat_operators;
+extern ts::Metrics::Counter::AtomicType *hrw_stat_conditions;
+void init_hrw_work_stats();
+
namespace header_rewrite_ns
{
constexpr int NUM_STATE_FLAGS = 16;
diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext b/tests/gold_tests/autest-site/ats_replay.test.ext
index c19ff58967..3f12395711 100644
--- a/tests/gold_tests/autest-site/ats_replay.test.ext
+++ b/tests/gold_tests/autest-site/ats_replay.test.ext
@@ -322,14 +322,28 @@ def ATSReplayTest(obj, replay_file: str):
for check in metric_checks:
metric_name = check['metric']
- expected_value = check['value']
- check_tr = obj.AddTestRun(f'Verify {metric_name} == {expected_value}')
- check_tr.Processes.Default.Command = f'traffic_ctl metric get {metric_name}'
- check_tr.Processes.Default.Env = ts.Env
- check_tr.Processes.Default.ReturnCode = 0
- check_tr.Processes.Default.Streams.All = Testers.ContainsExpression(
- f'^{re.escape(metric_name)}\\s+{expected_value}$', f'{metric_name} should be {expected_value}')
- check_tr.StillRunningAfter = ts
+ if 'value' in check:
+ expected_value = check['value']
+ check_tr = obj.AddTestRun(f'Verify {metric_name} == {expected_value}')
+ check_tr.Processes.Default.Command = f'traffic_ctl metric get {metric_name}'
+ check_tr.Processes.Default.Env = ts.Env
+ check_tr.Processes.Default.ReturnCode = 0
+ check_tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+ f'^{re.escape(metric_name)}\\s+{expected_value}$', f'{metric_name} should be {expected_value}')
+ check_tr.StillRunningAfter = ts
+ elif 'min' in check:
+ minimum = check['min']
+ check_tr = obj.AddTestRun(f'Verify {metric_name} >= {minimum}')
+ # Fail if the metric value (field 2) is below the minimum, or absent.
+ # $$ escapes the awk field ref from autest's command templating.
+ check_tr.Processes.Default.Command = (
+ f"traffic_ctl metric get {metric_name} | "
+ f"awk 'BEGIN {{ rc = 1 }} {{ if ($$2 + 0 >= {minimum}) rc = 0 }} END {{ exit rc }}'")
+ check_tr.Processes.Default.Env = ts.Env
+ check_tr.Processes.Default.ReturnCode = 0
+ check_tr.StillRunningAfter = ts
+ else:
+ raise ValueError(f"metric_checks entry for '{metric_name}' must specify 'value' or 'min'")
# wait for error log
if log_validation and log_validation.get('error_log'):
diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
index 97e1d6d285..b444acaae8 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
+++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
@@ -176,6 +176,12 @@ autest:
args:
- "rules/rule_session_vars.conf"
+ metric_checks:
+ - metric: "proxy.process.plugin.header_rewrite.operators"
+ min: 1
+ - metric: "proxy.process.plugin.header_rewrite.conditions"
+ min: 1
+
log_validation:
traffic_out:
excludes:
(trafficserver) 08/33: logging: add a marshalled-bytes counter (#13277)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 197ecbdeb00618b48af8b85b710103e253fb53f6
Author: Mo Chen <mo...@apache.org>
AuthorDate: Fri Jun 19 12:53:42 2026 -0500
logging: add a marshalled-bytes counter (#13277)
Adds proxy.process.log.marshalled_bytes, a counter that helps with estimating the cost of logging on an ATS instance.
(cherry picked from commit 46346ba4020f15a1e8e2e92b43baa51bf6df0f31)
---
include/proxy/logging/LogConfig.h | 1 +
src/proxy/logging/LogConfig.cc | 1 +
src/proxy/logging/LogObject.cc | 2 ++
3 files changed, 4 insertions(+)
diff --git a/include/proxy/logging/LogConfig.h b/include/proxy/logging/LogConfig.h
index 47a83d7693..f707f83755 100644
--- a/include/proxy/logging/LogConfig.h
+++ b/include/proxy/logging/LogConfig.h
@@ -48,6 +48,7 @@ struct LogsStatsBlock {
Metrics::Counter::AtomicType *event_log_access_aggr;
Metrics::Counter::AtomicType *event_log_access_full;
Metrics::Counter::AtomicType *event_log_access_fail;
+ Metrics::Counter::AtomicType *marshalled_bytes;
Metrics::Counter::AtomicType *num_sent_to_network;
Metrics::Counter::AtomicType *num_lost_before_sent_to_network;
Metrics::Counter::AtomicType *num_received_from_network;
diff --git a/src/proxy/logging/LogConfig.cc b/src/proxy/logging/LogConfig.cc
index dfa20f7d51..85d696b7d8 100644
--- a/src/proxy/logging/LogConfig.cc
+++ b/src/proxy/logging/LogConfig.cc
@@ -483,6 +483,7 @@ LogConfig::register_stat_callbacks()
log_rsb.event_log_access_aggr = Metrics::Counter::createPtr("proxy.process.log.event_log_access_aggr");
log_rsb.event_log_access_full = Metrics::Counter::createPtr("proxy.process.log.event_log_access_full");
log_rsb.event_log_access_fail = Metrics::Counter::createPtr("proxy.process.log.event_log_access_fail");
+ log_rsb.marshalled_bytes = Metrics::Counter::createPtr("proxy.process.log.marshalled_bytes");
log_rsb.num_sent_to_network = Metrics::Counter::createPtr("proxy.process.log.num_sent_to_network");
log_rsb.num_lost_before_sent_to_network = Metrics::Counter::createPtr("proxy.process.log.num_lost_before_sent_to_network");
log_rsb.num_received_from_network = Metrics::Counter::createPtr("proxy.process.log.num_received_from_network");
diff --git a/src/proxy/logging/LogObject.cc b/src/proxy/logging/LogObject.cc
index 997e394025..6cbbc65536 100644
--- a/src/proxy/logging/LogObject.cc
+++ b/src/proxy/logging/LogObject.cc
@@ -677,6 +677,8 @@ LogObject::log(LogAccess *lad, std::string_view text_entry)
} else if (lad) {
bytes_used = m_format->m_field_list.marshal(lad, &(*buffer)[offset]);
ink_assert(bytes_needed >= bytes_used);
+ // Count only entries that were successfully checked out and marshalled, not dropped ones.
+ Metrics::Counter::increment(log_rsb.marshalled_bytes, bytes_needed);
} else if (!text_entry.empty()) {
char *dst = &(*buffer)[offset];
memcpy(dst, text_entry.data(), text_entry.size());
(trafficserver) 30/33: pqsi-pqsp.test.py: address log order flakiness (#13321)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 3df7f9e3d95d4645797792518d9145127ae474bf
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Jun 24 20:38:37 2026 -0500
pqsi-pqsp.test.py: address log order flakiness (#13321)
The miss and hit transactions can flush to the custom log in either
order. A valid cache-hit result can therefore fail when only the first
line is normalized before the full-file comparison.
This waits for both expected records, normalizes the variable client
endpoint wherever it appears, and sorts the two-record output before
comparing it with the sorted gold file.
Fixes: #13319
(cherry picked from commit e8a0dd868edcb0185d38fc3570f8f0b0faf3a575)
---
tests/gold_tests/logging/gold/pqsi-pqsp.gold | 2 +-
tests/gold_tests/logging/pqsi-pqsp.test.py | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/tests/gold_tests/logging/gold/pqsi-pqsp.gold b/tests/gold_tests/logging/gold/pqsi-pqsp.gold
index 8238b1cbde..fa15ccf272 100644
--- a/tests/gold_tests/logging/gold/pqsi-pqsp.gold
+++ b/tests/gold_tests/logging/gold/pqsi-pqsp.gold
@@ -1,2 +1,2 @@
-abc
0 0
+abc
diff --git a/tests/gold_tests/logging/pqsi-pqsp.test.py b/tests/gold_tests/logging/pqsi-pqsp.test.py
index 6dc33ea68e..9ad5785541 100644
--- a/tests/gold_tests/logging/pqsi-pqsp.test.py
+++ b/tests/gold_tests/logging/pqsi-pqsp.test.py
@@ -78,10 +78,12 @@ tr.Processes.Default.ReturnCode = 0
log_filespec = os.path.join(ts.Variables.LOGDIR, 'field-test.log')
-# Wait for the cache-hit line to be written.
+# Wait for both transaction log lines to be written. Their write order is not
+# deterministic.
+Test.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-miss line.', log_filespec, r'^127\.0\.0\.1 [1-6][0-9]*$')
Test.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-hit line.', log_filespec, r'^0 0$')
tr = Test.AddTestRun()
-tr.Processes.Default.Command = "sed '1s/^127.0.0.1 [1-6][0-9]*$$/abc/' < " + log_filespec
+tr.Processes.Default.Command = "sed 's/^127\\.0\\.0\\.1 [1-6][0-9]*$$/abc/' < " + log_filespec + " | LC_ALL=C sort"
tr.Processes.Default.Streams.stdout = "gold/pqsi-pqsp.gold"
tr.Processes.Default.ReturnCode = 0
(trafficserver) 24/33: tests: add TLS gold tests (#13306)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 7d95f45b63d29bf054529cf32e38444c62315ac5
Author: Mo Chen <mo...@apache.org>
AuthorDate: Thu Jun 25 11:59:38 2026 -0500
tests: add TLS gold tests (#13306)
Increase autest coverage with the following tests:
- TLS renegotiation, rejected by default and allowed when configured
- the TLS record-size clamp, checked against on-the-wire record sizes
- a failed outbound TLS origin connection surfaced as a 5xx
- a TLS origin that resets the connection mid-request-body
(cherry picked from commit 3ce2acfbc6cf287e49638c4b054f48711c09a7e7)
---
AGENTS.md | 7 +
tests/gold_tests/tls/tls_flow_control.test.py | 116 +++++++++++++
tests/gold_tests/tls/tls_flow_control_client.py | 100 +++++++++++
.../gold_tests/tls/tls_origin_open_failed.test.py | 78 +++++++++
tests/gold_tests/tls/tls_origin_post_abort.test.py | 114 +++++++++++++
tests/gold_tests/tls/tls_post_abort_client.py | 127 ++++++++++++++
tests/gold_tests/tls/tls_post_abort_origin.py | 92 +++++++++++
tests/gold_tests/tls/tls_record_size.test.py | 117 +++++++++++++
tests/gold_tests/tls/tls_record_size_client.py | 155 +++++++++++++++++
tests/gold_tests/tls/tls_reload_under_load.test.py | 100 +++++++++++
.../gold_tests/tls/tls_reload_under_load_client.py | 183 +++++++++++++++++++++
tests/gold_tests/tls/tls_renegotiation.test.py | 124 ++++++++++++++
.../tls/tls_renegotiation_allowed.test.py | 145 ++++++++++++++++
.../tls/tls_renegotiation_allowed_client.py | 132 +++++++++++++++
tests/gold_tests/tls/tls_renegotiation_client.py | 67 ++++++++
15 files changed, 1657 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 75308cecdf..b686484d67 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -123,6 +123,13 @@ regularly).
using the Proxy Verifier format. This is simpler, more maintainable, and
parseable by tools.
+**Python conventions for test and helper scripts:**
+- Launch Python helpers with `{sys.executable}` rather than a hardcoded `python3`,
+ so the test runs under the same interpreter the harness uses.
+- Prefer f-strings over `str.format()` when building command lines, config lines,
+ and `Testers` expressions.
+- Add type annotations to helper functions.
+
**For complete details on writing autests, see:**
- `doc/developer-guide/testing/autests.en.rst` - Comprehensive guide to autest
- Proxy Verifier format: https://github.com/yahoo/proxy-verifier
diff --git a/tests/gold_tests/tls/tls_flow_control.test.py b/tests/gold_tests/tls/tls_flow_control.test.py
new file mode 100644
index 0000000000..f1da922182
--- /dev/null
+++ b/tests/gold_tests/tls/tls_flow_control.test.py
@@ -0,0 +1,116 @@
+'''
+Exercise HTTP tunnel flow control (proxy.config.http.flow_control) on a TLS
+client connection: a slow reader makes ATS throttle the origin and unthrottle as
+the buffered data drains. The full response body must still be delivered.
+'''
+# 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.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+
+class TestTlsFlowControl:
+ '''Verify a slow TLS reader under flow control still receives the whole body.'''
+
+ # Comfortably larger than the water marks so the tunnel throttles and has to
+ # unthrottle many times over the transfer.
+ _body_len: int = 8 * 1024 * 1024
+ _high_water: int = 64 * 1024
+ _low_water: int = 32 * 1024
+
+ _server_counter: int = 0
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._server = self._configure_server()
+ self._ts = self._configure_trafficserver()
+
+ def _configure_server(self) -> 'Process':
+ '''Configure the origin server with a large response body.
+
+ :return: The origin server Process.
+ '''
+ server = Test.MakeOriginServer(f'server-{TestTlsFlowControl._server_counter}')
+ TestTlsFlowControl._server_counter += 1
+
+ request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n"
+ f"Content-Length: {TestTlsFlowControl._body_len}\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": "x" * TestTlsFlowControl._body_len
+ }
+ server.addResponse("sessionlog.json", request_header, response_header)
+ return server
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server with HTTP flow control enabled over TLS.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestTlsFlowControl._ts_counter}', enable_tls=True, enable_cache=False)
+ TestTlsFlowControl._ts_counter += 1
+
+ ts.addDefaultSSLFiles()
+ ts.Disk.ssl_multicert_yaml.AddLines(
+ """
+ssl_multicert:
+ - dest_ip: "*"
+ ssl_cert_name: server.pem
+ ssl_key_name: server.key
+""".split("\n"))
+ ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}')
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ # Small thresholds on a network sink: the tunnel reenable fires before
+ # the socket write, so the unthrottle depends on the buffer draining.
+ 'proxy.config.http.flow_control.enabled': 1,
+ 'proxy.config.http.flow_control.high_water': TestTlsFlowControl._high_water,
+ 'proxy.config.http.flow_control.low_water': TestTlsFlowControl._low_water,
+ })
+
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "received signal|failed assertion", "ATS must not crash under TLS flow control")
+ ts.Disk.traffic_out.Content += Testers.ExcludesExpression(
+ "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error under TLS flow control")
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRun.'''
+ tr = Test.AddTestRun("slow TLS reader under flow control must receive the whole body")
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_flow_control_client.py")} '
+ f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj '
+ f'--expect-bytes {TestTlsFlowControl._body_len}')
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+ "RESULT=PASS", "the full body must be delivered under flow control")
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+ f"BODY_BYTES={TestTlsFlowControl._body_len}", "every body byte must arrive (no flow-control stall)")
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+
+TestTlsFlowControl().run()
diff --git a/tests/gold_tests/tls/tls_flow_control_client.py b/tests/gold_tests/tls/tls_flow_control_client.py
new file mode 100644
index 0000000000..66565c1912
--- /dev/null
+++ b/tests/gold_tests/tls/tls_flow_control_client.py
@@ -0,0 +1,100 @@
+# 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.
+"""Download a large object from ATS over TLS while reading the response body
+slowly, to exercise HTTP tunnel flow control (proxy.config.http.flow_control)
+on a TLS client connection.
+
+A slow reader keeps ATS's client-side write buffer full, so the tunnel
+repeatedly throttles the origin read and must unthrottle as the buffered data
+drains to the client. The whole body must still arrive; a flow-control stall
+(no unthrottle) shows up as a short or timed-out read. This path had no prior
+gold coverage, and the layered TLS VConnection drives the unthrottle through its
+demand-driven write rather than the inherited write-buffer-empty trap, so it is
+worth guarding directly.
+"""
+
+import argparse
+import socket
+import ssl
+import sys
+import time
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-p", "--port", type=int, required=True, help="ATS TLS port")
+ parser.add_argument("--host", default="ex.test", help="Host header / SNI")
+ parser.add_argument("--path", default="/obj", help="request path")
+ parser.add_argument("--expect-bytes", type=int, required=True, help="expected body length")
+ parser.add_argument("--read-size", type=int, default=16 * 1024, help="bytes per recv")
+ parser.add_argument("--read-delay", type=float, default=0.002, help="sleep between recvs (s)")
+ parser.add_argument("--recv-timeout", type=float, default=15.0, help="per-recv socket timeout (s)")
+ parser.add_argument("--deadline", type=float, default=120.0, help="overall wall-clock budget (s)")
+ args = parser.parse_args()
+
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+
+ deadline = time.monotonic() + args.deadline
+ body_received = 0
+ error = ""
+ try:
+ with socket.create_connection(("127.0.0.1", args.port), timeout=args.recv_timeout) as sock:
+ with ctx.wrap_socket(sock, server_hostname=args.host) as ssock:
+ ssock.settimeout(args.recv_timeout)
+ request = (f"GET {args.path} HTTP/1.1\r\nHost: {args.host}\r\n"
+ "Connection: close\r\n\r\n").encode()
+ ssock.sendall(request)
+
+ # Read until we have the full header block, keeping any body bytes
+ # that arrive in the same recv.
+ buf = b""
+ while b"\r\n\r\n" not in buf:
+ if time.monotonic() > deadline:
+ raise TimeoutError("deadline reached reading response headers")
+ chunk = ssock.recv(args.read_size)
+ if not chunk:
+ raise ConnectionError("connection closed before headers complete")
+ buf += chunk
+ header_blob, _, leftover = buf.partition(b"\r\n\r\n")
+ body_received = len(leftover)
+
+ # Drain the body slowly so ATS's client-side write buffer stays full
+ # and the tunnel relies on the buffer-empty unthrottle.
+ while body_received < args.expect_bytes:
+ if time.monotonic() > deadline:
+ raise TimeoutError(f"deadline reached after {body_received} body bytes")
+ time.sleep(args.read_delay)
+ chunk = ssock.recv(args.read_size)
+ if not chunk:
+ break
+ body_received += len(chunk)
+ except Exception as exc: # noqa: BLE001 - any failure is a test signal
+ error = f"{type(exc).__name__}: {exc}"
+
+ passed = error == "" and body_received == args.expect_bytes
+
+ print(f"BODY_BYTES={body_received}")
+ print(f"EXPECT_BYTES={args.expect_bytes}")
+ if error:
+ print(f"ERROR={error}")
+ print(f"RESULT={'PASS' if passed else 'FAIL'}")
+ return 0 if passed else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_origin_open_failed.test.py b/tests/gold_tests/tls/tls_origin_open_failed.test.py
new file mode 100644
index 0000000000..52092aadf0
--- /dev/null
+++ b/tests/gold_tests/tls/tls_origin_open_failed.test.py
@@ -0,0 +1,78 @@
+'''
+Verify that a failed outbound TLS origin connection is surfaced to the client as
+an error (5xx) without crashing ATS.
+'''
+# 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.
+
+import ports
+
+Test.Summary = __doc__
+
+
+class TestOriginOpenFailed:
+ '''Verify a failed outbound TLS connect is surfaced as a 5xx, not a crash.'''
+
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._ts = self._configure_trafficserver()
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server with an https origin whose connect fails.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestOriginOpenFailed._ts_counter}')
+ TestOriginOpenFailed._ts_counter += 1
+
+ # Reserve a port for a TLS origin; its liveness is irrelevant since the
+ # source bind below fails before any connect is attempted.
+ ports.get_port(ts, 'origin_port')
+
+ ts.Disk.remap_config.AddLine(f'map http://dead.test/ https://127.0.0.1:{ts.Variables.origin_port}/')
+ ts.Disk.records_config.update(
+ {
+ # Bind every outbound connection to a non-local source address
+ # (RFC 5737 documentation range) so bind() fails synchronously with
+ # EADDRNOTAVAIL, i.e. the connect fails before any TCP/TLS exchange
+ # rather than via a later, routable connect that would be refused
+ # asynchronously.
+ 'proxy.config.outgoing_ip_to_bind': '192.0.2.1',
+ 'proxy.config.http.connect_attempts_max_retries': 1,
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'http|ssl',
+ })
+
+ # Tearing down the failed outbound TLS connect must not crash ATS.
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "received signal|failed assertion", "ATS must not crash on a failed outbound TLS connect")
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRun.'''
+ tr = Test.AddTestRun("a failed outbound TLS connect is surfaced cleanly")
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.MakeCurlCommand(
+ f'-s -o /dev/null -w "%{{http_code}}" -H "Host: dead.test" http://127.0.0.1:{self._ts.Variables.port}/', ts=self._ts)
+ tr.Processes.Default.ReturnCode = 0
+ # A failed origin connection is surfaced as a 5xx (502 Bad Gateway).
+ tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("50[02]", "a failed origin connect yields a 5xx")
+ tr.StillRunningAfter = self._ts
+
+
+TestOriginOpenFailed().run()
diff --git a/tests/gold_tests/tls/tls_origin_post_abort.test.py b/tests/gold_tests/tls/tls_origin_post_abort.test.py
new file mode 100644
index 0000000000..4d989a26ef
--- /dev/null
+++ b/tests/gold_tests/tls/tls_origin_post_abort.test.py
@@ -0,0 +1,114 @@
+'''
+When a TLS origin resets the connection while ATS is still sending it a POST
+request body, ATS must fail the transaction promptly -- the transport error must
+reach the request-body tunnel right away as VC_EVENT_ERROR, not sit until the
+outbound inactivity timeout rescues it.
+'''
+# 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.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+
+class TestOriginPostAbort:
+ '''Verify a TLS origin RST mid-POST-body fails the transaction promptly.'''
+
+ _ts_counter: int = 0
+ _origin_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ Test.GetTcpPort("origin_port")
+ self._ts = self._configure_trafficserver()
+ self._origin = self._configure_origin()
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server with a TLS origin and a generous rescue timeout.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestOriginPostAbort._ts_counter}', enable_cache=False)
+ TestOriginPostAbort._ts_counter += 1
+
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.url_remap.remap_required': 1,
+ 'proxy.config.http.connect_attempts_max_retries': 0,
+ 'proxy.config.http.connect_attempts_timeout': 15,
+ # The rescue timeout: a prompt failure must beat this by a wide margin.
+ 'proxy.config.http.transaction_no_activity_timeout_out': 15,
+ 'proxy.config.http.transaction_no_activity_timeout_in': 30,
+ # Keep the kernel send buffer small so the request body cannot be
+ # absorbed by the kernel: ATS must still be mid-send at the RST.
+ 'proxy.config.net.sock_send_buffer_size_out': 65536,
+ 'proxy.config.ssl.client.verify.server.policy': 'DISABLED',
+ 'proxy.config.diags.debug.enabled': 0,
+ 'proxy.config.diags.debug.tags': 'http|ssl|ssl_io',
+ })
+ ts.Disk.remap_config.AddLine(f'map /post https://127.0.0.1:{Test.Variables.origin_port}')
+
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ 'received signal|failed assertion', 'ATS must not crash handling the mid-body origin abort')
+ return ts
+
+ def _configure_origin(self) -> 'Process':
+ '''Configure a raw-socket TLS origin that resets mid-request-body.
+
+ The scenario needs a TLS origin that accepts the request body and then RSTs
+ the connection mid-body (SO_LINGER 0). Proxy Verifier can only close cleanly
+ (close_notify), so it cannot express a mid-body reset; hence the small
+ raw-socket origin helper rather than an ATSReplayTest.
+
+ :return: The origin server Process.
+ '''
+ origin = Test.Processes.Process(
+ f'origin-{TestOriginPostAbort._origin_counter}',
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_post_abort_origin.py")} '
+ f'-p {Test.Variables.origin_port} '
+ f'-c {os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem")} -d 1.0')
+ TestOriginPostAbort._origin_counter += 1
+
+ # These markers prove the reset path was actually exercised: the origin
+ # completed the TLS handshake, received the forwarded request headers, and
+ # then reset the connection mid-body. Without them the client could pass on
+ # any prompt response -- e.g. a broken remap or a never-started origin that
+ # yields a fast 5xx without the body ever reaching a resetting origin.
+ origin.Streams.stdout = Testers.ContainsExpression(
+ 'request headers received', 'the origin must receive the forwarded request headers')
+ origin.Streams.stdout += Testers.ContainsExpression(
+ 'connection reset sent', 'the origin must reset the connection mid-body')
+ return origin
+
+ def run(self) -> None:
+ '''Configure and run the TestRun.'''
+ tr = Test.AddTestRun("POST to a TLS origin that resets mid-body must fail promptly")
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.StartBefore(self._origin, ready=When.PortOpen(Test.Variables.origin_port))
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_post_abort_client.py")} '
+ f'-p {self._ts.Variables.port} -t 8')
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+ 'PASS: transaction failed promptly', 'the POST must fail promptly on the origin RST')
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+ 'status-code: 5', 'ATS must surface the mid-body origin reset as a 5xx')
+ tr.StillRunningAfter = self._ts
+
+
+TestOriginPostAbort().run()
diff --git a/tests/gold_tests/tls/tls_post_abort_client.py b/tests/gold_tests/tls/tls_post_abort_client.py
new file mode 100644
index 0000000000..afd02385a1
--- /dev/null
+++ b/tests/gold_tests/tls/tls_post_abort_client.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+'''
+POST a large request body through ATS and measure how long ATS takes to fail
+the transaction after the origin aborts the connection mid-body.
+'''
+# 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.
+
+import argparse
+import select
+import socket
+import sys
+import time
+
+
+def parse_status_code(status_line: str) -> 'int | None':
+ '''Return the numeric HTTP status from a status line, or None if absent.'''
+ fields = status_line.split()
+ if len(fields) >= 2 and fields[0].startswith('HTTP/') and fields[1].isdigit():
+ return int(fields[1])
+ return None
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='POST through ATS; time how fast the transaction fails.')
+ parser.add_argument('-p', '--proxy-port', type=int, required=True, help='ATS HTTP port')
+ parser.add_argument('-s', '--size', type=int, default=64 * 1024 * 1024, help='request body size in bytes')
+ parser.add_argument(
+ '-t', '--threshold', type=float, default=8.0, help='maximum seconds from request start until ATS fails the transaction')
+ parser.add_argument('--timeout', type=float, default=60.0, help='give up entirely after this many seconds')
+ parser.add_argument(
+ '--pause-after', type=int, default=0, help='stop sending body bytes after this many (0: keep sending until done)')
+ args = parser.parse_args()
+
+ start = time.monotonic()
+ sock = socket.create_connection(('127.0.0.1', args.proxy_port), timeout=10)
+ request = (f'POST /post HTTP/1.1\r\n'
+ f'Host: origin\r\n'
+ f'Content-Length: {args.size}\r\n'
+ f'Connection: close\r\n'
+ f'\r\n').encode()
+ sock.sendall(request)
+ sock.setblocking(False)
+
+ chunk = b'x' * 65536
+ sent = 0
+ response = b''
+ outcome = None
+ while time.monotonic() - start < args.timeout:
+ want_write = sent < args.size and not (args.pause_after and sent >= args.pause_after)
+ readable, writable, _ = select.select([sock], [sock] if want_write else [], [], 1.0)
+ if readable:
+ try:
+ data = sock.recv(65536)
+ except (ConnectionResetError, BrokenPipeError, OSError) as e:
+ outcome = f'connection error while reading: {e.__class__.__name__}'
+ break
+ if not data:
+ outcome = 'connection closed by ATS'
+ break
+ response += data
+ if b'\r\n\r\n' in response:
+ outcome = 'response received'
+ break
+ if writable:
+ try:
+ sent += sock.send(chunk[:min(len(chunk), args.size - sent)])
+ except (ConnectionResetError, BrokenPipeError, OSError) as e:
+ outcome = f'connection error while sending: {e.__class__.__name__}'
+ break
+ if sent >= args.size:
+ outcome = 'entire body sent'
+ break
+
+ elapsed = time.monotonic() - start
+ if outcome is None:
+ outcome = 'gave up waiting'
+ status_line = response.split(b'\r\n', 1)[0].decode(errors='replace') if response else '<none>'
+ status_code = parse_status_code(status_line)
+ print(f'outcome: {outcome}')
+ print(f'status-line: {status_line}')
+ print(f'status-code: {status_code if status_code is not None else "<none>"}')
+ print(f'body-bytes-sent: {sent}')
+ print(f'elapsed: {elapsed:.3f}')
+
+ # The transaction must actually have failed, not completed.
+ if outcome in ('entire body sent', 'gave up waiting'):
+ print('FAIL: transaction did not fail at all')
+ return 2
+ # The client must have started streaming the request body; zero bytes sent
+ # would mean ATS failed at the request headers, not the mid-body path. (That
+ # the body reached a resetting origin is proven by the origin-side markers the
+ # test asserts, not by this client-side count.)
+ if sent <= 0:
+ print('FAIL: ATS failed before any request body was sent; mid-body path not exercised')
+ return 1
+ # A prompt failure must beat the inactivity-timeout rescue by a wide margin.
+ if elapsed > args.threshold:
+ print(f'FAIL: transaction failed only after {elapsed:.3f}s (threshold {args.threshold}s)')
+ return 1
+ # The upstream reset must be surfaced as a 5xx, not masked as success and not
+ # a bare connection drop with no status at all.
+ if status_code is None:
+ print('FAIL: ATS dropped the connection without surfacing a response status')
+ return 1
+ if not 500 <= status_code <= 599:
+ print(f'FAIL: ATS surfaced status {status_code}; expected a 5xx for the origin reset')
+ return 1
+ print('PASS: transaction failed promptly')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_post_abort_origin.py b/tests/gold_tests/tls/tls_post_abort_origin.py
new file mode 100644
index 0000000000..8300358a31
--- /dev/null
+++ b/tests/gold_tests/tls/tls_post_abort_origin.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+'''
+A TLS origin that accepts one connection, reads the request headers, then
+stops reading and aborts the connection with a TCP RST while the proxy is
+still sending the request body.
+'''
+# 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.
+
+import argparse
+import socket
+import ssl
+import struct
+import sys
+import time
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='TLS origin that RSTs mid-request-body.')
+ parser.add_argument('-p', '--port', type=int, required=True, help='port to listen on')
+ parser.add_argument('-c', '--cert', required=True, help='PEM file with certificate and key')
+ parser.add_argument('-d', '--delay', type=float, default=1.0, help='seconds to wait after the headers before the RST')
+ args = parser.parse_args()
+
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ context.load_cert_chain(args.cert)
+
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ # Keep the receive window small so the proxy cannot park the whole request
+ # body in kernel buffers: it must still be mid-send when the RST arrives.
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
+ listener.bind(('127.0.0.1', args.port))
+ listener.listen(5)
+ print(f'listening on {args.port}', flush=True)
+
+ # Accept until a connection completes the TLS handshake and delivers the
+ # request headers: the test harness probes the port (a bare TCP connect
+ # then close) to detect readiness, and that probe must not consume the
+ # one real connection.
+ tls = None
+ while tls is None:
+ conn, addr = listener.accept()
+ print(f'accepted connection from {addr}', flush=True)
+ try:
+ tls = context.wrap_socket(conn, server_side=True)
+ except (ssl.SSLError, OSError) as e:
+ print(f'TLS handshake failed (readiness probe?): {e}', flush=True)
+ conn.close()
+ tls = None
+ continue
+ print('TLS handshake complete', flush=True)
+ data = b''
+ while b'\r\n\r\n' not in data:
+ chunk = tls.recv(4096)
+ if not chunk:
+ break
+ data += chunk
+ if b'\r\n\r\n' not in data:
+ print('peer closed before request headers arrived', flush=True)
+ tls.close()
+ tls = None
+ continue
+ print('request headers received; not reading the body', flush=True)
+
+ # Let the proxy get deep into sending the request body, then abort. With
+ # SO_LINGER(0) the close discards buffered data and sends an RST; the
+ # unread body bytes in the receive queue force an RST as well. Do not
+ # unwrap()/shutdown the TLS layer: this is an abortive transport-level
+ # close, not a clean close_notify.
+ time.sleep(args.delay)
+ tls.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
+ tls.close()
+ print('connection reset sent', flush=True)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_record_size.test.py b/tests/gold_tests/tls/tls_record_size.test.py
new file mode 100644
index 0000000000..b40c33a6fa
--- /dev/null
+++ b/tests/gold_tests/tls/tls_record_size.test.py
@@ -0,0 +1,117 @@
+'''
+Exercise the TLS record-size clamp (proxy.config.ssl.max_record_size > 0): on a
+large TLS download the body must arrive intact and every application-data record
+on the wire must be clamped to the configured size.
+'''
+# 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.
+
+# NOTE: only the positive (fixed-clamp) branch of the record-sizing logic is
+# covered here. The documented dynamic mode (max_record_size == -1) cannot be
+# enabled through records.yaml because the record's validity check is [0-16383],
+# which rejects -1; that inconsistency is pre-existing, so the dynamic branch
+# stays uncovered by design.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+
+class TestRecordSizeClamp:
+ '''Verify max_record_size clamps every record of a large TLS download.'''
+
+ # Comfortably larger than the clamp so many records pass through it.
+ _body_len: int = 1024 * 1024
+ _max_record: int = 4096
+
+ _server_counter: int = 0
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._server = self._configure_server()
+ self._ts = self._configure_trafficserver()
+
+ def _configure_server(self) -> 'Process':
+ '''Configure the origin server with a large response body.
+
+ :return: The origin server Process.
+ '''
+ server = Test.MakeOriginServer(f'server-{TestRecordSizeClamp._server_counter}')
+ TestRecordSizeClamp._server_counter += 1
+
+ body = "x" * TestRecordSizeClamp._body_len
+ request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n"
+ f"Cache-Control: max-age=3600\r\nContent-Length: {TestRecordSizeClamp._body_len}\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": body
+ }
+ server.addResponse("sessionlog.json", request_header, response_header)
+ return server
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server with a positive max_record_size clamp.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestRecordSizeClamp._ts_counter}', enable_tls=True)
+ TestRecordSizeClamp._ts_counter += 1
+
+ ts.addDefaultSSLFiles()
+ ts.Disk.ssl_multicert_yaml.AddLines(
+ """
+ssl_multicert:
+ - dest_ip: "*"
+ ssl_cert_name: server.pem
+ ssl_key_name: server.key
+""".split("\n"))
+ ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}')
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ # Positive cap -> the write path clamps each TLS record to this many bytes.
+ 'proxy.config.ssl.max_record_size': TestRecordSizeClamp._max_record,
+ })
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRun.
+
+ The client downloads the object and measures the TLS records on the wire,
+ asserting both that the body is intact and that no application-data record
+ exceeds the configured clamp.
+ '''
+ tr = Test.AddTestRun("max_record_size>0 clamps records on a large TLS download")
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_record_size_client.py")} '
+ f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj '
+ f'--max-record {TestRecordSizeClamp._max_record} --expect-bytes {TestRecordSizeClamp._body_len}')
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+ "PASS: every application-data record is within the configured clamp",
+ "every TLS record must be clamped to the configured size")
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+
+TestRecordSizeClamp().run()
diff --git a/tests/gold_tests/tls/tls_record_size_client.py b/tests/gold_tests/tls/tls_record_size_client.py
new file mode 100644
index 0000000000..a169944e5c
--- /dev/null
+++ b/tests/gold_tests/tls/tls_record_size_client.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+'''
+Download an object from ATS over TLS and inspect the TLS records on the wire:
+confirm the body arrives intact AND that every application-data record is no
+larger than the configured proxy.config.ssl.max_record_size (plus AEAD overhead).
+
+A MemoryBIO drives the handshake so the raw ciphertext stream is visible; the
+5-byte TLS record headers (type, version, length) are in cleartext, so record
+sizes can be measured without decrypting. TLS 1.2 is pinned so that handshake
+messages are their own record type (22) and only real application data is type 23,
+and the cipher is restricted to AEAD (GCM) suites so the per-record overhead is a
+small fixed constant rather than the variable IV/MAC/padding of a CBC suite.
+'''
+# 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.
+
+import argparse
+import socket
+import ssl
+import sys
+from collections.abc import Iterator
+
+TLS_APPLICATION_DATA = 23
+# A clamped plaintext record becomes ciphertext of plaintext + AEAD overhead
+# (TLS1.2 GCM: 8-byte explicit nonce + 16-byte tag = 24 bytes; the cipher is pinned
+# to AEAD below). 256 is a generous ceiling over that, far below an unclamped ~16 KB
+# record, so the clamp check stays decisive and cannot be tripped by the larger,
+# variable expansion of a CBC suite.
+RECORD_OVERHEAD = 256
+
+
+def iter_record_lengths(buf: bytes | bytearray) -> Iterator[tuple[int, int]]:
+ '''Yield (content_type, record_length) for each complete TLS record in buf.'''
+ i, n = 0, len(buf)
+ while i + 5 <= n:
+ content_type = buf[i]
+ length = (buf[i + 3] << 8) | buf[i + 4]
+ if i + 5 + length > n:
+ break # truncated trailing record
+ yield content_type, length
+ i += 5 + length
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='Measure ATS TLS record sizes on a download.')
+ parser.add_argument('-p', '--port', type=int, required=True, help='ATS TLS port')
+ parser.add_argument('--host', default='ex.test', help='Host header / SNI')
+ parser.add_argument('--path', default='/obj', help='request path')
+ parser.add_argument('--max-record', type=int, required=True, help='configured proxy.config.ssl.max_record_size')
+ parser.add_argument('--expect-bytes', type=int, required=True, help='expected response body length')
+ args = parser.parse_args()
+
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2
+ ctx.maximum_version = ssl.TLSVersion.TLSv1_2
+ # Pin AEAD (GCM) suites: their per-record overhead is a small fixed 24 bytes, so a
+ # clamped record stays well within max_record + RECORD_OVERHEAD. A negotiated CBC
+ # suite could expand a record by IV + MAC + up to 255 bytes of padding and trip the
+ # clamp check spuriously. SECLEVEL=0 keeps the small test cert usable.
+ ctx.set_ciphers(
+ 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:'
+ 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:'
+ 'AES128-GCM-SHA256:AES256-GCM-SHA384@SECLEVEL=0')
+
+ incoming, outgoing = ssl.MemoryBIO(), ssl.MemoryBIO()
+ tls = ctx.wrap_bio(incoming, outgoing, server_hostname=args.host)
+
+ sock = socket.create_connection(('127.0.0.1', args.port), timeout=30)
+ sock.settimeout(30)
+ raw = bytearray() # every ciphertext byte the server sends, in order
+
+ def flush() -> None:
+ data = outgoing.read()
+ if data:
+ sock.sendall(data)
+
+ def feed() -> bytes:
+ chunk = sock.recv(65536)
+ if chunk:
+ raw.extend(chunk)
+ incoming.write(chunk)
+ return chunk
+
+ while True:
+ try:
+ tls.do_handshake()
+ break
+ except ssl.SSLWantReadError:
+ flush()
+ if not feed():
+ print('FAIL: server closed during handshake')
+ return 2
+ flush()
+
+ request = (f'GET {args.path} HTTP/1.1\r\n'
+ f'Host: {args.host}\r\n'
+ f'Connection: close\r\n\r\n').encode()
+ tls.write(request)
+ flush()
+
+ response = bytearray()
+ while True:
+ try:
+ data = tls.read(65536)
+ if not data:
+ break # clean close_notify
+ response.extend(data)
+ except ssl.SSLWantReadError:
+ flush()
+ if not feed():
+ break # socket closed by ATS
+ except ssl.SSLEOFError:
+ break
+
+ separator = response.find(b'\r\n\r\n')
+ body_len = len(response) - (separator + 4) if separator >= 0 else -1
+
+ app_lengths = [length for content_type, length in iter_record_lengths(raw) if content_type == TLS_APPLICATION_DATA]
+ max_record = max(app_lengths) if app_lengths else 0
+ limit = args.max_record + RECORD_OVERHEAD
+
+ print(
+ f'app_data_records={len(app_lengths)} max_record_len={max_record} '
+ f'limit={limit} body_len={body_len} expect={args.expect_bytes}')
+
+ if body_len != args.expect_bytes:
+ print(f'FAIL: body length {body_len} != expected {args.expect_bytes}')
+ return 1
+ if len(app_lengths) < 2:
+ print('FAIL: too few application-data records to judge clamping')
+ return 1
+ if max_record > limit:
+ print(f'FAIL: an application-data record ({max_record}) exceeds the clamp + overhead ({limit})')
+ return 1
+ print('PASS: every application-data record is within the configured clamp')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_reload_under_load.test.py b/tests/gold_tests/tls/tls_reload_under_load.test.py
new file mode 100644
index 0000000000..1828da760d
--- /dev/null
+++ b/tests/gold_tests/tls/tls_reload_under_load.test.py
@@ -0,0 +1,100 @@
+'''
+Existing cert/SNI reload tests reload while the server is idle. This one drives
+continuous concurrent TLS handshakes and reloads ssl_multicert.yaml on top of
+them, stressing the SSL/BIO ownership boundary of the layered TLS VConnection.
+The swapped-in certificate must take effect, every handshake must succeed, and
+ATS must not crash.
+'''
+# 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.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+
+class TestTlsReloadUnderLoad:
+ '''Reload TLS certs while concurrent handshakes are in flight; serving must not break.'''
+
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._ts = self._configure_trafficserver()
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server with a swappable live certificate.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestTlsReloadUnderLoad._ts_counter}', enable_tls=True, enable_cache=False)
+ TestTlsReloadUnderLoad._ts_counter += 1
+
+ # signed-bar is the live cert; signed2-bar shares its key and is copied over
+ # the live cert at reload time. Distinct fingerprints let the client prove the
+ # swap took effect under load.
+ ts.addSSLfile("ssl/signed-bar.pem")
+ ts.addSSLfile("ssl/signed-bar.key")
+ ts.addSSLfile("ssl/signed2-bar.pem")
+
+ ts.Disk.ssl_multicert_yaml.AddLines(
+ """
+ssl_multicert:
+ - dest_ip: "*"
+ ssl_cert_name: signed-bar.pem
+ ssl_key_name: signed-bar.key
+""".split("\n"))
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.exec_thread.autoconfig.scale': 1.0,
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'ssl',
+ })
+
+ # The reload must actually have run (otherwise the test would be vacuous).
+ ts.Disk.diags_log.Content = Testers.ContainsExpression(
+ "ssl_multicert.yaml finished loading", "the cert configuration must reload while load is in flight")
+ # The reload-under-load must not crash or trip an assertion / sanitizer.
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "received signal|failed assertion", "ATS must not crash reloading certs under load")
+ ts.Disk.traffic_out.Content += Testers.ExcludesExpression(
+ "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error reloading certs under load")
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRun.'''
+ tr = Test.AddTestRun("reload certs while TLS handshakes are in flight")
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_reload_under_load_client.py")} '
+ f'-p {self._ts.Variables.ssl_port} --sni bar.com --ssldir {self._ts.Variables.SSLDir} '
+ f'--live-cert signed-bar.pem '
+ f'--v2-cert {os.path.join(self._ts.Variables.SSLDir, "signed2-bar.pem")} '
+ f'--reloads 3 --duration 8 --concurrency 4')
+ # traffic_ctl (invoked by the client to reload) needs the runroot environment.
+ tr.Processes.Default.Env = self._ts.Env
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.All = Testers.ContainsExpression("RESULT=PASS", "load + reload run must pass")
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression("FAILURES=0", "no handshake may fail across the reload")
+ tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+ "CERT_CHANGED=1", "the swapped-in certificate must be served after the reload")
+ tr.StillRunningAfter = self._ts
+
+
+TestTlsReloadUnderLoad().run()
diff --git a/tests/gold_tests/tls/tls_reload_under_load_client.py b/tests/gold_tests/tls/tls_reload_under_load_client.py
new file mode 100644
index 0000000000..c4ed0577a3
--- /dev/null
+++ b/tests/gold_tests/tls/tls_reload_under_load_client.py
@@ -0,0 +1,183 @@
+# 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.
+"""Drive continuous concurrent TLS handshakes against ATS while a certificate
+reload (traffic_ctl config reload) happens mid-flight.
+
+This stresses the SSL/BIO ownership boundary of the layered TLS VConnection: the
+SSL context list is rebuilt while inbound handshakes and request/response writes
+are in flight. A correct server keeps serving (no failed handshakes, no crash)
+and the swapped-in certificate takes effect.
+
+The script is self-contained: worker threads hammer handshakes for the whole run
+while the main thread swaps the cert file and reloads ATS at a known offset, so
+the reload provably overlaps in-flight handshakes.
+"""
+
+import argparse
+import collections
+import hashlib
+import os
+import shutil
+import socket
+import ssl
+import subprocess
+import sys
+import threading
+import time
+
+
+def make_context() -> ssl.SSLContext:
+ # We validate handshake mechanics and cert identity (by fingerprint), not the
+ # trust chain, so disable verification to keep the client independent of which
+ # signer is currently installed.
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ return ctx
+
+
+class Stats:
+
+ def __init__(self) -> None:
+ self.lock = threading.Lock()
+ self.ok = 0
+ self.fail = 0
+ self.errors: collections.Counter = collections.Counter()
+ self.pre_fps: collections.Counter = collections.Counter()
+ self.post_fps: collections.Counter = collections.Counter()
+
+
+def worker(
+ args: argparse.Namespace, stats: Stats, stop: threading.Event, reload_started: threading.Event,
+ reload_settled: threading.Event) -> None:
+ ctx = make_context()
+ while not stop.is_set():
+ try:
+ with socket.create_connection((args.host, args.port), timeout=args.timeout) as sock:
+ with ctx.wrap_socket(sock, server_hostname=args.sni) as ssock:
+ der = ssock.getpeercert(binary_form=True)
+ fp = hashlib.sha256(der).hexdigest()[:16] if der else "none"
+ request = (f"GET / HTTP/1.1\r\nHost: {args.sni}\r\n"
+ "Connection: close\r\n\r\n").encode()
+ ssock.sendall(request)
+ resp = ssock.recv(256)
+ if not resp.startswith(b"HTTP/1."):
+ with stats.lock:
+ stats.fail += 1
+ stats.errors["bad_response"] += 1
+ continue
+ with stats.lock:
+ stats.ok += 1
+ # Bucket the served cert by phase. Ignore the transition window so
+ # pre==v1 and post==v2 cleanly, regardless of reload latency.
+ if not reload_started.is_set():
+ stats.pre_fps[fp] += 1
+ elif reload_settled.is_set():
+ stats.post_fps[fp] += 1
+ except Exception as exc: # noqa: BLE001 - any failure is a test signal
+ with stats.lock:
+ stats.fail += 1
+ stats.errors[type(exc).__name__] += 1
+
+
+def do_reload(env_traffic_ctl: str) -> None:
+ subprocess.run([env_traffic_ctl, "config", "reload"], check=True, timeout=30)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--host", default="127.0.0.1")
+ parser.add_argument("-p", "--port", type=int, required=True)
+ parser.add_argument("--sni", default="bar.com")
+ parser.add_argument("--ssldir", required=True, help="dir holding the live cert that gets swapped")
+ parser.add_argument("--live-cert", default="signed-bar.pem", help="cert filename inside --ssldir")
+ parser.add_argument("--v2-cert", required=True, help="source cert to copy over the live cert at reload")
+ parser.add_argument("--traffic-ctl", default="traffic_ctl")
+ parser.add_argument("--duration", type=float, default=8.0)
+ parser.add_argument("--concurrency", type=int, default=4)
+ parser.add_argument("--timeout", type=float, default=5.0)
+ parser.add_argument("--reload-at", type=float, default=2.0)
+ parser.add_argument("--reloads", type=int, default=3)
+ parser.add_argument("--reload-spacing", type=float, default=0.7)
+ parser.add_argument("--settle", type=float, default=0.8, help="grace after last reload before sampling post cert")
+ parser.add_argument("--min-ok", type=int, default=20)
+ args = parser.parse_args()
+
+ stats = Stats()
+ stop = threading.Event()
+ reload_started = threading.Event()
+ reload_settled = threading.Event()
+
+ workers = [
+ threading.Thread(target=worker, args=(args, stats, stop, reload_started, reload_settled), daemon=True)
+ for _ in range(args.concurrency)
+ ]
+ start = time.monotonic()
+ for t in workers:
+ t.start()
+
+ # Let load ramp up so the reload lands on top of in-flight handshakes.
+ time.sleep(args.reload_at)
+
+ live_path = os.path.join(args.ssldir, args.live_cert)
+ reloads_done = 0
+ reload_error = ""
+ try:
+ # Swap to v2 once; the first reload installs it. Further reloads keep
+ # rebuilding the SSL store under sustained load.
+ shutil.copyfile(args.v2_cert, live_path)
+ os.utime(live_path, None)
+ reload_started.set()
+ for i in range(args.reloads):
+ do_reload(args.traffic_ctl)
+ reloads_done += 1
+ if i != args.reloads - 1:
+ time.sleep(args.reload_spacing)
+ except Exception as exc: # noqa: BLE001
+ reload_error = f"{type(exc).__name__}: {exc}"
+
+ time.sleep(args.settle)
+ reload_settled.set()
+
+ elapsed = time.monotonic() - start
+ if args.duration > elapsed:
+ time.sleep(args.duration - elapsed)
+ stop.set()
+ for t in workers:
+ t.join(timeout=args.timeout + 2)
+
+ pre_fp = stats.pre_fps.most_common(1)[0][0] if stats.pre_fps else "none"
+ post_fp = stats.post_fps.most_common(1)[0][0] if stats.post_fps else "none"
+ cert_changed = int(pre_fp != "none" and post_fp != "none" and pre_fp != post_fp)
+
+ passed = (
+ reload_error == "" and reloads_done == args.reloads and stats.fail == 0 and stats.ok >= args.min_ok and cert_changed == 1)
+
+ print(f"HANDSHAKES_OK={stats.ok}")
+ print(f"FAILURES={stats.fail}")
+ print(f"RELOADS_DONE={reloads_done}")
+ print(f"CERT_CHANGED={cert_changed}")
+ print(f"PRE_FP={pre_fp} POST_FP={post_fp}")
+ if stats.errors:
+ print(f"ERROR_BREAKDOWN={dict(stats.errors)}")
+ if reload_error:
+ print(f"RELOAD_ERROR={reload_error}")
+ print(f"RESULT={'PASS' if passed else 'FAIL'}")
+ return 0 if passed else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/gold_tests/tls/tls_renegotiation.test.py b/tests/gold_tests/tls/tls_renegotiation.test.py
new file mode 100644
index 0000000000..70fecc14df
--- /dev/null
+++ b/tests/gold_tests/tls/tls_renegotiation.test.py
@@ -0,0 +1,124 @@
+'''
+With client renegotiation disallowed (the default), a TLSv1.2 client that asks to
+renegotiate must have its connection refused without taking down the server.
+'''
+# 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.
+
+import os
+import sys
+
+Test.Summary = __doc__
+
+# Renegotiation only exists in TLS 1.2 and earlier.
+Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport())
+
+
+class TestRenegotiationRefused:
+ '''Verify a refused client-initiated renegotiation does not crash ATS.'''
+
+ _server_counter: int = 0
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._server = self._configure_server()
+ self._ts = self._configure_trafficserver()
+
+ def _configure_server(self) -> 'Process':
+ '''Configure the origin server.
+
+ :return: The origin server Process.
+ '''
+ server = Test.MakeOriginServer(f'server-{TestRenegotiationRefused._server_counter}')
+ TestRenegotiationRefused._server_counter += 1
+
+ request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+ response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"}
+ server.addResponse("sessionlog.json", request_header, response_header)
+ return server
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server to refuse client renegotiation (the default).
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestRenegotiationRefused._ts_counter}', enable_tls=True)
+ TestRenegotiationRefused._ts_counter += 1
+
+ ts.addSSLfile("ssl/server.pem")
+ ts.addSSLfile("ssl/server.key")
+ ts.Disk.ssl_multicert_yaml.AddLines(
+ """
+ssl_multicert:
+ - dest_ip: "*"
+ ssl_cert_name: server.pem
+ ssl_key_name: server.key
+""".split("\n"))
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ # The secure default: never honor a client-initiated renegotiation.
+ 'proxy.config.ssl.allow_client_renegotiation': 0,
+ # Renegotiation requires a sub-TLS1.3 protocol.
+ 'proxy.config.ssl.TLSv1_3.enabled': 0,
+ 'proxy.config.ssl.TLSv1_2': 1,
+ # So the renegotiation-detection log line below is emitted.
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'ssl_load',
+ })
+ ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}')
+
+ # The refused renegotiation must not abort the process.
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "received signal|failed assertion", "ATS must refuse the renegotiation without crashing")
+ # ...and it must actually reach the renegotiation-detection path (otherwise
+ # a no-crash pass could mean the client never managed to renegotiate at all).
+ ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "trying to renegotiate from the client", "ATS must detect the client-initiated renegotiation")
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRuns.'''
+ # Complete a TLSv1.2 handshake, then ask to renegotiate. ATS must detect
+ # and refuse the renegotiation without aborting the process.
+ tr = Test.AddTestRun("client renegotiation is refused without crashing ATS")
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_renegotiation_client.py")} '
+ f'-p {self._ts.Variables.ssl_port} -s example.com')
+ tr.Processes.Default.ReturnCode = 0
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+ # ATS survived, so a normal request still succeeds.
+ tr = Test.AddTestRun("server still serves requests after the refused renegotiation")
+ tr.MakeCurlCommand(
+ (
+ "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k "
+ f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' "
+ f"https://example.com:{self._ts.Variables.ssl_port}/"),
+ ts=self._ts)
+ tr.Processes.Default.ReturnCode = 0
+ # curl -v writes the response status line to stderr.
+ tr.Processes.Default.Streams.stderr = Testers.ContainsExpression(
+ "HTTP/1.1 200 OK", "request after renegotiation should succeed")
+ tr.StillRunningAfter = self._ts
+
+
+TestRenegotiationRefused().run()
diff --git a/tests/gold_tests/tls/tls_renegotiation_allowed.test.py b/tests/gold_tests/tls/tls_renegotiation_allowed.test.py
new file mode 100644
index 0000000000..a5f58ff358
--- /dev/null
+++ b/tests/gold_tests/tls/tls_renegotiation_allowed.test.py
@@ -0,0 +1,145 @@
+'''
+With client renegotiation allowed, a TLSv1.2 client that asks to renegotiate must
+get a prompt answer from ATS (the renegotiation either completes or is refused
+with an alert) rather than hanging. ATS must flush the SSL protocol output the
+renegotiation generates; otherwise the answer is never sent and the client
+stalls.
+'''
+# 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.
+
+import os
+import sys
+
+from ports import get_port
+
+Test.Summary = __doc__
+
+# Renegotiation only exists in TLS 1.2 and earlier.
+Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport())
+
+
+class TestRenegotiationAllowed:
+ '''Verify an allowed client renegotiation is answered promptly, not stranded.'''
+
+ _server_counter: int = 0
+ _ts_counter: int = 0
+
+ def __init__(self) -> None:
+ '''Declare the test Processes.'''
+ self._server = self._configure_server()
+ self._ts = self._configure_trafficserver()
+
+ def _configure_server(self) -> 'Process':
+ '''Configure a plain HTTP/1.0 origin that closes each connection.
+
+ A python http.server origin answers each request on its own connection and
+ closes it, so ATS's client-facing TLS write face is fully drained between
+ requests. When the renegotiation ClientHello then arrives there is no
+ in-flight transport write to coincidentally carry the renegotiation answer
+ out -- exactly the condition under which a proxy that fails to flush its own
+ SSL protocol output would strand the answer and hang the client. (The
+ Proxy-Verifier microserver keeps the response connection in a state that
+ masks the stall, so it is unsuitable for this regression.)
+
+ :return: The origin server Process.
+ '''
+ server = Test.Processes.Process(f'server-{TestRenegotiationAllowed._server_counter}')
+ TestRenegotiationAllowed._server_counter += 1
+
+ origin_dir = os.path.join(Test.RunDirectory, "origin")
+ os.makedirs(origin_dir, exist_ok=True)
+ with open(os.path.join(origin_dir, "index.html"), "w") as f:
+ f.write("ok\n")
+
+ origin_port = get_port(server, "http_port")
+ server.Command = f'{sys.executable} -m http.server {origin_port} --bind 127.0.0.1 --directory {origin_dir}'
+ server.Ready = When.PortOpenv4(origin_port)
+ # The server is a long-running process the harness terminates at the end of
+ # the test, so its exit status is whatever the signal leaves behind.
+ server.ReturnCode = Any(None, 0, -2, -15)
+ return server
+
+ def _configure_trafficserver(self) -> 'Process':
+ '''Configure Traffic Server to honor client renegotiation.
+
+ :return: The Traffic Server Process.
+ '''
+ ts = Test.MakeATSProcess(f'ts-{TestRenegotiationAllowed._ts_counter}', enable_tls=True)
+ TestRenegotiationAllowed._ts_counter += 1
+
+ ts.addSSLfile("ssl/server.pem")
+ ts.addSSLfile("ssl/server.key")
+ ts.Disk.ssl_multicert_yaml.AddLines(
+ """
+ssl_multicert:
+ - dest_ip: "*"
+ ssl_cert_name: server.pem
+ ssl_key_name: server.key
+""".split("\n"))
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}',
+ 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}',
+ # The operator opts in: honor a client-initiated renegotiation.
+ 'proxy.config.ssl.allow_client_renegotiation': 1,
+ # Renegotiation requires a sub-TLS1.3 protocol.
+ 'proxy.config.ssl.TLSv1_3.enabled': 0,
+ 'proxy.config.ssl.TLSv1_2': 1,
+ })
+ ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}')
+
+ # The renegotiation must not abort the process.
+ ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "received signal|failed assertion", "ATS must service the renegotiation without crashing")
+ return ts
+
+ def run(self) -> None:
+ '''Configure and run the TestRuns.'''
+ # Complete a TLSv1.2 handshake and one request, let the response drain, ask
+ # to renegotiate, then drive a second request. ATS must answer the
+ # renegotiation promptly; the helper reports RENEGOTIATION-STALLED (and
+ # exits non-zero) if the client hangs waiting for the answer.
+ tr = Test.AddTestRun("client renegotiation is answered promptly")
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.Processes.Default.Command = (
+ f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_renegotiation_allowed_client.py")} '
+ f'-p {self._ts.Variables.ssl_port} -s example.com')
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(
+ "RENEGOTIATION-COMPLETED|RENEGOTIATION-REFUSED-PROMPTLY", "the renegotiation must be answered, not stranded")
+ tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression(
+ "RENEGOTIATION-STALLED", "the client must not hang waiting for the renegotiation answer")
+ tr.StillRunningAfter = self._ts
+ tr.StillRunningAfter = self._server
+
+ # ATS survived and keeps serving, so a normal request still succeeds.
+ tr = Test.AddTestRun("server still serves requests after the renegotiation")
+ tr.MakeCurlCommand(
+ (
+ "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k "
+ f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' "
+ f"https://example.com:{self._ts.Variables.ssl_port}/"),
+ ts=self._ts)
+ tr.Processes.Default.ReturnCode = 0
+ # curl -v writes the response status line to stderr.
+ tr.Processes.Default.Streams.stderr = Testers.ContainsExpression(
+ "HTTP/1.1 200 OK", "request after renegotiation should succeed")
+ tr.StillRunningAfter = self._ts
+
+
+TestRenegotiationAllowed().run()
diff --git a/tests/gold_tests/tls/tls_renegotiation_allowed_client.py b/tests/gold_tests/tls/tls_renegotiation_allowed_client.py
new file mode 100644
index 0000000000..ea4711be04
--- /dev/null
+++ b/tests/gold_tests/tls/tls_renegotiation_allowed_client.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+'''
+Drive an OpenSSL s_client connection that completes a TLSv1.2 handshake and
+then requests a client-initiated renegotiation, with ATS configured to allow
+client renegotiation. Verifies that ATS answers the renegotiation attempt
+promptly -- either by completing it (libraries that honor it) or by
+delivering its refusal alert (OpenSSL 3.x never honors a client
+renegotiation unless SSL_OP_ALLOW_CLIENT_RENEGOTIATION is set) -- instead of
+stranding the TLS response in its write buffer and leaving the client
+hanging.
+'''
+# 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.
+
+import argparse
+import shlex
+import subprocess
+import sys
+import threading
+import time
+
+
+class OutputCollector:
+ '''Accumulate a subprocess's merged output from a reader thread.'''
+
+ def __init__(self, stream):
+ self._stream = stream
+ self._lock = threading.Lock()
+ self._data = b''
+ self._thread = threading.Thread(target=self._read, daemon=True)
+ self._thread.start()
+
+ def _read(self):
+ while True:
+ chunk = self._stream.read1(4096)
+ if not chunk:
+ break
+ with self._lock:
+ self._data += chunk
+
+ def data(self) -> bytes:
+ with self._lock:
+ return self._data
+
+
+def wait_for(predicate, timeout: float) -> bool:
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ if predicate():
+ return True
+ time.sleep(0.1)
+ return predicate()
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='Trigger a TLSv1.2 client renegotiation against ATS and require a prompt answer.')
+ parser.add_argument('-p', '--ats-port', type=int, dest='ats_port', required=True, help='ATS TLS port number')
+ parser.add_argument('-s', '--server-name', type=str, dest='sni', default='example.com', help='SNI server name')
+ args = parser.parse_args()
+
+ # -tls1_2 forces a protocol that supports renegotiation (TLS 1.3 has none).
+ # No -quiet: it suppresses s_client's interpretation of the "R" command, so
+ # the renegotiation request would otherwise be sent as plain application data.
+ cmd = shlex.split(
+ f'openssl s_client -connect 127.0.0.1:{args.ats_port} -tls1_2 -servername {args.sni} -cipher DEFAULT@SECLEVEL=0')
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ output = OutputCollector(proc.stdout)
+ verdict = 'RENEGOTIATION-STALLED'
+
+ try:
+ request = b'GET / HTTP/1.1\r\nHost: ' + args.sni.encode() + b'\r\n\r\n'
+ proc.stdin.write(request)
+ proc.stdin.flush()
+ if not wait_for(lambda: b'HTTP/1.1 200 OK' in output.data(), 10):
+ print('NO-FIRST-RESPONSE: the request before the renegotiation never completed')
+ proc.kill()
+ return 1
+
+ # Let the first response fully drain so the proxy's TLS write face goes
+ # idle: that is exactly when the renegotiation answer has no in-flight
+ # transport write to ride out on, so a layered proxy that fails to flush
+ # SSL protocol output on its own strands the answer and the client hangs.
+ time.sleep(2)
+
+ # s_client interprets a line containing just "R" as a request to
+ # renegotiate the session.
+ proc.stdin.write(b'R\n')
+ proc.stdin.flush()
+ time.sleep(1)
+ # A second request provokes the client into resolving the
+ # renegotiation it started: it either completes it and sends the
+ # request, or it reads the server's refusal alert and exits.
+ try:
+ proc.stdin.write(request)
+ proc.stdin.flush()
+ except BrokenPipeError:
+ # The client already exited on the refusal alert.
+ pass
+
+ def renegotiation_answered():
+ return output.data().count(b'HTTP/1.1 200 OK') >= 2 or proc.poll() is not None
+
+ if wait_for(renegotiation_answered, 10):
+ if output.data().count(b'HTTP/1.1 200 OK') >= 2:
+ verdict = 'RENEGOTIATION-COMPLETED'
+ else:
+ verdict = 'RENEGOTIATION-REFUSED-PROMPTLY'
+ finally:
+ if proc.poll() is None:
+ proc.kill()
+ proc.wait()
+
+ sys.stdout.write(output.data().decode('utf-8', errors='replace'))
+ print(verdict)
+ return 0 if verdict != 'RENEGOTIATION-STALLED' else 1
+
+
+if __name__ == '__main__':
+ exit(main())
diff --git a/tests/gold_tests/tls/tls_renegotiation_client.py b/tests/gold_tests/tls/tls_renegotiation_client.py
new file mode 100644
index 0000000000..46af21aef0
--- /dev/null
+++ b/tests/gold_tests/tls/tls_renegotiation_client.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+'''
+Drive an OpenSSL s_client connection that completes a TLSv1.2 handshake and
+then requests a client-initiated renegotiation. Used to verify that Traffic
+Server refuses the renegotiation without crashing.
+'''
+# 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.
+
+import argparse
+import shlex
+import subprocess
+import sys
+import time
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description='Trigger a TLSv1.2 client renegotiation against ATS.')
+ parser.add_argument('-p', '--ats-port', type=int, dest='ats_port', required=True, help='ATS TLS port number')
+ parser.add_argument('-s', '--server-name', type=str, dest='sni', default='example.com', help='SNI server name')
+ args = parser.parse_args()
+
+ # -tls1_2 forces a protocol that supports renegotiation (TLS 1.3 has none).
+ # No -quiet: it suppresses s_client's interpretation of the "R" command, so
+ # the renegotiation request would otherwise be sent as plain application data.
+ cmd = shlex.split(
+ f'openssl s_client -connect 127.0.0.1:{args.ats_port} -tls1_2 -servername {args.sni} -cipher DEFAULT@SECLEVEL=0')
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+ # Complete the handshake and one request, then send "R" on its own line:
+ # s_client interprets that as a request to renegotiate the session.
+ try:
+ proc.stdin.write(b'GET / HTTP/1.1\r\nHost: ' + args.sni.encode() + b'\r\n\r\n')
+ proc.stdin.flush()
+ time.sleep(2)
+ proc.stdin.write(b'R\n')
+ proc.stdin.flush()
+ time.sleep(2)
+ proc.stdin.write(b'GET / HTTP/1.1\r\nHost: ' + args.sni.encode() + b'\r\n\r\n')
+ proc.stdin.flush()
+ out, _ = proc.communicate(timeout=10)
+ except (subprocess.TimeoutExpired, BrokenPipeError):
+ proc.kill()
+ out, _ = proc.communicate()
+
+ sys.stdout.write(out.decode('utf-8', errors='replace'))
+ # The client's own exit status is irrelevant: ATS legitimately tears down
+ # the connection on the refused renegotiation. The test asserts on ATS
+ # staying alive, not on this process.
+ exit(0)
+
+
+if __name__ == '__main__':
+ main()
(trafficserver) 27/33: Introduce Clang Thread Safety Analysis, and apply it to two subsystems (#13310)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit ab3a1ccbc68d2a2170f13951da37b66dd0b1d8b6
Author: Mo Chen <mo...@apache.org>
AuthorDate: Tue Jun 23 22:11:59 2026 -0500
Introduce Clang Thread Safety Analysis, and apply it to two subsystems (#13310)
Add TS_* annotation macros (tsutil/ts_thread_safety.h) wrapping Clang's
-Wthread-safety attributes, so a lock's contract -- which mutex guards
which data, which lock a function requires its caller to hold -- can be
expressed in the type system and proved at build time. They expand to
nothing off Clang: no runtime cost, and a no-op for GCC.
Add annotated lock types for the analysis to track: ts::mutex with
ts::lock_guard, and ts::shared_mutex (already ATS's own rwlock) marked
as a capability with ts::write_guard / ts::read_guard. Annotated code
takes its locks through these because the std:: RAII wrappers are too
flexible -- deferred locking, move, adopt/release -- for the analysis to
track, and ATS does not need that flexibility. The guard names mirror
std::lock_guard's rigid acquire-in-constructor / release-in-destructor
RAII rather than implying std::unique_lock's flexibility.
Add the ENABLE_THREAD_SAFETY_ANALYSIS option (Clang-only, on by default
as a warning) and THREAD_SAFETY_ANALYSIS_AS_ERROR, which the CI and
branch presets enable so violations are errors that gate merges while
local and dev builds stay warnings. Install the new headers with the
rest of tsutil, and add a unit test compiled with the analysis enabled
as a worked example. Skip FreeBSD: its libc annotates the pthread
primitives themselves, so -Wthread-safety there flags ATS's existing
hand-rolled mutex wrappers (tscore/ink_mutex.h and others) tree-wide;
bringing FreeBSD into the gate needs those legacy wrappers made
analysis-clean first.
Apply the analysis to two subsystems:
Metrics::Storage: valid(), lookup(IdType) and name() read
_cur_blob/_cur_off/_blobs with no lock held, while create()/createSpan()/
current() access the same fields under the mutex (rename() likewise read
them before locking). A single Storage is shared by all threads, so a
metric registered at runtime -- a plugin TSStatCreate or a config reload
-- advances those fields while live traffic reads them: a data race.
Take the mutex on every access and mark the fields guarded by it; the
reads that were missing a lock take it exclusively, matching the existing
locked paths.
SSLOriginSessionCache: the origin session map and queue are reachable
from every thread; mark them guarded by the cache mutex so the compiler
enforces the locking that was previously only convention. Replace the
hand-rolled lock witness on remove_oldest_session (a std::unique_lock
parameter checked with owns_lock()) with a compile-time TS_REQUIRES
precondition.
(cherry picked from commit 2b6dce0314f32f95860fcef3acaef8397aeb2cc4)
---
CMakeLists.txt | 31 +++++
CMakePresets.json | 2 +
include/tsutil/Metrics.h | 18 +--
include/tsutil/TsMutex.h | 100 +++++++++++++++
include/tsutil/TsSharedMutex.h | 54 +++++++--
include/tsutil/ts_thread_safety.h | 97 +++++++++++++++
src/iocore/net/SSLSessionCache.cc | 21 ++--
src/iocore/net/SSLSessionCache.h | 8 +-
src/tsutil/CMakeLists.txt | 15 +++
src/tsutil/Metrics.cc | 16 +--
src/tsutil/unit_tests/test_thread_safety.cc | 182 ++++++++++++++++++++++++++++
11 files changed, 505 insertions(+), 39 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2dcc13163c..16d262aff2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -169,6 +169,8 @@ option(EXTERNAL_LIBSWOC "Use external libswoc (default OFF)")
option(LINK_PLUGINS "Link core libraries to plugins (default OFF)")
option(ENABLE_PROBES "Enable ATS SystemTap probes (default OFF)")
option(ENABLE_VERIFY_PLUGINS "Enable plugin verification tests (default ON)" ON)
+option(ENABLE_THREAD_SAFETY_ANALYSIS "Enable Clang -Wthread-safety analysis (Clang only, default ON)" ON)
+option(THREAD_SAFETY_ANALYSIS_AS_ERROR "Treat thread-safety findings as errors; for CI gating (default OFF)")
# Setup user
# NOTE: this is the user trafficserver runs as
@@ -358,6 +360,35 @@ elseif(ENABLE_TSAN)
add_link_options(-g -fsanitize=thread)
endif()
+# Clang Thread Safety Analysis. The TS_* annotations (tsutil/ts_thread_safety.h)
+# compile to nothing on GCC, and the flag is Clang-only on purpose (GCC does not
+# know -Wthread-safety and would itself error if passed it), so this whole block
+# is a no-op for GCC builds.
+#
+# On Clang it is on by default but only a WARNING: -Wno-error=thread-safety keeps
+# it a warning even in builds that otherwise use -Werror, so an in-progress
+# annotation never blocks a developer's build. CI sets
+# THREAD_SAFETY_ANALYSIS_AS_ERROR=ON to promote findings to errors and gate
+# merges.
+#
+# Skip FreeBSD: its libc annotates the pthread primitives themselves, so
+# -Wthread-safety there flags ATS's existing hand-rolled mutex wrappers
+# (tscore/ink_mutex.h, ink_rwlock, ...) tree-wide, not just newly-annotated code.
+# Enabling it on FreeBSD needs those legacy wrappers made analysis-clean first.
+# Elsewhere the platform leaves the pthread primitives un-annotated, so only
+# annotated code is analyzed and this stays quiet until a real violation.
+if(ENABLE_THREAD_SAFETY_ANALYSIS
+ AND CMAKE_CXX_COMPILER_ID MATCHES "Clang"
+ AND NOT CMAKE_SYSTEM_NAME STREQUAL "FreeBSD"
+)
+ add_compile_options(-Wthread-safety)
+ if(THREAD_SAFETY_ANALYSIS_AS_ERROR)
+ add_compile_options(-Werror=thread-safety)
+ else()
+ add_compile_options(-Wno-error=thread-safety)
+ endif()
+endif()
+
if(ENABLE_PROBES)
add_compile_options("-DENABLE_SYSTEMTAP_PROBES")
endif()
diff --git a/CMakePresets.json b/CMakePresets.json
index 6e0735c87f..cec863644f 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -125,6 +125,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_COMPILE_WARNING_AS_ERROR": "ON",
+ "ENABLE_THREAD_SAFETY_ANALYSIS": "ON",
+ "THREAD_SAFETY_ANALYSIS_AS_ERROR": "ON",
"ENABLE_CCACHE": "ON",
"BUILD_EXPERIMENTAL_PLUGINS": "ON",
"ENABLE_WASM_WAMR": "OFF",
diff --git a/include/tsutil/Metrics.h b/include/tsutil/Metrics.h
index 630f079b4d..4f47fd5485 100644
--- a/include/tsutil/Metrics.h
+++ b/include/tsutil/Metrics.h
@@ -38,6 +38,7 @@
#include "swoc/MemSpan.h"
#include "tsutil/Assert.h"
+#include "tsutil/TsMutex.h"
namespace ts
{
@@ -304,17 +305,17 @@ private:
class Storage
{
- BlobStorage _blobs;
- uint16_t _cur_blob = 0;
- uint16_t _cur_off = 0;
- LookupTable _lookups;
- mutable std::mutex _mutex;
+ BlobStorage _blobs TS_GUARDED_BY(_mutex);
+ uint16_t _cur_blob TS_GUARDED_BY(_mutex) = 0;
+ uint16_t _cur_off TS_GUARDED_BY(_mutex) = 0;
+ LookupTable _lookups TS_GUARDED_BY(_mutex);
+ mutable ts::mutex _mutex;
public:
Storage(const Storage &) = delete;
Storage &operator=(const Storage &) = delete;
- Storage()
+ Storage() TS_NO_THREAD_SAFETY_ANALYSIS // single-threaded construction; not yet shared
{
_blobs[0] = std::make_unique<NamesAndAtomics>();
release_assert(_blobs[0]);
@@ -325,7 +326,7 @@ private:
~Storage() {}
IdType create(const std::string_view name, const MetricType type = MetricType::COUNTER);
- void addBlob();
+ void addBlob() TS_REQUIRES(_mutex);
IdType lookup(const std::string_view name) const;
AtomicType *lookup(const std::string_view name, IdType *out_id, MetricType *out_type = nullptr) const;
AtomicType *lookup(Metrics::IdType id, std::string_view *out_name = nullptr, MetricType *out_type = nullptr) const;
@@ -337,7 +338,7 @@ private:
std::pair<int16_t, int16_t>
current() const
{
- std::lock_guard lock(_mutex);
+ ts::lock_guard lock(_mutex);
return {_cur_blob, _cur_off};
}
@@ -346,6 +347,7 @@ private:
{
auto [blob, entry] = _splitID(id);
+ ts::lock_guard lock(_mutex);
return (id >= 0 && ((blob < _cur_blob && entry < MAX_SIZE) || (blob == _cur_blob && entry <= _cur_off)));
}
};
diff --git a/include/tsutil/TsMutex.h b/include/tsutil/TsMutex.h
new file mode 100644
index 0000000000..6bc193dc4e
--- /dev/null
+++ b/include/tsutil/TsMutex.h
@@ -0,0 +1,100 @@
+/** @file
+
+ A std::mutex annotated for Clang Thread Safety Analysis, with a matching
+ scoped lock guard.
+
+ These are the plain-mutex counterparts to ts::shared_mutex and its
+ reader/writer guards (TsSharedMutex.h): use ts::mutex with ts::lock_guard
+ wherever you would otherwise use std::mutex with std::lock_guard, but want the
+ data it protects checked by -Wthread-safety. The runtime behavior is exactly
+ that of std::mutex; the annotations are compile-time only (see
+ tsutil/ts_thread_safety.h).
+
+ @section license License
+
+ 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.
+ */
+
+#pragma once
+
+#include <mutex>
+
+#include "tsutil/ts_thread_safety.h"
+
+namespace ts
+{
+// A std::mutex marked as a Clang thread-safety capability, so data guarded by it
+// can be checked by -Wthread-safety. Same interface and runtime behavior as
+// std::mutex.
+//
+class TS_CAPABILITY("mutex") mutex
+{
+public:
+ mutex() = default;
+ mutex(mutex const &) = delete;
+ mutex &operator=(mutex const &) = delete;
+
+ // The lock/unlock bodies are the trusted implementation of this capability:
+ // exempt them from analysis so only the capability contract on each signature
+ // is checked. The actual data-race checking happens at the call sites.
+ void
+ lock() TS_ACQUIRE() TS_NO_THREAD_SAFETY_ANALYSIS
+ {
+ _m.lock();
+ }
+ bool
+ try_lock() TS_TRY_ACQUIRE(true) TS_NO_THREAD_SAFETY_ANALYSIS
+ {
+ return _m.try_lock();
+ }
+ void
+ unlock() TS_RELEASE() TS_NO_THREAD_SAFETY_ANALYSIS
+ {
+ _m.unlock();
+ }
+
+ using native_handle_type = std::mutex::native_handle_type;
+
+ native_handle_type
+ native_handle()
+ {
+ return _m.native_handle();
+ }
+
+private:
+ std::mutex _m;
+};
+
+// RAII guard for ts::mutex that carries Clang thread-safety capability state.
+// Prefer over std::lock_guard / std::unique_lock in code annotated for
+// -Wthread-safety (see tsutil/ts_thread_safety.h for why the std wrappers are
+// not tracked).
+//
+class TS_SCOPED_CAPABILITY lock_guard
+{
+public:
+ explicit lock_guard(mutex &m) TS_ACQUIRE(m) : _m(m) { _m.lock(); }
+ ~lock_guard() TS_RELEASE() { _m.unlock(); }
+
+ lock_guard(lock_guard const &) = delete;
+ lock_guard &operator=(lock_guard const &) = delete;
+
+private:
+ mutex &_m;
+};
+
+} // end namespace ts
diff --git a/include/tsutil/TsSharedMutex.h b/include/tsutil/TsSharedMutex.h
index ccd025c98a..bf09885040 100644
--- a/include/tsutil/TsSharedMutex.h
+++ b/include/tsutil/TsSharedMutex.h
@@ -27,6 +27,7 @@
#include <pthread.h>
#include "tsutil/Strerror.h"
#include "tsutil/Assert.h"
+#include "tsutil/ts_thread_safety.h"
#ifdef X
#error "X preprocessor symbol defined"
@@ -48,7 +49,7 @@ namespace ts
{
// A class with the same interface as std::shared_mutex, but which is not prone to writer starvation.
//
-class shared_mutex
+class TS_CAPABILITY("shared_mutex") shared_mutex
{
public:
shared_mutex() {}
@@ -58,8 +59,13 @@ public:
shared_mutex(shared_mutex const &) = delete;
shared_mutex &operator=(shared_mutex const &) = delete;
+ // The lock/unlock methods are the trusted implementation of this capability:
+ // their bodies drive the raw pthread_rwlock_t, which some libc headers (e.g.
+ // FreeBSD's <pthread.h>) annotate as a capability in its own right. Exempt the
+ // bodies from analysis so only the capability contract on each signature is
+ // checked; the actual data-race checking happens at the call sites.
void
- lock()
+ lock() TS_ACQUIRE() TS_NO_THREAD_SAFETY_ANALYSIS
{
int error = pthread_rwlock_wrlock(&_lock);
if (error != 0) {
@@ -69,7 +75,7 @@ public:
}
bool
- try_lock()
+ try_lock() TS_TRY_ACQUIRE(true) TS_NO_THREAD_SAFETY_ANALYSIS
{
int error = pthread_rwlock_trywrlock(&_lock);
if (EBUSY == error) {
@@ -84,7 +90,7 @@ public:
}
void
- unlock()
+ unlock() TS_RELEASE() TS_NO_THREAD_SAFETY_ANALYSIS
{
X(debug_assert(_exclusive);)
X(_exclusive = false;)
@@ -93,7 +99,7 @@ public:
}
void
- lock_shared()
+ lock_shared() TS_ACQUIRE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS
{
int error = pthread_rwlock_rdlock(&_lock);
if (error != 0) {
@@ -105,7 +111,7 @@ public:
}
bool
- try_lock_shared()
+ try_lock_shared() TS_TRY_ACQUIRE_SHARED(true) TS_NO_THREAD_SAFETY_ANALYSIS
{
int error = pthread_rwlock_tryrdlock(&_lock);
if (EBUSY == error) {
@@ -121,7 +127,7 @@ public:
}
void
- unlock_shared()
+ unlock_shared() TS_RELEASE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS
{
X(debug_assert(_shared > 0);)
X(--_shared;)
@@ -148,7 +154,7 @@ public:
private:
void
- _unlock()
+ _unlock() TS_NO_THREAD_SAFETY_ANALYSIS
{
int error = pthread_rwlock_unlock(&_lock);
if (error != 0) {
@@ -178,6 +184,38 @@ private:
X(std::atomic<int> _shared{0};)
};
+// RAII guards for ts::shared_mutex that carry Clang thread-safety capability
+// state. Prefer these over std::unique_lock / std::shared_lock in code annotated
+// for -Wthread-safety: the analysis does not reliably track the std wrappers
+// (see tsutil/ts_thread_safety.h).
+//
+class TS_SCOPED_CAPABILITY write_guard
+{
+public:
+ explicit write_guard(shared_mutex &m) TS_ACQUIRE(m) : _m(m) { _m.lock(); }
+ ~write_guard() TS_RELEASE() { _m.unlock(); }
+
+ write_guard(write_guard const &) = delete;
+ write_guard &operator=(write_guard const &) = delete;
+
+private:
+ shared_mutex &_m;
+};
+
+class TS_SCOPED_CAPABILITY read_guard
+{
+public:
+ explicit read_guard(shared_mutex &m) TS_ACQUIRE_SHARED(m) : _m(m) { _m.lock_shared(); }
+ // A scoped-capability destructor uses the plain release form even for a shared acquire.
+ ~read_guard() TS_RELEASE() { _m.unlock_shared(); }
+
+ read_guard(read_guard const &) = delete;
+ read_guard &operator=(read_guard const &) = delete;
+
+private:
+ shared_mutex &_m;
+};
+
} // end namespace ts
#undef X
diff --git a/include/tsutil/ts_thread_safety.h b/include/tsutil/ts_thread_safety.h
new file mode 100644
index 0000000000..36e3b36e4e
--- /dev/null
+++ b/include/tsutil/ts_thread_safety.h
@@ -0,0 +1,97 @@
+/** @file
+
+ Clang Thread Safety Analysis annotation macros.
+
+ These wrap Clang's @c -Wthread-safety attributes so a lock's contract can be
+ expressed in the type system: which mutex guards which data, and which lock a
+ function requires its caller to hold. The compiler then proves, at build time,
+ that guarded data is only touched while the right lock is held.
+
+ The annotations are compile-time only. Under Clang they expand to
+ @c __attribute__((...)) consumed by the analysis; under every other compiler
+ they expand to nothing. They generate no code and have zero runtime cost.
+
+ Important note: Clang's analysis only tracks lock state through types marked as
+ capabilities (the mutex) and scoped capabilities (the RAII guard). The @c std::
+ lock wrappers are deliberately flexible -- deferred locking, adopt/release, and
+ movability let the held state escape a single scope -- which makes it impossible
+ to track statically, and ATS does not need that flexibility. Annotated code
+ therefore takes its locks through ATS-owned annotated types -- @c ts::mutex with
+ @c ts::lock_guard, or @c ts::shared_mutex with @c ts::write_guard /
+ @c ts::read_guard -- whose simple acquire-in-constructor /
+ release-in-destructor contract the analysis can follow.
+
+ @section license License
+
+ 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.
+ */
+
+#pragma once
+
+#if defined(__clang__)
+#define TS_THREAD_ANNOTATION(x) __attribute__((x))
+#else
+#define TS_THREAD_ANNOTATION(x)
+#endif
+
+/// Mark a class as a capability (a lock-like object that can be "held").
+#define TS_CAPABILITY(name) TS_THREAD_ANNOTATION(capability(name))
+
+/// Mark an RAII type whose lifetime holds a capability (acquire in ctor,
+/// release in dtor), e.g. a scoped lock guard.
+#define TS_SCOPED_CAPABILITY TS_THREAD_ANNOTATION(scoped_lockable)
+
+/// The annotated data member may only be accessed while @a x is held.
+#define TS_GUARDED_BY(x) TS_THREAD_ANNOTATION(guarded_by(x))
+
+/// The data pointed to by the annotated pointer may only be accessed while
+/// @a x is held.
+#define TS_PT_GUARDED_BY(x) TS_THREAD_ANNOTATION(pt_guarded_by(x))
+
+/// The function acquires the listed capabilities (exclusively / shared).
+#define TS_ACQUIRE(...) TS_THREAD_ANNOTATION(acquire_capability(__VA_ARGS__))
+#define TS_ACQUIRE_SHARED(...) TS_THREAD_ANNOTATION(acquire_shared_capability(__VA_ARGS__))
+
+/// The function releases the listed capabilities. Use the plain form (not the
+/// shared form) on a scoped-capability destructor even for a shared guard.
+#define TS_RELEASE(...) TS_THREAD_ANNOTATION(release_capability(__VA_ARGS__))
+#define TS_RELEASE_SHARED(...) TS_THREAD_ANNOTATION(release_shared_capability(__VA_ARGS__))
+
+/// The function conditionally acquires a capability, holding it only on the
+/// branch where it returns @a success_value.
+#define TS_TRY_ACQUIRE(...) TS_THREAD_ANNOTATION(try_acquire_capability(__VA_ARGS__))
+#define TS_TRY_ACQUIRE_SHARED(...) TS_THREAD_ANNOTATION(try_acquire_shared_capability(__VA_ARGS__))
+
+/// The caller must already hold the listed capabilities (exclusively / shared).
+#define TS_REQUIRES(...) TS_THREAD_ANNOTATION(requires_capability(__VA_ARGS__))
+#define TS_REQUIRES_SHARED(...) TS_THREAD_ANNOTATION(requires_shared_capability(__VA_ARGS__))
+
+/// The caller must NOT hold the listed capabilities (prevents self-deadlock).
+#define TS_EXCLUDES(...) TS_THREAD_ANNOTATION(locks_excluded(__VA_ARGS__))
+
+/// Declare that a getter returns a reference to the named capability.
+#define TS_RETURN_CAPABILITY(x) TS_THREAD_ANNOTATION(lock_returned(x))
+
+/// Tell the analysis a capability is held here without acquiring it (runtime
+/// assertion form).
+#define TS_ASSERT_CAPABILITY(x) TS_THREAD_ANNOTATION(assert_capability(x))
+#define TS_ASSERT_SHARED_CAPABILITY(x) TS_THREAD_ANNOTATION(assert_shared_capability(x))
+
+/// Disable the analysis for a single function. Use sparingly, for code the
+/// analysis cannot model (recursive acquire, hand-off across threads,
+/// single-threaded destructors).
+#define TS_NO_THREAD_SAFETY_ANALYSIS TS_THREAD_ANNOTATION(no_thread_safety_analysis)
diff --git a/src/iocore/net/SSLSessionCache.cc b/src/iocore/net/SSLSessionCache.cc
index bf34359343..a459bf40f3 100644
--- a/src/iocore/net/SSLSessionCache.cc
+++ b/src/iocore/net/SSLSessionCache.cc
@@ -339,7 +339,7 @@ SSLSessionBucket::~SSLSessionBucket() {}
SSLOriginSessionCache::SSLOriginSessionCache() {}
-SSLOriginSessionCache::~SSLOriginSessionCache()
+SSLOriginSessionCache::~SSLOriginSessionCache() TS_NO_THREAD_SAFETY_ANALYSIS // single-threaded teardown
{
while (auto *node = orig_sess_que.pop()) {
delete node;
@@ -374,8 +374,8 @@ SSLOriginSessionCache::insert_session(const std::string &lookup_key, SSL_SESSION
new SSLOriginSession(lookup_key, curve, group_name, std::shared_ptr<SSL_SESSION>{sess_ptr, SSLSessDeleter}));
auto new_node = ssl_orig_session.release();
- std::unique_lock lock(mutex);
- auto entry = orig_sess_map.find(lookup_key);
+ ts::write_guard lock(mutex);
+ auto entry = orig_sess_map.find(lookup_key);
if (entry != orig_sess_map.end()) {
auto node = entry->second;
Dbg(dbg_ctl_ssl_origin_session_cache, "found duplicate key: %s, replacing %p with %p", lookup_key.c_str(),
@@ -385,7 +385,7 @@ SSLOriginSessionCache::insert_session(const std::string &lookup_key, SSL_SESSION
delete node;
} else if (orig_sess_map.size() >= SSLConfigParams::origin_session_cache_size) {
Dbg(dbg_ctl_ssl_origin_session_cache, "origin session cache full, removing oldest session");
- remove_oldest_session(lock);
+ remove_oldest_session();
}
orig_sess_que.enqueue(new_node);
@@ -397,8 +397,8 @@ SSLOriginSessionCache::get_session(const std::string &lookup_key, ssl_curve_id *
{
Dbg(dbg_ctl_ssl_origin_session_cache, "get session: %s", lookup_key.c_str());
- std::shared_lock lock(mutex);
- auto entry = orig_sess_map.find(lookup_key);
+ ts::read_guard lock(mutex);
+ auto entry = orig_sess_map.find(lookup_key);
if (entry == orig_sess_map.end()) {
return nullptr;
}
@@ -413,11 +413,8 @@ SSLOriginSessionCache::get_session(const std::string &lookup_key, ssl_curve_id *
}
void
-SSLOriginSessionCache::remove_oldest_session(const std::unique_lock<ts::shared_mutex> &lock)
+SSLOriginSessionCache::remove_oldest_session()
{
- // Caller must hold the bucket shared_mutex with unique_lock.
- ink_release_assert(lock.owns_lock());
-
while (orig_sess_que.head && orig_sess_que.size >= static_cast<int>(SSLConfigParams::origin_session_cache_size)) {
auto node = orig_sess_que.pop();
Dbg(dbg_ctl_ssl_origin_session_cache, "remove oldest session: %s, session ptr: %p", node->key.c_str(), node->shared_sess.get());
@@ -430,8 +427,8 @@ void
SSLOriginSessionCache::remove_session(const std::string &lookup_key)
{
// We can't bail on contention here because this session MUST be removed.
- std::unique_lock lock(mutex);
- auto entry = orig_sess_map.find(lookup_key);
+ ts::write_guard lock(mutex);
+ auto entry = orig_sess_map.find(lookup_key);
if (entry != orig_sess_map.end()) {
auto node = entry->second;
Dbg(dbg_ctl_ssl_origin_session_cache, "remove session: %s, session ptr: %p", lookup_key.c_str(), node->shared_sess.get());
diff --git a/src/iocore/net/SSLSessionCache.h b/src/iocore/net/SSLSessionCache.h
index 769c20adf1..6cba98a84d 100644
--- a/src/iocore/net/SSLSessionCache.h
+++ b/src/iocore/net/SSLSessionCache.h
@@ -228,9 +228,9 @@ public:
void remove_session(const std::string &lookup_key);
private:
- void remove_oldest_session(const std::unique_lock<ts::shared_mutex> &lock);
+ void remove_oldest_session() TS_REQUIRES(mutex);
- mutable ts::shared_mutex mutex;
- CountQueue<SSLOriginSession> orig_sess_que;
- std::map<std::string, SSLOriginSession *> orig_sess_map;
+ mutable ts::shared_mutex mutex;
+ CountQueue<SSLOriginSession> orig_sess_que TS_GUARDED_BY(mutex);
+ std::map<std::string, SSLOriginSession *> orig_sess_map TS_GUARDED_BY(mutex);
};
diff --git a/src/tsutil/CMakeLists.txt b/src/tsutil/CMakeLists.txt
index 49be0fb553..7d0e9a651c 100644
--- a/src/tsutil/CMakeLists.txt
+++ b/src/tsutil/CMakeLists.txt
@@ -32,7 +32,9 @@ set(TSUTIL_PUBLIC_HEADERS
${PROJECT_SOURCE_DIR}/include/tsutil/Strerror.h
${PROJECT_SOURCE_DIR}/include/tsutil/StringConvert.h
${PROJECT_SOURCE_DIR}/include/tsutil/Regex.h
+ ${PROJECT_SOURCE_DIR}/include/tsutil/TsMutex.h
${PROJECT_SOURCE_DIR}/include/tsutil/TsSharedMutex.h
+ ${PROJECT_SOURCE_DIR}/include/tsutil/ts_thread_safety.h
${PROJECT_SOURCE_DIR}/include/tsutil/YamlCfg.h
${PROJECT_SOURCE_DIR}/include/tsutil/ts_ip.h
${PROJECT_SOURCE_DIR}/include/tsutil/ts_meta.h
@@ -80,9 +82,22 @@ if(BUILD_TESTING)
unit_tests/test_StringConvert.cc
unit_tests/test_Regex.cc
unit_tests/test_ts_meta.cc
+ unit_tests/test_thread_safety.cc
unit_tests/test_time_parser.cc
)
+ # The thread-safety test doubles as a compile-time check: build it with the
+ # analysis enabled and treat any finding as an error so a broken annotation
+ # chain fails the build. Scoped to the same lanes as the global analysis block
+ # in the top-level CMakeLists.txt (Clang, excluding FreeBSD); a no-op for GCC,
+ # where the macros expand to nothing.
+ if(NOT CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+ set_source_files_properties(
+ unit_tests/test_thread_safety.cc
+ PROPERTIES COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,AppleClang>:-Wthread-safety;-Werror=thread-safety>"
+ )
+ endif()
+
target_link_libraries(test_tsutil PRIVATE tsutil Catch2::Catch2WithMain)
add_catch2_test(NAME test_tsutil COMMAND $<TARGET_FILE:test_tsutil>)
diff --git a/src/tsutil/Metrics.cc b/src/tsutil/Metrics.cc
index ae96c1dbba..92bcb3bf6b 100644
--- a/src/tsutil/Metrics.cc
+++ b/src/tsutil/Metrics.cc
@@ -57,8 +57,8 @@ Metrics::Storage::addBlob() // The mutex must be held before calling this!
Metrics::IdType
Metrics::Storage::create(std::string_view name, const MetricType type)
{
- std::lock_guard lock(_mutex);
- auto it = _lookups.find(name);
+ ts::lock_guard lock(_mutex);
+ auto it = _lookups.find(name);
if (it != _lookups.end()) {
return it->second;
@@ -81,8 +81,8 @@ Metrics::Storage::create(std::string_view name, const MetricType type)
Metrics::IdType
Metrics::Storage::lookup(const std::string_view name) const
{
- std::lock_guard lock(_mutex);
- auto it = _lookups.find(name);
+ ts::lock_guard lock(_mutex);
+ auto it = _lookups.find(name);
if (it != _lookups.end()) {
return it->second;
@@ -94,6 +94,7 @@ Metrics::Storage::lookup(const std::string_view name) const
Metrics::AtomicType *
Metrics::Storage::lookup(Metrics::IdType id, std::string_view *out_name, Metrics::MetricType *out_type) const
{
+ ts::lock_guard lock(_mutex);
auto [blob_ix, offset] = _splitID(id);
Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get();
@@ -140,6 +141,7 @@ Metrics::Storage::lookup(const std::string_view name, Metrics::IdType *out_id, M
std::string_view
Metrics::Storage::name(Metrics::IdType id) const
{
+ ts::lock_guard lock(_mutex);
auto [blob_ix, offset] = _splitID(id);
Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get();
@@ -164,7 +166,7 @@ Metrics::SpanType
Metrics::Storage::createSpan(size_t size, Metrics::MetricType type, Metrics::IdType *id)
{
release_assert(size <= MAX_SIZE);
- std::lock_guard lock(_mutex);
+ ts::lock_guard lock(_mutex);
if (_cur_off + size > MAX_SIZE) {
addBlob();
@@ -187,6 +189,7 @@ Metrics::Storage::createSpan(size_t size, Metrics::MetricType type, Metrics::IdT
bool
Metrics::Storage::rename(Metrics::IdType id, std::string_view name)
{
+ ts::lock_guard lock(_mutex);
auto [blob_ix, offset] = _splitID(id);
Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get();
@@ -195,8 +198,7 @@ Metrics::Storage::rename(Metrics::IdType id, std::string_view name)
return false;
}
- std::string &cur = std::get<0>(std::get<0>(*blob)[offset]);
- std::lock_guard lock(_mutex);
+ std::string &cur = std::get<0>(std::get<0>(*blob)[offset]);
if (cur.length() > 0) {
_lookups.erase(cur);
diff --git a/src/tsutil/unit_tests/test_thread_safety.cc b/src/tsutil/unit_tests/test_thread_safety.cc
new file mode 100644
index 0000000000..1325534977
--- /dev/null
+++ b/src/tsutil/unit_tests/test_thread_safety.cc
@@ -0,0 +1,182 @@
+/** @file
+
+ End-to-end test of the Clang thread-safety annotation chain.
+
+ This translation unit is compiled with -Wthread-safety -Werror=thread-safety
+ under Clang (see CMakeLists.txt), so it doubles as a compile-time assertion:
+ the annotated example below must analyze cleanly, and any access to the
+ protected data outside a held guard would fail the build.
+
+ The chain demonstrated here is:
+ * ts::mutex / ts::shared_mutex are capabilities (TsMutex.h,
+ TsSharedMutex.h),
+ * ts::lock_guard / ts::write_guard /
+ ts::read_guard supply the held capability for
+ the duration of a scope (TsMutex.h,
+ TsSharedMutex.h),
+ * the data members below are annotated as protected by that capability.
+
+ @section license License
+
+ 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.
+ */
+
+#include "tsutil/TsMutex.h"
+#include "tsutil/TsSharedMutex.h"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <map>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace
+{
+class ProtectedTable
+{
+public:
+ void
+ put(const std::string &key, int value)
+ {
+ ts::write_guard lock(_mutex);
+ _map[key] = value; // write under exclusive lock
+ bump_locked(); // calls a TS_REQUIRES helper while the lock is held
+ }
+
+ int
+ get(const std::string &key)
+ {
+ ts::read_guard lock(_mutex);
+ auto it = _map.find(key); // read under shared lock
+ return it == _map.end() ? -1 : it->second;
+ }
+
+ int
+ writes() const
+ {
+ ts::read_guard lock(_mutex);
+ return _writes;
+ }
+
+private:
+ // The compiler proves the caller holds _mutex; no runtime witness needed.
+ void
+ bump_locked() TS_REQUIRES(_mutex)
+ {
+ ++_writes;
+ }
+
+ mutable ts::shared_mutex _mutex;
+ std::map<std::string, int> _map TS_GUARDED_BY(_mutex);
+ int _writes TS_GUARDED_BY(_mutex) = 0;
+};
+
+// The plain-mutex counterpart: ts::mutex guarded data taken through
+// ts::lock_guard.
+class ProtectedCounter
+{
+public:
+ void
+ add(int n)
+ {
+ ts::lock_guard lock(_mutex);
+ _sum += n; // write under exclusive lock
+ bump_locked(); // calls a TS_REQUIRES helper while the lock is held
+ }
+
+ int
+ sum() const
+ {
+ ts::lock_guard lock(_mutex);
+ return _sum;
+ }
+
+ int
+ adds() const
+ {
+ ts::lock_guard lock(_mutex);
+ return _adds;
+ }
+
+private:
+ void
+ bump_locked() TS_REQUIRES(_mutex)
+ {
+ ++_adds;
+ }
+
+ mutable ts::mutex _mutex;
+ int _sum TS_GUARDED_BY(_mutex) = 0;
+ int _adds TS_GUARDED_BY(_mutex) = 0;
+};
+
+// Counterexample (kept as documentation): adding an unguarded accessor such as
+// int peek() const { return _writes; }
+// to ProtectedTable makes this file fail to compile under -Werror=thread-safety,
+// with: "reading variable '_writes' requires holding shared_mutex '_mutex'".
+// That compile-time failure is the whole point.
+} // namespace
+
+TEST_CASE("ts::shared_mutex annotated guards provide mutual exclusion", "[thread_safety]")
+{
+ ProtectedTable table;
+
+ constexpr int n_threads = 8;
+ constexpr int per_thread = 1000;
+
+ std::vector<std::thread> threads;
+ threads.reserve(n_threads);
+ for (int t = 0; t < n_threads; ++t) {
+ threads.emplace_back([&table, t]() {
+ const std::string key = "k" + std::to_string(t);
+ for (int i = 0; i < per_thread; ++i) {
+ table.put(key, i);
+ (void)table.get(key);
+ }
+ });
+ }
+ for (auto &th : threads) {
+ th.join();
+ }
+
+ REQUIRE(table.writes() == n_threads * per_thread);
+}
+
+TEST_CASE("ts::mutex annotated guard provides mutual exclusion", "[thread_safety]")
+{
+ ProtectedCounter counter;
+
+ constexpr int n_threads = 8;
+ constexpr int per_thread = 1000;
+
+ std::vector<std::thread> threads;
+ threads.reserve(n_threads);
+ for (int t = 0; t < n_threads; ++t) {
+ threads.emplace_back([&counter]() {
+ for (int i = 0; i < per_thread; ++i) {
+ counter.add(1);
+ }
+ });
+ }
+ for (auto &th : threads) {
+ th.join();
+ }
+
+ REQUIRE(counter.sum() == n_threads * per_thread);
+ REQUIRE(counter.adds() == n_threads * per_thread);
+}
(trafficserver) 21/33: header_rewrite: Fix a leak and truncation in set-body-from (#13303)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit e8fe4526bb7077426908c96cf6417aed6953b89d
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 08:54:42 2026 -0500
header_rewrite: Fix a leak and truncation in set-body-from (#13303)
Two related fixes in the header_rewrite set-body-from operator's createRequestString():
Memory leak. The request URL was fetched with TSUrlStringGet(), which returns a TSmalloc()-allocated buffer the caller owns, but it was never freed. Every non-internal transaction matching a set-body-from rule leaked one URL-sized allocation, growing unbounded over time. Free it, mirroring the existing owned-pointer handling in ConditionUrl.
Truncation / buffer over-read. snprintf() returns the length it would have written, not the number of bytes actually written, and that value can exceed the 256-byte req_buf. Passing it to TSFetchUrl() as the request length reads past the stack buffer when a URL overflows. The request has always been capped at 256 bytes — snprintf() only ever writes that much into req_buf — so this changes nothing for requests that fit; it just returns TS_ERROR on overflow instead of over-reading. (Rai [...]
(cherry picked from commit 92112e1b8ac31030c150f321e98392e7766b503f)
---
plugins/header_rewrite/operators.cc | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc
index 53baa8456e..133991228c 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -100,12 +100,19 @@ createRequestString(const std::string_view &value, char (&req_buf)[MAX_SIZE], in
if (TSUrlCreate(url_buf, &url_loc) == TS_SUCCESS && TSUrlParse(url_buf, url_loc, &start, end) == TS_PARSE_DONE) {
const char *host = TSUrlHostGet(url_buf, url_loc, &host_len);
- const char *url = TSUrlStringGet(url_buf, url_loc, &url_len);
+ char *url = TSUrlStringGet(url_buf, url_loc, &url_len);
- *req_buf_size = snprintf(req_buf, MAX_SIZE, "GET %.*s HTTP/1.1\r\nHost: %.*s\r\n\r\n", url_len, url, host_len, host);
+ int written = snprintf(req_buf, MAX_SIZE, "GET %.*s HTTP/1.1\r\nHost: %.*s\r\n\r\n", url_len, url, host_len, host);
+ TSfree(url);
TSMBufferDestroy(url_buf);
+ if (written < 0 || static_cast<unsigned int>(written) >= MAX_SIZE) {
+ Dbg(pi_dbg_ctl, "Request string does not fit in %u byte buffer, not sending truncated request", MAX_SIZE);
+ return TS_ERROR;
+ }
+ *req_buf_size = written;
+
return TS_SUCCESS;
} else {
Dbg(pi_dbg_ctl, "Failed to parse url %s", start);
(trafficserver) 09/33: Cripts: make URL::Query copyable via deep-copy of _state (#13269)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 2b0ccc94ad041cfb0d6d28c7b3fb610a4a3f2671
Author: Serris Santos <se...@gmail.com>
AuthorDate: Thu Jun 18 12:31:47 2026 -0700
Cripts: make URL::Query copyable via deep-copy of _state (#13269)
(cherry picked from commit 2441fa711474b334e66b376e78b1a333a5e6041b)
---
include/cripts/Urls.hpp | 38 +++++++++++++++
src/cripts/tests/query_test.cc | 15 ++++++
tests/gold_tests/cripts/cripts.test.py | 44 +++++++++++++++++-
tests/gold_tests/cripts/files/query_copy.cript | 64 ++++++++++++++++++++++++++
tests/gold_tests/cripts/gold/basic_cript.gold | 1 +
tests/gold_tests/cripts/gold/certs_cript.gold | 1 +
6 files changed, 162 insertions(+), 1 deletion(-)
diff --git a/include/cripts/Urls.hpp b/include/cripts/Urls.hpp
index 73580aa068..3ed143bae2 100644
--- a/include/cripts/Urls.hpp
+++ b/include/cripts/Urls.hpp
@@ -335,6 +335,25 @@ public:
using Component::Component;
+ // _state is deep-copied, but the segment `string_view`s (and Component::_owner) still
+ // reference the source URL. The copy's views dangle if the source URL's path is rewritten,
+ // and Flush()/operator= on the copy write back to the source URL via TSUrlPathSet.
+ Path(const self_type &o) : Component(o), _state(o._state ? std::make_unique<State>(*o._state) : nullptr) {}
+
+ self_type &
+ operator=(const self_type &o)
+ {
+ if (this != &o) {
+ auto new_state = o._state ? std::make_unique<State>(*o._state) : nullptr;
+ Component::operator=(o);
+ _state = std::move(new_state);
+ }
+ return *this;
+ }
+
+ Path(self_type &&) = default;
+ self_type &operator=(self_type &&) = default;
+
void Reset() override;
cripts::string_view GetSV() override;
@@ -485,6 +504,25 @@ public:
_state->standalone = true;
}
+ // _state is deep-copied, but the parameter `string_view`s (and Component::_owner) still
+ // reference the source URL. The copy's views dangle if the source URL's query is rewritten,
+ // and Flush()/operator= on the copy write back to the source URL via TSUrlHttpQuerySet.
+ Query(const self_type &o) : Component(o), _state(o._state ? std::make_unique<State>(*o._state) : nullptr) {}
+
+ self_type &
+ operator=(const self_type &o)
+ {
+ if (this != &o) {
+ auto new_state = o._state ? std::make_unique<State>(*o._state) : nullptr;
+ Component::operator=(o);
+ _state = std::move(new_state);
+ }
+ return *this;
+ }
+
+ Query(self_type &&) = default;
+ self_type &operator=(self_type &&) = default;
+
void Reset() override;
cripts::string_view GetSV() override;
diff --git a/src/cripts/tests/query_test.cc b/src/cripts/tests/query_test.cc
index 8c29598201..83500dd23a 100644
--- a/src/cripts/tests/query_test.cc
+++ b/src/cripts/tests/query_test.cc
@@ -31,6 +31,21 @@ do_remap()
{
borrow url = Client::URL::get();
+ // Force the live Query to parse so _state is populated;
+ // otherwise copies won't exercise the deep-copy path.
+ url.query["foo"];
+
+ // Copy-construct Query to exercise the explicit copy ctor (Query owns a std::unique_ptr<State>).
+ // Note: this remains tied to the same underlying URL; it is not an independent snapshot.
+ cripts::Url::Query query_copy = url.query;
+ query_copy.Erase("foo");
+
+ // Print the copy before mutating the live URL. After url.query.Flush() below,
+ // url.query is Reset()'d and TSUrlHttpQuerySet may invalidate the TS-owned
+ // buffer that query_copy's State string_views point into.
+ CDebug("Live: {}", url.query);
+ CDebug("Copy (foo erased): {}", query_copy);
+
switch (AsInteger(instance.data[0])) {
case 0:
url.query.Erase({"foo", "bar"}, true);
diff --git a/tests/gold_tests/cripts/cripts.test.py b/tests/gold_tests/cripts/cripts.test.py
index ce46154ee5..501a6aac17 100644
--- a/tests/gold_tests/cripts/cripts.test.py
+++ b/tests/gold_tests/cripts/cripts.test.py
@@ -40,12 +40,24 @@ class CriptsBasicTest:
request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
response_header = {
- "headers": "HTTP/1.1 200 OK\r\responseHeader: unchanged\r\n\r\n",
+ "headers": "HTTP/1.1 200 OK\r\nresponseHeader: unchanged\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"timestamp": "1469733493.993",
"body": ""
}
self.server.addResponse("sessionfile.log", request_header, response_header)
+ query_request_header = {
+ "headers": "GET /path/to/resource?foo=1&bar=2&baz=3 HTTP/1.1\r\nHost: does.not.matter\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+ }
+ query_response_header = {
+ "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+ }
+ self.server.addResponse("sessionfile.log", query_request_header, query_response_header)
+
def setUpTS(self):
self.ts = Test.MakeATSProcess("ts_in", enable_tls=True, enable_cache=False, enable_cripts=True)
@@ -53,6 +65,7 @@ class CriptsBasicTest:
self.ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key")
self.ts.Setup.Copy('files/basic.cript', self.ts.Variables.CONFIGDIR)
+ self.ts.Setup.Copy('files/query_copy.cript', self.ts.Variables.CONFIGDIR)
self.ts.Disk.records_config.update(
{
@@ -66,6 +79,8 @@ class CriptsBasicTest:
self.ts.Disk.remap_config.AddLine(
f'map https://www.example.com:{self.ts.Variables.ssl_port} http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript'
)
+ self.ts.Disk.remap_config.AddLine(
+ f'map http://query.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=query_copy.cript')
def runHeaderTest(self):
tr = Test.AddTestRun('Exercise traffic through cripts.')
@@ -85,8 +100,35 @@ class CriptsBasicTest:
tr.Processes.Default.Streams.stderr = "gold/certs_cript.gold"
tr.StillRunningAfter = self.server
+ def runQueryCopyTest(self):
+ tr = Test.AddTestRun('Exercise Query/Path copy ctor / copy-assign in cripts.')
+ tr.MakeCurlCommand(
+ f'-v -H "Host: query.example.com" "http://127.0.0.1:{self.ts.Variables.port}/path/to/resource?foo=1&bar=2&baz=3"',
+ ts=self.ts)
+ tr.Processes.Default.ReturnCode = 0
+ # Live URL is untouched: original query is preserved.
+ tr.Processes.Default.Streams.stderr = Testers.ContainsExpression(
+ r"X-Original-Query: foo=1&bar=2&baz=3", "Original query must round-trip unchanged")
+ # Mutating the copy (Erase "foo") must not have leaked into the original.
+ tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+ r"X-Copy-Query: bar=2&baz=3", "Erase on the copy must drop foo without affecting the original")
+ # Copy-assignment path produces a snapshot equal to the original.
+ tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+ r"X-Assigned-Query: foo=1&bar=2&baz=3", "Copy-assigned query must equal the original")
+ # Live URL path is untouched: original path is preserved.
+ tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+ r"X-Original-Path: path/to/resource", "Original path must round-trip unchanged")
+ # Mutating the copy (segment[0] = "edited") must not have leaked into the original.
+ tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+ r"X-Copy-Path: edited/to/resource", "Segment edit on the copy must not affect the original")
+ # Copy-assignment path produces a snapshot equal to the original.
+ tr.Processes.Default.Streams.stderr += Testers.ContainsExpression(
+ r"X-Assigned-Path: path/to/resource", "Copy-assigned path must equal the original")
+ tr.StillRunningAfter = self.server
+
def run(self):
self.runHeaderTest()
+ self.runQueryCopyTest()
if not Condition.CurlUsingUnixDomainSocket():
self.runCertsTest()
diff --git a/tests/gold_tests/cripts/files/query_copy.cript b/tests/gold_tests/cripts/files/query_copy.cript
new file mode 100644
index 0000000000..3121a6c4fd
--- /dev/null
+++ b/tests/gold_tests/cripts/files/query_copy.cript
@@ -0,0 +1,64 @@
+/*
+ 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.
+*/
+
+#include <cripts/Preamble.hpp>
+
+do_remap()
+{
+ borrow url = cripts::Client::URL::Get();
+
+ // Force the live Query / Path to parse so _state is populated; otherwise the
+ // copies below take the nullptr branch and the deep-copy logic isn't exercised.
+ url.query["foo"];
+ url.path[0];
+
+ // Copy-construct to exercise the explicit copy ctor (each owns a std::unique_ptr<State>).
+ // Note: this remains tied to the same underlying URL; it is not an independent snapshot.
+ cripts::Url::Query query_copy = url.query;
+ cripts::Url::Query query_assigned;
+ cripts::Url::Path path_copy = url.path;
+ cripts::Url::Path path_assigned;
+
+ query_assigned = url.query;
+ path_assigned = url.path;
+
+ query_copy.Erase("foo");
+ path_copy[0] = "edited";
+
+ // Materialize into owned strings so do_send_response sees stable values.
+ txn_data[0] = cripts::string(url.query.GetSV());
+ txn_data[1] = cripts::string(query_copy.GetSV());
+ txn_data[2] = cripts::string(query_assigned.GetSV());
+ txn_data[3] = cripts::string(url.path.GetSV());
+ txn_data[4] = cripts::string(path_copy.GetSV());
+ txn_data[5] = cripts::string(path_assigned.GetSV());
+}
+
+do_send_response()
+{
+ borrow resp = cripts::Client::Response::Get();
+
+ resp["X-Original-Query"] = AsString(txn_data[0]);
+ resp["X-Copy-Query"] = AsString(txn_data[1]);
+ resp["X-Assigned-Query"] = AsString(txn_data[2]);
+ resp["X-Original-Path"] = AsString(txn_data[3]);
+ resp["X-Copy-Path"] = AsString(txn_data[4]);
+ resp["X-Assigned-Path"] = AsString(txn_data[5]);
+}
+
+#include <cripts/Epilogue.hpp>
diff --git a/tests/gold_tests/cripts/gold/basic_cript.gold b/tests/gold_tests/cripts/gold/basic_cript.gold
index f3a6ea87f2..f06580416c 100644
--- a/tests/gold_tests/cripts/gold/basic_cript.gold
+++ b/tests/gold_tests/cripts/gold/basic_cript.gold
@@ -7,6 +7,7 @@
``
< HTTP/1.1 200 OK
< responseHeader: changed
+< Content-Length: 0
< Date: ``
< Age: ``
< Connection: keep-alive
diff --git a/tests/gold_tests/cripts/gold/certs_cript.gold b/tests/gold_tests/cripts/gold/certs_cript.gold
index bfc31a9e71..7bb8621565 100644
--- a/tests/gold_tests/cripts/gold/certs_cript.gold
+++ b/tests/gold_tests/cripts/gold/certs_cript.gold
@@ -6,6 +6,7 @@
``
< HTTP/1.1 200 OK
< responseHeader: changed
+< Content-Length: 0
< Date: ``
< Age: ``
< Connection: keep-alive
(trafficserver) 14/33: net: count application bytes in read_bytes for TLS (#13282)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 39bcc86588dba5898dbb85005b402a387543e5a1
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 16:01:50 2026 -0500
net: count application bytes in read_bytes for TLS (#13282)
On TLS connections, net.read_bytes was only counting some bytes for the
handshake, and none of the incoming ciphertext. This is neither intuitive
nor consistent with net.write_bytes.
The fix for now is to make it symmetric with net.write_bytes. Count the
plaintext bytes for TLS. This means not counting the handshake bytes.
For a long term fix, I plan to add more comprehensive metrics for TLS.
(cherry picked from commit 8e36c2810315953af8fca88f5f666ebfb4c5c247)
---
.../monitoring/statistics/core/network-io.en.rst | 19 +++++++++++++++++++
src/iocore/net/SSLNetVConnection.cc | 5 +++--
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/doc/admin-guide/monitoring/statistics/core/network-io.en.rst b/doc/admin-guide/monitoring/statistics/core/network-io.en.rst
index 56168ab959..f26d51457c 100644
--- a/doc/admin-guide/monitoring/statistics/core/network-io.en.rst
+++ b/doc/admin-guide/monitoring/statistics/core/network-io.en.rst
@@ -79,10 +79,29 @@ Network I/O
:type: counter
:units: bytes
+ Application-layer bytes read from client and origin connections. For TLS
+ connections this is the decrypted payload, symmetric with ``write_bytes``; it
+ does not include TLS handshake or record-layer framing.
+
+.. ts:stat:: global proxy.process.net.read_bytes_count integer
+ :type: counter
+
+ The number of read operations that contributed to ``read_bytes``. For TLS
+ connections this is one per decrypted-read pass, not per socket read.
+
.. ts:stat:: global proxy.process.net.write_bytes integer
:type: counter
:units: bytes
+ Application-layer bytes written to client and origin connections. For TLS
+ connections this is the plaintext payload; it does not include TLS handshake
+ or record-layer framing.
+
+.. ts:stat:: global proxy.process.net.write_bytes_count integer
+ :type: counter
+
+ The number of write operations that contributed to ``write_bytes``.
+
.. ts:stat:: global proxy.process.tcp.total_accepts integer
:type: counter
diff --git a/src/iocore/net/SSLNetVConnection.cc b/src/iocore/net/SSLNetVConnection.cc
index 35404629a6..f92d29528f 100644
--- a/src/iocore/net/SSLNetVConnection.cc
+++ b/src/iocore/net/SSLNetVConnection.cc
@@ -287,6 +287,9 @@ SSLNetVConnection::_ssl_read_from_net(int64_t &ret)
Dbg(dbg_ctl_ssl, "bytes_read=%" PRId64, bytes_read);
s->vio.ndone += bytes_read;
+ // Decrypted application bytes, to match write_bytes (also plaintext for TLS).
+ Metrics::Counter::increment(net_rsb.read_bytes, bytes_read);
+ Metrics::Counter::increment(net_rsb.read_bytes_count);
this->netActivity();
ret = bytes_read;
@@ -351,8 +354,6 @@ SSLNetVConnection::read_raw_data()
r = total_read - rattempted + r;
}
}
- Metrics::Counter::increment(net_rsb.read_bytes, r);
- Metrics::Counter::increment(net_rsb.read_bytes_count);
if (!this->haveCheckedProxyProtocol) {
// The PROXY Protocol, by spec, is designed to require only the first TCP packet of bytes
(trafficserver) 12/33: Enable probes in Fedora C++20 CI (#13280)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit c958a7dd30f5d92dfeba699985a90a2157ccb608
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Jun 16 09:41:09 2026 -0500
Enable probes in Fedora C++20 CI (#13280)
Fedora CI runs the C++20 preset rather than the base Fedora preset, so
probe coverage drifted out of that build even though ci-fedora had the
option enabled.
This makes the C++20 preset inherit the Fedora preset and keeps only the
C++ standard override locally, so shared Fedora CI settings stay in one
place.
(cherry picked from commit 08470e9a6baac67d4f85620a221becc7bbe9a312)
---
CMakePresets.json | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/CMakePresets.json b/CMakePresets.json
index 3fe7e010b8..6e0735c87f 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -194,13 +194,9 @@
"name": "ci-fedora-cxx20",
"displayName": "CI Fedora c++20",
"description": "CI Pipeline config for Fedora Linux compiled with c++20",
- "inherits": ["ci"],
+ "inherits": ["ci-fedora"],
"cacheVariables": {
- "opentelemetry_ROOT": "/opt",
- "CURL_ROOT": "/opt",
- "wamr_ROOT": "/opt",
- "CMAKE_CXX_STANDARD": "20",
- "ENABLE_CRIPTS": "ON"
+ "CMAKE_CXX_STANDARD": "20"
}
},
{
@@ -450,4 +446,3 @@
}
]
}
-
(trafficserver) 11/33: Fix bounds check in CacheVC::scanObject (#13263)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 641d15d07a8a041b0bff08abc3d08208fe4b7979
Author: Brian Olsen <br...@comcast.com>
AuthorDate: Tue Jun 16 06:25:34 2026 -0600
Fix bounds check in CacheVC::scanObject (#13263)
(cherry picked from commit dd38491fd5ae9aeaa1dee414bcc7359efcc2247a)
---
src/iocore/cache/CacheVC.cc | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/iocore/cache/CacheVC.cc b/src/iocore/cache/CacheVC.cc
index 54a7abc739..8dc154a887 100644
--- a/src/iocore/cache/CacheVC.cc
+++ b/src/iocore/cache/CacheVC.cc
@@ -769,9 +769,17 @@ CacheVC::scanObject(int /* event ATS_UNUSED */, Event * /* e ATS_UNUSED */)
}
break;
}
- if (doc->data() - buf->data() > static_cast<int>(io.aiocb.aio_nbytes)) {
- might_need_overlap_read = true;
- goto Lskip;
+ {
+ size_t const doc_off = reinterpret_cast<char *>(doc) - buf->data();
+ // Bounds-check in unsigned domain: doc must lie within the
+ // buffer, with room for the Doc header, and doc->hlen must
+ // fit in the remaining bytes before doc->hdr() and
+ // HTTPInfo::unmarshal walk it.
+ if (io.aiocb.aio_nbytes < doc_off || (io.aiocb.aio_nbytes - doc_off) < sizeof(Doc) ||
+ (io.aiocb.aio_nbytes - doc_off - sizeof(Doc)) < doc->hlen) {
+ might_need_overlap_read = true;
+ goto Lskip;
+ }
}
{
char *tmp = doc->hdr();
(trafficserver) 05/33: Add S3-FIFO RAM cache eviction algorithm (ram_cache.algorithm = 2) (#13255)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 4cde4ed3beda47b4edbf9540185958c0df5f4b25
Author: Phong Nguyen <ph...@gmail.com>
AuthorDate: Tue Jun 23 15:36:48 2026 -0700
Add S3-FIFO RAM cache eviction algorithm (ram_cache.algorithm = 2) (#13255)
* Add S3-FIFO RAM cache eviction algorithm (ram_cache.algorithm = 2)
S3-FIFO (Yang et al., SOSP 2023) is a FIFO-based eviction policy: a
small admission queue and a main queue (a 2-bit clock), plus a ghost
queue of recently evicted keys. The small queue and ghost filter
one-hit-wonders, giving scan resistance and strong hit rates on CDN
and key-value workloads at low cost -- a hit needs no list reordering.
Selectable as ram_cache.algorithm = 2 alongside CLFUS (0) and LRU (1).
The policy is byte-budgeted; its eviction metadata (the ghost included,
bounded by object size and an entry-count cap) is accounted within
ram_cache.size so total memory stays within the configured budget. Like
LRU and CLFUS it enforces one resident copy per key (a put with a new
aux key discards the stale one) and allocates entries from a per-thread
ProxyAllocator (Thread.h).
Co-Authored-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
* Fix ghost capacity calculation truncation order
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
* Make S3-FIFO RAM cache tunables configurable via records.yaml
The S3-FIFO queue split, ghost bounds, and promotion threshold were
compile-time constants. Expose them as
proxy.config.cache.ram_cache.s3fifo.{main_percent,ghost_size_percent,
ghost_mem_percent,promote_threshold} so operators can tune the policy
without a rebuild; the defaults match the paper and the prior behavior.
Each setting carries a RECC_INT range in RecordsConfig.cc, so an
out-of-range value is rejected at config load with a warning and the
documented default is used in its place -- the same guard every other
RAM cache record relies on, rather than a bespoke clamp. The admin
guide and the ram_cache regression test (a non-default "tuned" pass)
are updated to cover the new settings.
Co-Authored-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
* Add entry for S3-FIFO into NOTICE
---------
Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
(cherry picked from commit edc35b31ca3888483f0475d4139c8a6917ebd9fa)
---
NOTICE | 9 +
doc/admin-guide/files/records.yaml.en.rst | 63 ++++-
doc/admin-guide/storage/index.en.rst | 14 +-
include/iocore/cache/Cache.h | 5 +-
include/iocore/eventsystem/Thread.h | 1 +
src/iocore/cache/CMakeLists.txt | 1 +
src/iocore/cache/Cache.cc | 70 +++--
src/iocore/cache/CacheProcessor.cc | 7 +
src/iocore/cache/CacheTest.cc | 28 +-
src/iocore/cache/P_CacheInternal.h | 4 +
src/iocore/cache/P_RamCache.h | 1 +
src/iocore/cache/RamCacheS3FIFO.cc | 437 ++++++++++++++++++++++++++++++
src/records/RecordsConfig.cc | 11 +-
13 files changed, 609 insertions(+), 42 deletions(-)
diff --git a/NOTICE b/NOTICE
index 31787fecbf..ad0c5f11b5 100644
--- a/NOTICE
+++ b/NOTICE
@@ -118,3 +118,12 @@ LS-HPACK provides functionality to encode and decode HTTP headers using
HPACK compression mechanism specified in RFC 7541.
Copyright (c) 2018 - 2023 LiteSpeed Technologies Inc, (MIT License)
https://github.com/litespeedtech/ls-hpack.git
+
+~~
+
+S3-FIFO: A simple, scalable FIFO-based algorithm with three static queues
+Copyright 2023, Carnegie Mellon University (Apache-2.0)
+
+Website: https://s3fifo.com/
+Paper: http://dx.doi.org/10.1145/3600006.3613147
+Reference implementation: https://github.com/Thesys-lab/sosp23-s3fifo
diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst
index 2e70f637b4..777f2b29a7 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -2888,10 +2888,65 @@ RAM Cache
.. ts:cv:: CONFIG proxy.config.cache.ram_cache.algorithm INT 1
- Two distinct RAM caches are supported, the default (1) being the simpler
- **LRU** (*Least Recently Used*) cache. As an alternative, the **CLFUS**
- (*Clocked Least Frequently Used by Size*) is also available, by changing this
- configuration to 0.
+ Three RAM cache eviction algorithms are supported, selected by this value:
+
+ ``1``
+ **LRU** (*Least Recently Used*), the default -- the simplest policy,
+ favoring recency. Pairs with
+ :ts:cv:`proxy.config.cache.ram_cache.use_seen_filter` for scan
+ resistance.
+
+ ``0``
+ **CLFUS** (*Clocked Least Frequently Used by Size*), which balances
+ recency, frequency, and object size. It is the only algorithm that
+ supports in-RAM compression
+ (:ts:cv:`proxy.config.cache.ram_cache.compress`).
+
+ ``2``
+ **S3-FIFO** (*Simple Scalable Static FIFO*): a small admission queue and
+ a main queue (both FIFO), plus a ghost queue of recently evicted keys,
+ which together filter one-hit-wonders. Scan-resistant and inexpensive
+ (no per-hit reordering); strong hit rates on CDN and key-value
+ workloads. Its eviction metadata (including the ghost) is accounted
+ within :ts:cv:`proxy.config.cache.ram_cache.size`. Experimental; it does
+ not use the seen filter or support in-RAM compression. Its queue split,
+ ghost bounds, and promotion threshold can be tuned with the
+ ``proxy.config.cache.ram_cache.s3fifo.*`` settings below; the defaults
+ follow the original paper and suit most workloads.
+
+.. ts:cv:: CONFIG proxy.config.cache.ram_cache.s3fifo.main_percent INT 90
+
+ Only applies when :ts:cv:`proxy.config.cache.ram_cache.algorithm` is ``2``
+ (S3-FIFO). The target size of the main queue as a percentage of the resident
+ budget; the remainder is the small admission queue. The default ``90`` gives
+ the ~10% small / ~90% main split from the paper. Valid range is ``1`` to
+ ``99`` (an out-of-range value is rejected with a warning and the default is
+ used); a larger value grows the main queue at the expense of the admission
+ queue.
+
+.. ts:cv:: CONFIG proxy.config.cache.ram_cache.s3fifo.ghost_size_percent INT 90
+
+ Only applies when :ts:cv:`proxy.config.cache.ram_cache.algorithm` is ``2``
+ (S3-FIFO). The ghost queue remembers the keys of recently evicted objects for
+ up to this percentage of :ts:cv:`proxy.config.cache.ram_cache.size` worth of
+ object bytes. Valid range is ``0`` to ``100``; ``0`` disables this bound.
+
+.. ts:cv:: CONFIG proxy.config.cache.ram_cache.s3fifo.ghost_mem_percent INT 25
+
+ Only applies when :ts:cv:`proxy.config.cache.ram_cache.algorithm` is ``2``
+ (S3-FIFO). Caps the memory the ghost queue's per-key metadata may consume at
+ this percentage of :ts:cv:`proxy.config.cache.ram_cache.size`. This metadata
+ is counted against the configured RAM cache size, so total memory stays
+ within the budget regardless of object cardinality. Valid range is ``0`` to
+ ``100``; ``0`` disables the ghost queue.
+
+.. ts:cv:: CONFIG proxy.config.cache.ram_cache.s3fifo.promote_threshold INT 2
+
+ Only applies when :ts:cv:`proxy.config.cache.ram_cache.algorithm` is ``2``
+ (S3-FIFO). The number of times an object in the small admission queue must be
+ reused before it is promoted to the main queue instead of being demoted to
+ the ghost. The default ``2`` admits objects seen at least twice. Valid range
+ is ``1`` to ``3``; a higher value makes admission to the main queue stricter.
.. ts:cv:: CONFIG proxy.config.cache.ram_cache.use_seen_filter INT 1
diff --git a/doc/admin-guide/storage/index.en.rst b/doc/admin-guide/storage/index.en.rst
index d6f650e89c..22788eeaa4 100644
--- a/doc/admin-guide/storage/index.en.rst
+++ b/doc/admin-guide/storage/index.en.rst
@@ -75,18 +75,22 @@ and reduces load on disks, especially during temporary traffic peaks.
You can configure the RAM cache size to suit your needs, as described in
:ref:`changing-the-size-of-the-ram-cache` below.
-The RAM cache supports two cache eviction algorithms, a regular *LRU*
-(Least Recently Used) and the more advanced *CLFUS* (Clocked Least
+The RAM cache supports three cache eviction algorithms: a regular *LRU*
+(Least Recently Used); the more advanced *CLFUS* (Clocked Least
Frequently Used by Size; which balances recentness, frequency, and size
-to maximize hit rate, similar to a most frequently used algorithm).
-The default is to use *LRU*, and this is controlled via
+to maximize hit rate, similar to a most frequently used algorithm); and
+*S3-FIFO* (Simple Scalable Static FIFO), a FIFO-based policy whose small
+admission queue and ghost list filter one-hit-wonders, giving strong hit
+rates on CDN and key-value workloads at low cost. The default is to use
+*LRU*, and this is controlled via
:ts:cv:`proxy.config.cache.ram_cache.algorithm`.
Both the *LRU* and *CLFUS* RAM caches support a configuration to increase
scan resistance. In a typical *LRU*, if you request all possible objects in
sequence, you will effectively churn the cache on every request. The option
:ts:cv:`proxy.config.cache.ram_cache.use_seen_filter` can be set to add some
-resistance against this problem.
+resistance against this problem. *S3-FIFO* is scan-resistant by design,
+through its admission queue, and does not use the seen filter.
In addition, *CLFUS* also supports compressing in the RAM cache itself.
This can be useful for content which is not compressed by itself (e.g.
diff --git a/include/iocore/cache/Cache.h b/include/iocore/cache/Cache.h
index 3a7da523b5..9a0fe5d430 100644
--- a/include/iocore/cache/Cache.h
+++ b/include/iocore/cache/Cache.h
@@ -36,8 +36,9 @@ static constexpr ts::ModuleVersion CACHE_MODULE_VERSION(1, 0);
#define SCAN_KB_PER_SECOND 8192 // 1TB/8MB = 131072 = 36 HOURS to scan a TB
-#define RAM_CACHE_ALGORITHM_CLFUS 0
-#define RAM_CACHE_ALGORITHM_LRU 1
+#define RAM_CACHE_ALGORITHM_CLFUS 0
+#define RAM_CACHE_ALGORITHM_LRU 1
+#define RAM_CACHE_ALGORITHM_S3FIFO 2
#define CACHE_COMPRESSION_NONE 0
#define CACHE_COMPRESSION_FASTLZ 1
diff --git a/include/iocore/eventsystem/Thread.h b/include/iocore/eventsystem/Thread.h
index 0a34dd633c..435762e71e 100644
--- a/include/iocore/eventsystem/Thread.h
+++ b/include/iocore/eventsystem/Thread.h
@@ -134,6 +134,7 @@ public:
ProxyAllocator openDirEntryAllocator;
ProxyAllocator ramCacheCLFUSEntryAllocator;
ProxyAllocator ramCacheLRUEntryAllocator;
+ ProxyAllocator ramCacheS3FIFOEntryAllocator;
ProxyAllocator evacuationBlockAllocator;
ProxyAllocator ioDataAllocator;
ProxyAllocator ioAllocator;
diff --git a/src/iocore/cache/CMakeLists.txt b/src/iocore/cache/CMakeLists.txt
index 2775cef4e3..278ec839f8 100644
--- a/src/iocore/cache/CMakeLists.txt
+++ b/src/iocore/cache/CMakeLists.txt
@@ -33,6 +33,7 @@ add_library(
PreservationTable.cc
RamCacheCLFUS.cc
RamCacheLRU.cc
+ RamCacheS3FIFO.cc
Store.cc
Stripe.cc
StripeSM.cc
diff --git a/src/iocore/cache/Cache.cc b/src/iocore/cache/Cache.cc
index 14855e98fa..e1c59e9c24 100644
--- a/src/iocore/cache/Cache.cc
+++ b/src/iocore/cache/Cache.cc
@@ -54,35 +54,39 @@ constexpr ts::VersionNumber CACHE_DB_VERSION(CACHE_DB_MAJOR_VERSION, CACHE_DB_MI
// Configuration
-int64_t cache_config_ram_cache_size = AUTO_SIZE_RAM_CACHE;
-int cache_config_ram_cache_algorithm = 1;
-int cache_config_ram_cache_compress = 0;
-int cache_config_ram_cache_compress_percent = 90;
-int cache_config_ram_cache_use_seen_filter = 1;
-int cache_config_http_max_alts = 3;
-int cache_config_log_alternate_eviction = 0;
-int cache_config_dir_sync_frequency = 60;
-int cache_config_dir_sync_delay = 500;
-int cache_config_dir_sync_max_write = (2 * 1024 * 1024);
-int cache_config_dir_sync_parallel_tasks = 1;
-int cache_config_permit_pinning = 0;
-int cache_config_select_alternate = 1;
-int cache_config_max_doc_size = 0;
-int cache_config_min_average_object_size = ESTIMATED_OBJECT_SIZE;
-int64_t cache_config_ram_cache_cutoff = AGG_SIZE;
-int cache_config_max_disk_errors = 5;
-int cache_config_hit_evacuate_percent = 10;
-int cache_config_hit_evacuate_size_limit = 0;
-int cache_config_force_sector_size = 0;
-int cache_config_target_fragment_size = DEFAULT_TARGET_FRAGMENT_SIZE;
-int cache_config_agg_write_backlog = AGG_SIZE * 2;
-int cache_config_enable_checksum = 0;
-int cache_config_alt_rewrite_max_size = 4096;
-int cache_config_read_while_writer = 0;
-int cache_config_mutex_retry_delay = 2;
-int cache_read_while_writer_retry_delay = 50;
-int cache_config_read_while_writer_max_retries = 10;
-int cache_config_persist_bad_disks = false;
+int64_t cache_config_ram_cache_size = AUTO_SIZE_RAM_CACHE;
+int cache_config_ram_cache_algorithm = 1;
+int cache_config_ram_cache_compress = 0;
+int cache_config_ram_cache_compress_percent = 90;
+int cache_config_ram_cache_use_seen_filter = 1;
+int cache_config_ram_cache_s3fifo_main_percent = 90;
+int cache_config_ram_cache_s3fifo_ghost_size_percent = 90;
+int cache_config_ram_cache_s3fifo_ghost_mem_percent = 25;
+int cache_config_ram_cache_s3fifo_promote_threshold = 2;
+int cache_config_http_max_alts = 3;
+int cache_config_log_alternate_eviction = 0;
+int cache_config_dir_sync_frequency = 60;
+int cache_config_dir_sync_delay = 500;
+int cache_config_dir_sync_max_write = (2 * 1024 * 1024);
+int cache_config_dir_sync_parallel_tasks = 1;
+int cache_config_permit_pinning = 0;
+int cache_config_select_alternate = 1;
+int cache_config_max_doc_size = 0;
+int cache_config_min_average_object_size = ESTIMATED_OBJECT_SIZE;
+int64_t cache_config_ram_cache_cutoff = AGG_SIZE;
+int cache_config_max_disk_errors = 5;
+int cache_config_hit_evacuate_percent = 10;
+int cache_config_hit_evacuate_size_limit = 0;
+int cache_config_force_sector_size = 0;
+int cache_config_target_fragment_size = DEFAULT_TARGET_FRAGMENT_SIZE;
+int cache_config_agg_write_backlog = AGG_SIZE * 2;
+int cache_config_enable_checksum = 0;
+int cache_config_alt_rewrite_max_size = 4096;
+int cache_config_read_while_writer = 0;
+int cache_config_mutex_retry_delay = 2;
+int cache_read_while_writer_retry_delay = 50;
+int cache_config_read_while_writer_max_retries = 10;
+int cache_config_persist_bad_disks = false;
// Globals
@@ -845,6 +849,14 @@ ink_cache_init(ts::ModuleVersion v)
RecEstablishStaticConfigInt32(cache_config_ram_cache_compress_percent, "proxy.config.cache.ram_cache.compress_percent");
cache_config_ram_cache_use_seen_filter = RecGetRecordInt("proxy.config.cache.ram_cache.use_seen_filter").value_or(0);
+ RecEstablishStaticConfigInt32(cache_config_ram_cache_s3fifo_main_percent, "proxy.config.cache.ram_cache.s3fifo.main_percent");
+ RecEstablishStaticConfigInt32(cache_config_ram_cache_s3fifo_ghost_size_percent,
+ "proxy.config.cache.ram_cache.s3fifo.ghost_size_percent");
+ RecEstablishStaticConfigInt32(cache_config_ram_cache_s3fifo_ghost_mem_percent,
+ "proxy.config.cache.ram_cache.s3fifo.ghost_mem_percent");
+ RecEstablishStaticConfigInt32(cache_config_ram_cache_s3fifo_promote_threshold,
+ "proxy.config.cache.ram_cache.s3fifo.promote_threshold");
+
RecEstablishStaticConfigInt32(cache_config_http_max_alts, "proxy.config.cache.limits.http.max_alts");
Dbg(dbg_ctl_cache_init, "proxy.config.cache.limits.http.max_alts = %d", cache_config_http_max_alts);
diff --git a/src/iocore/cache/CacheProcessor.cc b/src/iocore/cache/CacheProcessor.cc
index fe5b9cb77e..8d75053e59 100644
--- a/src/iocore/cache/CacheProcessor.cc
+++ b/src/iocore/cache/CacheProcessor.cc
@@ -1431,6 +1431,10 @@ CacheProcessor::cacheInitialized()
if (gnstripes) {
// new ram_caches, with algorithm from the config
+ static const char *const ram_cache_algorithm_name[] = {"CLFUS", "LRU", "S3-FIFO"};
+ int const ram_alg = cache_config_ram_cache_algorithm;
+ Dbg(dbg_ctl_cache_init, "ram_cache algorithm = %d (%s)", ram_alg,
+ (ram_alg >= 0 && ram_alg <= RAM_CACHE_ALGORITHM_S3FIFO) ? ram_cache_algorithm_name[ram_alg] : "unknown");
for (int i = 0; i < gnstripes; i++) {
switch (cache_config_ram_cache_algorithm) {
default:
@@ -1440,6 +1444,9 @@ CacheProcessor::cacheInitialized()
case RAM_CACHE_ALGORITHM_LRU:
gstripes[i]->ram_cache = new_RamCacheLRU();
break;
+ case RAM_CACHE_ALGORITHM_S3FIFO:
+ gstripes[i]->ram_cache = new_RamCacheS3FIFO();
+ break;
}
}
diff --git a/src/iocore/cache/CacheTest.cc b/src/iocore/cache/CacheTest.cc
index cd86df6051..3d39abe8cc 100644
--- a/src/iocore/cache/CacheTest.cc
+++ b/src/iocore/cache/CacheTest.cc
@@ -679,8 +679,34 @@ REGRESSION_TEST(ram_cache)(RegressionTest *t, int level, int *pstatus)
for (int s = 20; s <= 24; s += 4) {
int64_t cache_size = 1LL << s;
*pstatus = REGRESSION_TEST_PASSED;
- if (!test_RamCache(t, new_RamCacheLRU(), "LRU", cache_size) || !test_RamCache(t, new_RamCacheCLFUS(), "CLFUS", cache_size)) {
+ if (!test_RamCache(t, new_RamCacheLRU(), "LRU", cache_size) || !test_RamCache(t, new_RamCacheCLFUS(), "CLFUS", cache_size) ||
+ !test_RamCache(t, new_RamCacheS3FIFO(), "S3-FIFO", cache_size)) {
*pstatus = REGRESSION_TEST_FAILED;
}
}
+
+ // Exercise the S3-FIFO tunables with valid non-default values: the policy must still pass the
+ // hit-rate floor and the size invariant, proving the records -> init() config plumbing is wired
+ // and that a non-default queue split, ghost bound, and promotion threshold remain correct.
+ // (Out-of-range values are rejected by the records.yaml RECC_INT validation, not here.)
+ {
+ int const saved_main = cache_config_ram_cache_s3fifo_main_percent;
+ int const saved_gsize = cache_config_ram_cache_s3fifo_ghost_size_percent;
+ int const saved_gmem = cache_config_ram_cache_s3fifo_ghost_mem_percent;
+ int const saved_promote = cache_config_ram_cache_s3fifo_promote_threshold;
+ int64_t const cache_size = 1LL << 24;
+
+ cache_config_ram_cache_s3fifo_main_percent = 80; // valid, non-default
+ cache_config_ram_cache_s3fifo_ghost_size_percent = 50;
+ cache_config_ram_cache_s3fifo_ghost_mem_percent = 15;
+ cache_config_ram_cache_s3fifo_promote_threshold = 1;
+ if (!test_RamCache(t, new_RamCacheS3FIFO(), "S3-FIFO tuned", cache_size)) {
+ *pstatus = REGRESSION_TEST_FAILED;
+ }
+
+ cache_config_ram_cache_s3fifo_main_percent = saved_main;
+ cache_config_ram_cache_s3fifo_ghost_size_percent = saved_gsize;
+ cache_config_ram_cache_s3fifo_ghost_mem_percent = saved_gmem;
+ cache_config_ram_cache_s3fifo_promote_threshold = saved_promote;
+ }
}
diff --git a/src/iocore/cache/P_CacheInternal.h b/src/iocore/cache/P_CacheInternal.h
index 08e77291bb..c030fe211d 100644
--- a/src/iocore/cache/P_CacheInternal.h
+++ b/src/iocore/cache/P_CacheInternal.h
@@ -130,6 +130,10 @@ extern int cache_config_agg_write_backlog;
extern int cache_config_ram_cache_compress;
extern int cache_config_ram_cache_compress_percent;
extern int cache_config_ram_cache_use_seen_filter;
+extern int cache_config_ram_cache_s3fifo_main_percent;
+extern int cache_config_ram_cache_s3fifo_ghost_size_percent;
+extern int cache_config_ram_cache_s3fifo_ghost_mem_percent;
+extern int cache_config_ram_cache_s3fifo_promote_threshold;
extern int cache_config_hit_evacuate_percent;
extern int cache_config_hit_evacuate_size_limit;
extern int cache_config_force_sector_size;
diff --git a/src/iocore/cache/P_RamCache.h b/src/iocore/cache/P_RamCache.h
index b4833e6d96..1afe103751 100644
--- a/src/iocore/cache/P_RamCache.h
+++ b/src/iocore/cache/P_RamCache.h
@@ -45,3 +45,4 @@ public:
RamCache *new_RamCacheLRU();
RamCache *new_RamCacheCLFUS();
+RamCache *new_RamCacheS3FIFO();
diff --git a/src/iocore/cache/RamCacheS3FIFO.cc b/src/iocore/cache/RamCacheS3FIFO.cc
new file mode 100644
index 0000000000..7471165107
--- /dev/null
+++ b/src/iocore/cache/RamCacheS3FIFO.cc
@@ -0,0 +1,437 @@
+/** @file
+
+ A brief file description
+
+ @section license License
+
+ 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.
+ */
+
+// S3-FIFO RAM cache replacement policy.
+//
+// The design follows Yang, Zhang, Qiu, Yue & Rashmi, "FIFO queues are all you need for cache
+// eviction" (SOSP 2023) and https://s3fifo.com, mirroring the libCacheSim reference (S3FIFO.c).
+//
+// EXPERIMENTAL: three FIFO queues -- a small admission queue S (~10% of capacity), a main queue M
+// (~90%, a 2-bit CLOCK), and a ghost queue G that remembers the keys of recently evicted objects
+// (no data). New objects enter S; on eviction from S an object that was reused (frequency >= 2) is
+// promoted to M, otherwise it is demoted to G. A subsequent miss whose key is in G enters M
+// directly. The small queue + ghost give S3-FIFO admission control that filters one-hit-wonders --
+// exactly the CDN "quick demotion" property -- at FIFO cost (no per-hit reordering). The ghost
+// holds keys, so it costs per-entry metadata at high object cardinality; that metadata is bounded
+// and counted against ram_cache.size so total memory stays within the configured budget. Selectable
+// as proxy.config.cache.ram_cache.algorithm = 2; the queue split, ghost bounds, and promotion
+// threshold are tunable via proxy.config.cache.ram_cache.s3fifo.* (see init() and the admin guide,
+// records.yaml.en.rst).
+
+#include "P_RamCache.h"
+#include "P_CacheInternal.h"
+#include "StripeSM.h"
+#include "iocore/eventsystem/IOBuffer.h"
+#include "tscore/CryptoHash.h"
+#include "tscore/List.h"
+
+#define ENTRY_OVERHEAD 128 // per-entry metadata counted against ram_cache.size
+#define FREQ_MAX 3 // 2-bit saturating frequency counter; keep ram_cache.s3fifo.promote_threshold's range in sync
+
+namespace
+{
+DbgCtl dbg_ctl_ram_cache{"ram_cache"};
+} // namespace
+
+enum { SEG_SMALL = 0, SEG_MAIN = 1, SEG_GHOST = 2 };
+
+struct RamCacheS3FIFOEntry {
+ CryptoHash key;
+ uint64_t auxkey;
+ uint32_t size; // object bytes; resident entries account size + ENTRY_OVERHEAD against the budget
+ uint8_t seg; // SEG_SMALL / SEG_MAIN / SEG_GHOST
+ uint8_t freq; // 0..FREQ_MAX
+ LINK(RamCacheS3FIFOEntry, lru_link);
+ LINK(RamCacheS3FIFOEntry, hash_link);
+ Ptr<IOBufferData> data; // null for ghost entries
+};
+
+struct RamCacheS3FIFO : public RamCache {
+ int get(CryptoHash *key, Ptr<IOBufferData> *ret_data, uint64_t auxkey = 0) override;
+ int put(CryptoHash *key, IOBufferData *data, uint32_t len, bool copy = false, uint64_t auxkey = 0) override;
+ int fixup(const CryptoHash *key, uint64_t old_auxkey, uint64_t new_auxkey) override;
+ int64_t size() const override;
+ void init(int64_t max_bytes, StripeSM *stripe) override;
+
+private:
+ int _main_percent = 90; // main queue target size, percent of the resident budget (configurable)
+ int _promote_threshold = 2; // reuse count in the small queue that promotes an object to main (configurable)
+ int64_t _max_bytes = 0;
+ int64_t _ghost_size_limit = 0; // ghost object-size bound (keeps it proportional for big objects)
+ int64_t _ghost_max = 0; // ghost entry-count bound (caps metadata to ghost_mem_percent)
+ int64_t _s_bytes = 0; // resident bytes in the small queue (incl. ENTRY_OVERHEAD per entry)
+ int64_t _m_bytes = 0; // resident bytes in the main queue
+ int64_t _g_bytes = 0; // ghost object-size sum (vs _ghost_size_limit)
+ int64_t _g_count = 0; // ghost entries; each costs ~ENTRY_OVERHEAD, counted in the budget
+ int64_t _objects = 0; // resident objects (S + M)
+ int64_t _nentries = 0; // hash entries (resident + ghost), for sizing the hash table
+
+ Que(RamCacheS3FIFOEntry, lru_link) _seg[3]; // head = oldest, tail = newest, per segment
+ DList(RamCacheS3FIFOEntry, hash_link) *_bucket = nullptr;
+ int _nbuckets = 0;
+ int _ibuckets = 0;
+ StripeSM *_stripe = nullptr;
+
+ void _resize_hashtable();
+ RamCacheS3FIFOEntry *_lookup(const CryptoHash *key, uint64_t auxkey);
+ void _unlink_hash(RamCacheS3FIFOEntry *e);
+ void _remove(RamCacheS3FIFOEntry *e);
+ void _to_main(RamCacheS3FIFOEntry *e);
+ void _to_ghost(RamCacheS3FIFOEntry *e);
+ bool _evict_ghost_one();
+ void _enforce_ghost();
+ void _evict_small();
+ void _evict_main();
+ void _evict();
+};
+
+ClassAllocator<RamCacheS3FIFOEntry, false> ramCacheS3FIFOEntryAllocator("RamCacheS3FIFOEntry");
+
+static const int bucket_sizes[] = {8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143,
+ 4194301, 8388593, 16777213, 33554393, 67108859, 134217689, 268435399, 536870909, 1073741827};
+
+void
+RamCacheS3FIFO::_resize_hashtable()
+{
+ ink_release_assert(_ibuckets < static_cast<int>(sizeof(bucket_sizes) / sizeof(bucket_sizes[0])));
+ int anbuckets = bucket_sizes[_ibuckets];
+ int64_t s = anbuckets * sizeof(DList(RamCacheS3FIFOEntry, hash_link));
+
+ DList(RamCacheS3FIFOEntry, hash_link) *new_bucket = static_cast<DList(RamCacheS3FIFOEntry, hash_link) *>(ats_malloc(s));
+ memset(static_cast<void *>(new_bucket), 0, s);
+ if (_bucket) {
+ for (int64_t i = 0; i < _nbuckets; i++) {
+ RamCacheS3FIFOEntry *e = nullptr;
+ while ((e = _bucket[i].pop())) {
+ new_bucket[e->key.slice32(3) % anbuckets].push(e);
+ }
+ }
+ ats_free(_bucket);
+ }
+ _bucket = new_bucket;
+ _nbuckets = anbuckets;
+}
+
+void
+RamCacheS3FIFO::init(int64_t abytes, StripeSM *astripe)
+{
+ _stripe = astripe;
+ _max_bytes = abytes;
+ if (!_max_bytes) {
+ return;
+ }
+
+ // The tunables are range-validated in records.yaml (RECC_INT): an out-of-range value is rejected
+ // there with a warning and the documented default is used in its place, so the values read here
+ // are already within their safe ranges -- no clamping needed.
+ _main_percent = cache_config_ram_cache_s3fifo_main_percent;
+ _promote_threshold = cache_config_ram_cache_s3fifo_promote_threshold;
+ int ghost_size_percent = cache_config_ram_cache_s3fifo_ghost_size_percent;
+ int ghost_mem_percent = cache_config_ram_cache_s3fifo_ghost_mem_percent;
+
+ _ghost_size_limit = (_max_bytes * ghost_size_percent) / 100;
+ // The ghost stores keys only, but each key still costs ~ENTRY_OVERHEAD of real memory. Bound the
+ // ghost by both its object-size sum (keeps it proportional for large objects) and an entry count
+ // that caps its metadata at ghost_mem_percent of the configured size; the metadata is counted
+ // against the budget (see put) so total memory never exceeds ram_cache.size.
+ _ghost_max = ((_max_bytes * ghost_mem_percent) / 100) / ENTRY_OVERHEAD;
+
+ Dbg(dbg_ctl_ram_cache,
+ "S3-FIFO init %" PRId64 " bytes: main_percent=%d ghost_size_percent=%d ghost_mem_percent=%d promote_threshold=%d", _max_bytes,
+ _main_percent, ghost_size_percent, ghost_mem_percent, _promote_threshold);
+
+ _resize_hashtable();
+}
+
+int64_t
+RamCacheS3FIFO::size() const
+{
+ // Memory accounted against ram_cache.size: resident data plus per-entry overhead, plus the
+ // ghost's per-key overhead. This matches the budget enforced in put(), and is O(1).
+ return _s_bytes + _m_bytes + _g_count * ENTRY_OVERHEAD;
+}
+
+RamCacheS3FIFOEntry *
+RamCacheS3FIFO::_lookup(const CryptoHash *key, uint64_t auxkey)
+{
+ uint32_t i = key->slice32(3) % _nbuckets;
+ RamCacheS3FIFOEntry *e = _bucket[i].head;
+ while (e) {
+ if (e->key == *key && e->auxkey == auxkey) {
+ return e;
+ }
+ e = e->hash_link.next;
+ }
+ return nullptr;
+}
+
+void
+RamCacheS3FIFO::_unlink_hash(RamCacheS3FIFOEntry *e)
+{
+ uint32_t b = e->key.slice32(3) % _nbuckets;
+ _bucket[b].remove(e);
+}
+
+// Remove an entry from whichever segment holds it, update all accounting, and free it. The single
+// removal point used by eviction, ghost trimming, and stale-aux-key discard in put().
+void
+RamCacheS3FIFO::_remove(RamCacheS3FIFOEntry *e)
+{
+ _seg[e->seg].remove(e);
+ if (e->seg == SEG_GHOST) {
+ _g_bytes -= e->size;
+ _g_count--;
+ } else {
+ int64_t resident = ENTRY_OVERHEAD + e->size;
+ if (e->seg == SEG_SMALL) {
+ _s_bytes -= resident;
+ } else {
+ _m_bytes -= resident;
+ }
+ ts::Metrics::Gauge::decrement(cache_rsb.ram_cache_bytes, resident);
+ ts::Metrics::Gauge::decrement(_stripe->cache_vol->vol_rsb.ram_cache_bytes, resident);
+ _objects--;
+ }
+ _unlink_hash(e);
+ _nentries--;
+ e->data = nullptr;
+ THREAD_FREE(e, ramCacheS3FIFOEntryAllocator, this_thread());
+}
+
+// Promote a small-queue object that has proven popular into the main queue (resident, no data
+// change), resetting its frequency for the main-queue CLOCK.
+void
+RamCacheS3FIFO::_to_main(RamCacheS3FIFOEntry *e)
+{
+ _seg[SEG_SMALL].remove(e);
+ _s_bytes -= ENTRY_OVERHEAD + e->size;
+ e->seg = SEG_MAIN;
+ e->freq = 0;
+ _seg[SEG_MAIN].enqueue(e);
+ _m_bytes += ENTRY_OVERHEAD + e->size;
+}
+
+// Demote a small-queue object to the ghost queue: drop its data (freeing resident bytes) but keep
+// its key so a quick re-reference can be admitted straight to the main queue.
+void
+RamCacheS3FIFO::_to_ghost(RamCacheS3FIFOEntry *e)
+{
+ _seg[SEG_SMALL].remove(e);
+ _s_bytes -= ENTRY_OVERHEAD + e->size;
+ ts::Metrics::Gauge::decrement(cache_rsb.ram_cache_bytes, ENTRY_OVERHEAD + e->size);
+ ts::Metrics::Gauge::decrement(_stripe->cache_vol->vol_rsb.ram_cache_bytes, ENTRY_OVERHEAD + e->size);
+ e->data = nullptr;
+ e->seg = SEG_GHOST;
+ e->freq = 0;
+ _seg[SEG_GHOST].enqueue(e);
+ _g_bytes += e->size;
+ _g_count++;
+ _objects--;
+ _enforce_ghost();
+}
+
+// Drop the oldest ghost key; returns false if the ghost was already empty.
+bool
+RamCacheS3FIFO::_evict_ghost_one()
+{
+ RamCacheS3FIFOEntry *g = _seg[SEG_GHOST].head;
+ if (!g) {
+ return false;
+ }
+ _remove(g);
+ return true;
+}
+
+void
+RamCacheS3FIFO::_enforce_ghost()
+{
+ while ((_g_bytes > _ghost_size_limit || _g_count > _ghost_max) && _evict_ghost_one()) {}
+}
+
+// Evict from the small queue: promote reused objects to main, demote the first un-reused object to
+// the ghost (which is what actually frees resident bytes). If everything got promoted the small
+// queue empties and the caller falls through to evicting main.
+void
+RamCacheS3FIFO::_evict_small()
+{
+ while (_seg[SEG_SMALL].head) {
+ RamCacheS3FIFOEntry *c = _seg[SEG_SMALL].head;
+ if (c->freq >= _promote_threshold) {
+ _to_main(c);
+ } else {
+ _to_ghost(c);
+ return;
+ }
+ }
+}
+
+// Evict from the main queue (2-bit CLOCK): a reused object is reinserted at the tail with its
+// frequency decremented; the first object with frequency 0 is evicted.
+void
+RamCacheS3FIFO::_evict_main()
+{
+ while (_seg[SEG_MAIN].head) {
+ RamCacheS3FIFOEntry *c = _seg[SEG_MAIN].head;
+ if (c->freq >= 1) {
+ _seg[SEG_MAIN].remove(c);
+ c->freq -= 1; // 2-bit clock: a reused object gets another pass (freq is already <= FREQ_MAX)
+ _seg[SEG_MAIN].enqueue(c);
+ } else {
+ _remove(c);
+ return;
+ }
+ }
+}
+
+void
+RamCacheS3FIFO::_evict()
+{
+ // Main targets MAIN_PERCENT of the budget actually available to resident data -- the ghost's
+ // metadata is reserved out of the configured size, so basing the split on raw _max_bytes would
+ // (when the ghost is full) keep _m_bytes below the limit forever and starve the small queue.
+ int64_t resident_budget = _max_bytes - _g_count * ENTRY_OVERHEAD;
+ if (_m_bytes > resident_budget * _main_percent / 100 || _s_bytes == 0) {
+ _evict_main();
+ } else {
+ _evict_small();
+ }
+}
+
+int
+RamCacheS3FIFO::get(CryptoHash *key, Ptr<IOBufferData> *ret_data, uint64_t auxkey)
+{
+ if (!_max_bytes) {
+ return 0;
+ }
+ RamCacheS3FIFOEntry *e = _lookup(key, auxkey);
+ if (e && e->seg != SEG_GHOST) {
+ if (e->freq < FREQ_MAX) {
+ e->freq++;
+ }
+ (*ret_data) = e->data;
+ ts::Metrics::Counter::increment(cache_rsb.ram_cache_hits);
+ ts::Metrics::Counter::increment(_stripe->cache_vol->vol_rsb.ram_cache_hits);
+ return 1;
+ }
+ ts::Metrics::Counter::increment(cache_rsb.ram_cache_misses);
+ ts::Metrics::Counter::increment(_stripe->cache_vol->vol_rsb.ram_cache_misses);
+ return 0;
+}
+
+int
+RamCacheS3FIFO::put(CryptoHash *key, IOBufferData *data, [[maybe_unused]] uint32_t len, bool, uint64_t auxkey)
+{
+ if (!_max_bytes) {
+ return 0;
+ }
+ uint32_t size = data->block_size();
+ uint32_t i = key->slice32(3) % _nbuckets;
+
+ // Walk the hash chain. A resident hit just counts the reference; a ghost hit is admitted fresh to
+ // the main queue; an entry with the same key but a different aux key is stale and is discarded --
+ // the same one-resident-copy-per-key contract LRU and CLFUS enforce.
+ bool ghost_hit = false;
+ RamCacheS3FIFOEntry *e = _bucket[i].head;
+ while (e) {
+ if (e->key == *key) {
+ if (e->auxkey == auxkey) {
+ if (e->seg != SEG_GHOST) { // already resident: count the reference (matches LRU's bump)
+ if (e->freq < FREQ_MAX) {
+ e->freq++;
+ }
+ return 1;
+ }
+ RamCacheS3FIFOEntry *next = e->hash_link.next; // ghost hit: admit a fresh copy to main
+ _remove(e);
+ ghost_hit = true;
+ e = next;
+ continue;
+ }
+ RamCacheS3FIFOEntry *next = e->hash_link.next; // stale aux key: discard the old copy
+ _remove(e);
+ e = next;
+ continue;
+ }
+ e = e->hash_link.next;
+ }
+
+ // Keep total memory within the configured size: resident data+overhead plus the ghost's
+ // metadata (ENTRY_OVERHEAD per remembered key) must fit. Evict resident first; if that is
+ // exhausted but ghost metadata still pushes over, drop ghost keys too.
+ int64_t need = ENTRY_OVERHEAD + size;
+ while (_s_bytes + _m_bytes + _g_count * ENTRY_OVERHEAD + need > _max_bytes) {
+ if (_s_bytes + _m_bytes > 0) {
+ _evict();
+ } else if (!_evict_ghost_one()) {
+ break;
+ }
+ }
+ if (_s_bytes + _m_bytes + _g_count * ENTRY_OVERHEAD + need > _max_bytes) {
+ return 0; // object larger than the whole cache: do not admit, so the budget is never exceeded
+ }
+
+ RamCacheS3FIFOEntry *ne = THREAD_ALLOC(ramCacheS3FIFOEntryAllocator, this_ethread());
+ ne->key = *key;
+ ne->auxkey = auxkey;
+ ne->size = size;
+ ne->freq = 0;
+ ne->seg = ghost_hit ? SEG_MAIN : SEG_SMALL;
+ ne->data = data;
+ _bucket[i].push(ne);
+ _seg[ne->seg].enqueue(ne);
+ if (ghost_hit) {
+ _m_bytes += need;
+ } else {
+ _s_bytes += need;
+ }
+ _objects++;
+ _nentries++;
+ ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, need);
+ ts::Metrics::Gauge::increment(_stripe->cache_vol->vol_rsb.ram_cache_bytes, need);
+
+ if (_nentries > _nbuckets * 0.75) {
+ ++_ibuckets;
+ _resize_hashtable();
+ }
+ return 1;
+}
+
+int
+RamCacheS3FIFO::fixup(const CryptoHash *key, uint64_t old_auxkey, uint64_t new_auxkey)
+{
+ if (!_max_bytes) {
+ return 0;
+ }
+ RamCacheS3FIFOEntry *e = _lookup(key, old_auxkey);
+ if (e && e->seg != SEG_GHOST) {
+ e->auxkey = new_auxkey;
+ return 1;
+ }
+ return 0;
+}
+
+RamCache *
+new_RamCacheS3FIFO()
+{
+ return new RamCacheS3FIFO;
+}
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index bf80bb290d..e22948bd38 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -854,7 +854,16 @@ static constexpr RecordElement RecordsConfig[] =
// # alternatively: 20971520 (20MB)
{RECT_CONFIG, "proxy.config.cache.ram_cache.size", RECD_INT, "-1", RECU_RESTART_TS, RR_NULL, RECC_STR, "^-?[0-9]+[A-Za-z]{0,}$", RECA_NULL}
,
- {RECT_CONFIG, "proxy.config.cache.ram_cache.algorithm", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
+ {RECT_CONFIG, "proxy.config.cache.ram_cache.algorithm", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-2]", RECA_NULL}
+ ,
+ // # S3-FIFO (ram_cache.algorithm = 2) tunables
+ {RECT_CONFIG, "proxy.config.cache.ram_cache.s3fifo.main_percent", RECD_INT, "90", RECU_RESTART_TS, RR_NULL, RECC_INT, "[1-99]", RECA_NULL}
+ ,
+ {RECT_CONFIG, "proxy.config.cache.ram_cache.s3fifo.ghost_size_percent", RECD_INT, "90", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-100]", RECA_NULL}
+ ,
+ {RECT_CONFIG, "proxy.config.cache.ram_cache.s3fifo.ghost_mem_percent", RECD_INT, "25", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-100]", RECA_NULL}
+ ,
+ {RECT_CONFIG, "proxy.config.cache.ram_cache.s3fifo.promote_threshold", RECD_INT, "2", RECU_RESTART_TS, RR_NULL, RECC_INT, "[1-3]", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.cache.ram_cache.use_seen_filter", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-9]", RECA_NULL}
,
(trafficserver) 17/33: ProxyProtocol: free pp_info heap on NetVConnection recycle (#13293)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit e2fb34599335c0383b7ac18e793a5842453109c3
Author: Mo Chen <mo...@apache.org>
AuthorDate: Thu Jun 18 16:13:16 2026 -0500
ProxyProtocol: free pp_info heap on NetVConnection recycle (#13293)
NetVConnection embeds a ProxyProtocol by value, and a PROXY v2 header parsed
by has_proxy_protocol() heap-allocates ProxyProtocol::additional_data (the TLV
blob) plus the tlv map. The NetVConnection ClassAllocators are
Destruct_on_free=false, so ~ProxyProtocol never runs when a VC is recycled and
UnixNetVConnection::clear() did not release pp_info -- the next placement-new on
the recycled slot abandoned the buffer and map nodes, leaking once per recycled
connection that carried a PROXY v2 header. Behind a PROXY-protocol load
balancer that is every inbound connection.
Add ProxyProtocol::reset() and call it from UnixNetVConnection::clear() (the
single recycle chokepoint for the Unix, SSL and QUIC VCs). reset() swaps the
members with empty containers rather than clear()ing them, because clear()
retains capacity that the recycle would still abandon.
Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
(cherry picked from commit 95a13a552d02a5f4621a3c10127b7f16b4b47780)
---
include/iocore/net/ProxyProtocol.h | 19 ++++++++++
src/iocore/net/UnixNetVConnection.cc | 3 ++
src/iocore/net/unit_tests/test_ProxyProtocol.cc | 50 +++++++++++++++++++++++++
3 files changed, 72 insertions(+)
diff --git a/include/iocore/net/ProxyProtocol.h b/include/iocore/net/ProxyProtocol.h
index d3a7c73671..7d7fc34e46 100644
--- a/include/iocore/net/ProxyProtocol.h
+++ b/include/iocore/net/ProxyProtocol.h
@@ -82,6 +82,25 @@ public:
other.tlv.clear();
}
~ProxyProtocol() = default;
+
+ /** Release owned heap and reset to the default-constructed state.
+ *
+ * Swap rather than clear() the containers: clear() retains capacity, which would be abandoned
+ * (leaked) when the slot is reused, since the NetVConnection allocators are Destruct_on_free=false
+ * and so never run ~ProxyProtocol.
+ */
+ void
+ reset()
+ {
+ std::string{}.swap(additional_data);
+ std::unordered_map<uint8_t, std::string_view>{}.swap(tlv);
+ version = ProxyProtocolVersion::UNDEFINED;
+ ip_family = AF_UNSPEC;
+ type = 0;
+ src_addr = {};
+ dst_addr = {};
+ }
+
int set_additional_data(std::string_view data);
void set_ipv4_addrs(in_addr_t src_addr, uint16_t src_port, in_addr_t dst_addr, uint16_t dst_port);
void set_ipv6_addrs(const in6_addr &src_addr, uint16_t src_port, const in6_addr &dst_addr, uint16_t dst_port);
diff --git a/src/iocore/net/UnixNetVConnection.cc b/src/iocore/net/UnixNetVConnection.cc
index 29e1b4dead..8e18df53ed 100644
--- a/src/iocore/net/UnixNetVConnection.cc
+++ b/src/iocore/net/UnixNetVConnection.cc
@@ -1220,6 +1220,9 @@ UnixNetVConnection::clear()
read.vio.vc_server = nullptr;
write.vio.vc_server = nullptr;
options.reset();
+ // The VC allocator is Destruct_on_free=false (~ProxyProtocol never runs on recycle), so release
+ // pp_info's heap explicitly here.
+ pp_info.reset();
if (netvc_context == NET_VCONNECTION_OUT) {
read.vio.buffer.clear();
write.vio.buffer.clear();
diff --git a/src/iocore/net/unit_tests/test_ProxyProtocol.cc b/src/iocore/net/unit_tests/test_ProxyProtocol.cc
index ada8605f08..cc62e63e82 100644
--- a/src/iocore/net/unit_tests/test_ProxyProtocol.cc
+++ b/src/iocore/net/unit_tests/test_ProxyProtocol.cc
@@ -956,3 +956,53 @@ TEST_CASE("ProxyProtocol Rule of 5", "[ProxyProtocol]")
CHECK(orig_tlv.value() == "original");
}
}
+
+TEST_CASE("ProxyProtocol reset", "[ProxyProtocol]")
+{
+ // A PROXY v2 / TCP over IPv4 header carrying TLVs, so the parsed pp_info owns both the
+ // additional_data string and the tlv map that reset() must release.
+ uint8_t raw_data[] = {
+ 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, ///< preface
+ 0x55, 0x49, 0x54, 0x0A, ///<
+ 0x21, ///< version & command
+ 0x11, ///< protocol & family
+ 0x00, 0x32, ///< len
+ 0xC0, 0x00, 0x02, 0x01, ///< src_addr
+ 0xC6, 0x33, 0x64, 0x01, ///< dst_addr
+ 0xC3, 0x50, ///< src_port
+ 0x01, 0xBB, ///< dst_port
+ 0x01, 0x00, 0x02, 0x68, 0x32, /// PP2_TYPE_ALPN (h2)
+ 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, /// PP2_TYPE_AUTHORITY (abc)
+ 0x20, 0x00, 0x18, 0x01, 0x00, 0x00, 0x00, 0x00, /// PP2_TYPE_SSL (client=0x01, verify=0)
+ 0x23, 0x00, 0x03, 0x58, 0x59, 0x5A, /// PP2_SUBTYPE_SSL_CIPHER (XYZ)
+ 0x26, 0x00, 0x04, 0x58, 0x31, 0x32, 0x33, /// PP2_SUBTYPE_SSL_GROUP (X123)
+ 0x21, 0x00, 0x03, 0x54, 0x4C, 0x53, /// PP2_SUBTYPE_SSL_VERSION (TLS)
+ };
+
+ swoc::TextView tv(reinterpret_cast<char *>(raw_data), sizeof(raw_data));
+
+ ProxyProtocol pp_info;
+ REQUIRE(proxy_protocol_parse(&pp_info, tv) == tv.size());
+
+ // Precondition: pp_info is fully populated, including its heap-owning members.
+ REQUIRE(pp_info.version == ProxyProtocolVersion::V2);
+ REQUIRE(pp_info.ip_family == AF_INET);
+ REQUIRE(pp_info.type == SOCK_STREAM);
+ REQUIRE_FALSE(pp_info.tlv.empty());
+ REQUIRE(pp_info.get_tlv(PP2_TYPE_ALPN).has_value());
+
+ pp_info.reset();
+
+ // Every field is back to the default-constructed state and the backing storage is released.
+ CHECK(pp_info.version == ProxyProtocolVersion::UNDEFINED);
+ CHECK(pp_info.ip_family == AF_UNSPEC);
+ CHECK(pp_info.type == 0);
+ CHECK_FALSE(ats_is_ip(&pp_info.src_addr));
+ CHECK_FALSE(ats_is_ip(&pp_info.dst_addr));
+ CHECK(pp_info.tlv.empty());
+ CHECK_FALSE(pp_info.get_tlv(PP2_TYPE_ALPN).has_value());
+
+ // reset() is idempotent and safe to call on an already-empty object.
+ pp_info.reset();
+ CHECK(pp_info.tlv.empty());
+}
(trafficserver) 29/33: TLS: Fix memory leaks in cert load and OCSP stapling (#13318)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 83bb02235ca79c804b934de441634af4b61ad93d
Author: Mo Chen <mo...@apache.org>
AuthorDate: Wed Jun 24 23:13:31 2026 -0500
TLS: Fix memory leaks in cert load and OCSP stapling (#13318)
Several error and early-return paths in the TLS server certificate loading
and OCSP stapling code omit cleanup and leak native objects. Most are gated
on misconfiguration (bad or unreadable cert files) or allocation failure, so
they accumulate slowly rather than on every handshake.
1. setClientCertCACerts leaked the X509_STORE when X509_STORE_load_locations
failed; the store is only adopted by SSL_set0_verify_cert_store on success.
This fires per handshake on the sni.yaml verify_client path when the CA
file or directory is bad. It also now skips the load when X509_STORE_new
returns null, which would otherwise crash on allocation failure.
2. SSLNetVConnection::populate leaked the migrated SSL object when the base
populate failed: the source VC has already relinquished ownership, so the
SSL must be freed when this VC does not adopt it.
3. load_certs leaked the leaf certificate on the mid-chain failure returns
(bad intermediate chain, private key, or CA file). The leaf is now held
in a scoped_X509 so every path releases it.
4. _prep_ssl_ctx and SSLCreateServerContext leaked already-parsed leaves when
load_certs_and_cross_reference_names failed partway, because the X509_free
drain was skipped on the early return.
5. set_session_id_context leaked the client CA name stack on the digest error
paths that run before ownership is handed to the SSL_CTX.
6. ssl_callback_ocsp_stapling ignored the OCSP-response set result and leaked
the buffer on failure. On BoringSSL it now uses SSL_set_ocsp_response,
which copies the cached buffer and needs no per-handshake allocation; on
OpenSSL it checks SSL_set_tlsext_status_ocsp_resp and frees on failure.
7. stapling_refresh_response leaked the duplicated OCSP CERTID when
TS_OCSP_request_add0_id failed.
(cherry picked from commit b85ebd8709ac16ca8cdae5372bab38fc7b90e148)
---
src/iocore/net/OCSPStapling.cc | 17 +++++++++-
src/iocore/net/SSLNetVConnection.cc | 4 +++
src/iocore/net/SSLUtils.cc | 63 ++++++++++++++++++++-----------------
3 files changed, 55 insertions(+), 29 deletions(-)
diff --git a/src/iocore/net/OCSPStapling.cc b/src/iocore/net/OCSPStapling.cc
index 0c500f9006..342e0dea49 100644
--- a/src/iocore/net/OCSPStapling.cc
+++ b/src/iocore/net/OCSPStapling.cc
@@ -1240,6 +1240,8 @@ stapling_refresh_response(certinfo *cinf, TS_OCSP_RESPONSE **prsp)
goto err;
}
if (!TS_OCSP_request_add0_id(req, id)) {
+ // add0 only takes ownership of id on success.
+ TS_OCSP_CERTID_free(id);
goto err;
}
@@ -1424,6 +1426,11 @@ ssl_callback_ocsp_stapling(SSL *ssl, void *)
Error("ssl_callback_ocsp_stapling: failed to get certificate status for %s", cinf->certname);
return SSL_TLSEXT_ERR_NOACK;
} else {
+#ifdef OPENSSL_IS_BORINGSSL
+ // SSL_set_ocsp_response copies the response, so hand it the cached buffer directly.
+ int set_ok = SSL_set_ocsp_response(ssl, cinf->resp_der, cinf->resp_derlen);
+ ink_mutex_release(&cinf->stapling_mutex);
+#else
unsigned char *p = static_cast<unsigned char *>(OPENSSL_malloc(cinf->resp_derlen));
if (p == nullptr) {
ink_mutex_release(&cinf->stapling_mutex);
@@ -1432,7 +1439,15 @@ ssl_callback_ocsp_stapling(SSL *ssl, void *)
}
memcpy(p, cinf->resp_der, cinf->resp_derlen);
ink_mutex_release(&cinf->stapling_mutex);
- SSL_set_tlsext_status_ocsp_resp(ssl, p, cinf->resp_derlen);
+ // Takes ownership of p and frees it on success; on failure it does not.
+ int set_ok = SSL_set_tlsext_status_ocsp_resp(ssl, p, cinf->resp_derlen);
+ if (set_ok == 0) {
+ OPENSSL_free(p);
+ }
+#endif
+ if (set_ok == 0) {
+ return SSL_TLSEXT_ERR_NOACK;
+ }
Dbg(dbg_ctl_ssl_ocsp, "ssl_callback_ocsp_stapling: successfully got certificate status for %s", cinf->certname);
Dbg(dbg_ctl_ssl_ocsp, "is_prefetched:%d uri:%s", cinf->is_prefetched, cinf->uri);
return SSL_TLSEXT_ERR_OK;
diff --git a/src/iocore/net/SSLNetVConnection.cc b/src/iocore/net/SSLNetVConnection.cc
index b14462a4da..e4150f0915 100644
--- a/src/iocore/net/SSLNetVConnection.cc
+++ b/src/iocore/net/SSLNetVConnection.cc
@@ -1743,6 +1743,10 @@ SSLNetVConnection::populate(Connection &con, Continuation *c, void *arg)
{
int retval = super::populate(con, c, arg);
if (retval != EVENT_DONE) {
+ // arg is the migrated SSL this VC would adopt below; release it since we won't.
+ if (arg != nullptr) {
+ SSL_free(static_cast<SSL *>(arg));
+ }
return retval;
}
// Add in the SSL data
diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc
index 7c37a2367d..ef1d0080dd 100644
--- a/src/iocore/net/SSLUtils.cc
+++ b/src/iocore/net/SSLUtils.cc
@@ -1261,8 +1261,11 @@ setClientCertCACerts(SSL *ssl, const char *file, const char *dir)
#if TS_HAS_VERIFY_CERT_STORE
// The set0 version will take ownership of the X509_STORE object
X509_STORE *ctx = X509_STORE_new();
- if (X509_STORE_load_locations(ctx, file && file[0] != '\0' ? file : nullptr, dir && dir[0] != '\0' ? dir : nullptr)) {
+ if (ctx != nullptr &&
+ X509_STORE_load_locations(ctx, file && file[0] != '\0' ? file : nullptr, dir && dir[0] != '\0' ? dir : nullptr)) {
SSL_set0_verify_cert_store(ssl, ctx);
+ } else {
+ X509_STORE_free(ctx);
}
// SSL_set_client_CA_list takes ownership of the STACK_OF(X509) structure
@@ -1688,13 +1691,15 @@ SSLCreateServerContext(const SSLConfigParams *params, const SSLMultiCertConfigPa
std::unordered_map<int, std::set<std::string>> unique_names;
SSLMultiCertConfigLoader::CertLoadData data;
SSLCertContextType cert_type;
- if (!loader.load_certs_and_cross_reference_names(cert_list, data, params, sslMultiCertSettings, common_names, unique_names,
- &cert_type)) {
- return nullptr;
- }
+ bool loaded = loader.load_certs_and_cross_reference_names(cert_list, data, params, sslMultiCertSettings, common_names,
+ unique_names, &cert_type);
+ // cert_list may hold parsed leaves even when loading failed partway.
for (auto &i : cert_list) {
X509_free(i);
}
+ if (!loaded) {
+ return nullptr;
+ }
std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> ctx(nullptr, &SSL_CTX_free);
@@ -1739,24 +1744,24 @@ SSLMultiCertConfigLoader::_prep_ssl_ctx(const shared_SSLMultiCertConfigParams &
const SSLConfigParams *params = this->_params;
SSLCertContextType cert_type;
- if (!this->load_certs_and_cross_reference_names(cert_list, data, params, sslMultCertSettings.get(), common_names, unique_names,
- &cert_type)) {
- return false;
- }
-
- int i = 0;
- bool good_certs = true;
- for (auto const &cert : cert_list) {
- const char *current_cert_name = data.cert_names_list[i].c_str();
- if (0 > SSLMultiCertConfigLoader::check_server_cert_now(cert, current_cert_name)) {
- /* At this point, we know cert is bad, and we've already printed a
- descriptive reason as to why cert is bad to the log file */
- Dbg(this->_dbg_ctl(), "Marking certificate as NOT VALID: %s", current_cert_name);
- good_certs = false;
+ bool good_certs = this->load_certs_and_cross_reference_names(cert_list, data, params, sslMultCertSettings.get(), common_names,
+ unique_names, &cert_type);
+
+ if (good_certs) {
+ int i = 0;
+ for (auto const &cert : cert_list) {
+ const char *current_cert_name = data.cert_names_list[i].c_str();
+ if (0 > SSLMultiCertConfigLoader::check_server_cert_now(cert, current_cert_name)) {
+ /* At this point, we know cert is bad, and we've already printed a
+ descriptive reason as to why cert is bad to the log file */
+ Dbg(this->_dbg_ctl(), "Marking certificate as NOT VALID: %s", current_cert_name);
+ good_certs = false;
+ }
+ i++;
}
- i++;
}
+ // cert_list may hold parsed leaves even when loading failed partway.
for (auto &cert : cert_list) {
X509_free(cert);
}
@@ -2516,10 +2521,10 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, const std::vector<std::string
keyPath.empty() ? "[empty key path]" : keyPath.c_str());
return false;
}
- scoped_BIO bio(BIO_new_mem_buf(secret_data.data(), secret_data.size()));
- X509 *cert = nullptr;
+ scoped_BIO bio(BIO_new_mem_buf(secret_data.data(), secret_data.size()));
+ scoped_X509 cert;
if (bio) {
- cert = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
+ cert.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
} else {
SSLError("failed to create bio for certificate secret %s of length %ld", data.cert_names_list[i].c_str(), secret_data.size());
return false;
@@ -2531,9 +2536,8 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, const std::vector<std::string
}
Dbg(dbg_ctl_ssl_load, "for ctx=%p, using certificate %s", ctx, cert_names_list[i].c_str());
- if (!SSL_CTX_use_certificate(ctx, cert)) {
+ if (!SSL_CTX_use_certificate(ctx, cert.get())) {
SSLError("Failed to assign cert from %s to SSL_CTX", cert_names_list[i].c_str());
- X509_free(cert);
return false;
}
@@ -2587,16 +2591,15 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, const std::vector<std::string
if (sslMultCertSettings->ocsp_response) {
const char *ocsp_response_name = data.ocsp_list[i].c_str();
std::string completeOCSPResponsePath(Layout::relative_to(params->ssl_ocsp_response_path_only, ocsp_response_name));
- if (!ssl_stapling_init_cert(ctx, cert, cert_names_list[i].c_str(), completeOCSPResponsePath.c_str())) {
+ if (!ssl_stapling_init_cert(ctx, cert.get(), cert_names_list[i].c_str(), completeOCSPResponsePath.c_str())) {
Warning("failed to configure SSL_CTX for OCSP Stapling info for certificate at %s", cert_names_list[i].c_str());
}
} else {
- if (!ssl_stapling_init_cert(ctx, cert, cert_names_list[i].c_str(), nullptr)) {
+ if (!ssl_stapling_init_cert(ctx, cert.get(), cert_names_list[i].c_str(), nullptr)) {
Warning("failed to configure SSL_CTX for OCSP Stapling info for certificate at %s", cert_names_list[i].c_str());
}
}
}
- X509_free(cert);
}
return true;
}
@@ -2647,6 +2650,7 @@ SSLMultiCertConfigLoader::set_session_id_context(SSL_CTX *ctx, const SSLConfigPa
// Set the list of CA's to send to client if we ask for a client certificate
SSL_CTX_set_client_CA_list(ctx, ca_list);
+ ca_list = nullptr; // ownership transferred to ctx
}
if (EVP_DigestFinal_ex(digest, hash_buf, &hash_len) == 0) {
@@ -2663,6 +2667,9 @@ SSLMultiCertConfigLoader::set_session_id_context(SSL_CTX *ctx, const SSLConfigPa
fail:
EVP_MD_CTX_free(digest);
+ if (ca_list != nullptr) {
+ sk_X509_NAME_pop_free(ca_list, X509_NAME_free);
+ }
return result;
}
(trafficserver) 20/33: tools/clang-format.sh: cache clang-format in the common git dir (#13302)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit b8a6099c3027410a99e4ca51b1775d4781f040e6
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 15:59:54 2026 -0500
tools/clang-format.sh: cache clang-format in the common git dir (#13302)
clang-format.sh located its download cache with `git rev-parse
--absolute-git-dir`, which in a linked worktree is the per-worktree git
dir (.git/worktrees/<name>) rather than the shared .git. clang-format
is installed once under the common dir, so in a worktree the pre-commit
hook and the format target could not find it and failed with "No
clang-format found".
Resolve the common git dir instead, matching how CMakeLists.txt already
computes GIT_COMMON_DIR. It is made absolute with `cd ... && pwd`
rather than `git rev-parse --path-format=absolute`, avoiding a
dependency on git 2.31+; that --path-format form was removed in #11495
over a cmake 3.28 incompatibility.
The no-argument default target now uses `git rev-parse --show-toplevel`
so a bare run inside a worktree formats that worktree, not the main
checkout.
Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
(cherry picked from commit 9eac80fc2d9af95cd37716dceed39e4bfe58c5e2)
---
tools/clang-format.sh | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/tools/clang-format.sh b/tools/clang-format.sh
index 23e17bb6a4..6f9ee6a78e 100755
--- a/tools/clang-format.sh
+++ b/tools/clang-format.sh
@@ -24,8 +24,12 @@ PKGDATE="20240430"
function main() {
set -e # exit on error
- GIT_DIR=$(git rev-parse --absolute-git-dir)
- ROOT=${ROOT:-${GIT_DIR}/fmt/${PKGDATE}}
+ # Resolve the common git directory, which is shared by all linked worktrees, so the
+ # downloaded clang-format is cached and found in one place. --git-common-dir can return a
+ # relative path, so make it absolute without --path-format (which needs git 2.31+); this
+ # matches how CMakeLists.txt resolves GIT_COMMON_DIR.
+ GIT_COMMON_DIR=$(cd "$(git rev-parse --git-common-dir)" && pwd)
+ ROOT=${ROOT:-${GIT_COMMON_DIR}/fmt/${PKGDATE}}
# The presence of this file indicates clang-format was successfully installed.
INSTALLED_SENTINEL=${ROOT}/.clang-format-installed
@@ -38,7 +42,7 @@ function main() {
exit 2
fi
fi
- DIR=${@:-$(dirname ${GIT_DIR})}
+ DIR=${@:-$(git rev-parse --show-toplevel)}
PACKAGE="clang-format-${PKGDATE}.tar.bz2"
VERSION="clang-format version 18.1.2 (https://github.com/llvm/llvm-project.git 26a1d6601d727a96f4301d0d8647b5a42760ae0c)"
@@ -119,6 +123,6 @@ EOF
if [[ "$(basename -- "$0")" == 'clang-format.sh' ]]; then
main "$@"
else
- GIT_DIR=$(git rev-parse --absolute-git-dir)
- ROOT=${ROOT:-${GIT_DIR}/fmt/${PKGDATE}}
+ GIT_COMMON_DIR=$(cd "$(git rev-parse --git-common-dir)" && pwd)
+ ROOT=${ROOT:-${GIT_COMMON_DIR}/fmt/${PKGDATE}}
fi
(trafficserver) 18/33: Fix pending HostDB DNS queue removal race (#13294)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 508184818088b4b44ef6c8c51ff113895f836fd3
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Tue Jun 23 14:30:28 2026 -0500
Fix pending HostDB DNS queue removal race (#13294)
A crash was observed while HostDB was probing pending DNS state for a
request, with the stack unwinding through the DNS lookup path:
#4 swoc::bwf::ExternalNames::operator()(...)
at libswoc_1.5.15/include/swoc/bwf_base.h:610
#7 HostDBContinuation::do_dns(...)
at src/iocore/hostdb/HostDB.cc:1352
#11 probe(...)
at src/iocore/hostdb/HostDB.cc:581
The core showed an obviously corrupted ExternalNames this pointer while
HostDB was allocating a continuation from the pending DNS path. The local
signal cleanup path could edit the pending-DNS queue without the bucket
lock, leaving stale revalidation readers able to walk links while they
were being changed.
This routes that cleanup through the locked pending-DNS removal helper
and uses its result to decide whether the continuation still owns
self-cleanup. This keeps queue membership checks and removals
synchronized without changing the timeout path's lifetime behavior.
(cherry picked from commit ad5728ca9c4b7f84abc5ce5a95bb17396b3a975a)
---
src/iocore/hostdb/HostDB.cc | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/iocore/hostdb/HostDB.cc b/src/iocore/hostdb/HostDB.cc
index fc7bc40fea..780de2a5d6 100644
--- a/src/iocore/hostdb/HostDB.cc
+++ b/src/iocore/hostdb/HostDB.cc
@@ -837,6 +837,7 @@ HostDBContinuation::dnsEvent(int event, HostEnt *e)
// Event should be immediate or interval.
if (!action.continuation) {
// Nothing to do, give up.
+ bool cleanup_self = false;
if (event == EVENT_INTERVAL) {
// Timeout - clear all queries queued up for this FQDN because none of the other ones have sent an
// actual DNS query. If the request rate is high enough this can cause a persistent queue where the
@@ -846,9 +847,9 @@ HostDBContinuation::dnsEvent(int event, HostEnt *e)
} else {
// "local" signal to give up, usually due this being one of those "other" queries.
// That generally means @a this has already been removed from the queue, but just in case...
- hostDB.pending_dns_for_hash(hash.hash).remove(this);
+ cleanup_self = hostDB.remove_from_pending_dns_for_hash(hash.hash, this);
}
- if (hostDB.remove_from_pending_dns_for_hash(hash.hash, this)) {
+ if (cleanup_self) {
hostdb_cont_free(this);
}
return EVENT_DONE;
(trafficserver) 15/33: compress: count uncompressed input bytes (#13287)
Posted by cm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 4fbb38df68baa325296d2ba22ccf8a479ff8d3ec
Author: Mo Chen <mo...@apache.org>
AuthorDate: Sat Jun 20 20:58:40 2026 -0500
compress: count uncompressed input bytes (#13287)
Add proxy.process.plugin.compress.bytes_in to track the volume of data the
compress plugin processes.
Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
(cherry picked from commit 1e109ce2d47d6a7c523d8116afc265699ff686d5)
---
plugins/compress/compress.cc | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/plugins/compress/compress.cc b/plugins/compress/compress.cc
index 30cb749d7b..58fd52802f 100644
--- a/plugins/compress/compress.cc
+++ b/plugins/compress/compress.cc
@@ -28,6 +28,7 @@
#include "tscore/ink_config.h"
#include <tsutil/PostScript.h>
+#include <tsutil/Metrics.h>
#include "ts/ts.h"
#include "tscore/ink_defs.h"
@@ -349,8 +350,18 @@ compress_transform_init(TSCont contp, Data *data)
TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
}
+static ts::Metrics::Counter::AtomicType *compress_stat_bytes_in = nullptr;
+
static void
-compress_transform_one(Data *data, TSIOBufferReader upstream_reader, int amount)
+init_compress_stats()
+{
+ if (compress_stat_bytes_in == nullptr) {
+ compress_stat_bytes_in = ts::Metrics::Counter::createPtr("proxy.process.plugin.compress.bytes_in");
+ }
+}
+
+static void
+compress_transform_one(Data *data, TSIOBufferReader upstream_reader, int64_t amount)
{
TSIOBufferBlock downstream_blkp;
int64_t upstream_length;
@@ -371,6 +382,10 @@ compress_transform_one(Data *data, TSIOBufferReader upstream_reader, int amount)
upstream_length = amount;
}
+ if (compress_stat_bytes_in != nullptr) {
+ compress_stat_bytes_in->increment(upstream_length);
+ }
+
#if HAVE_ZSTD_H
if (data->compression_type & COMPRESSION_TYPE_ZSTD && (data->compression_algorithms & ALGORITHM_ZSTD)) {
Zstd::transform_one(data, upstream_buffer, upstream_length);
@@ -1032,6 +1047,8 @@ TSPluginInit(int argc, const char *argv[])
fatal("the compress plugin failed to register");
}
+ Compress::init_compress_stats();
+
info("TSPluginInit %s", argv[0]);
if (!Compress::global_hidden_header_name) {
@@ -1060,6 +1077,7 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
info("The compress plugin is successfully initialized");
+ Compress::init_compress_stats();
return TS_SUCCESS;
}