This is an automated email from the ASF dual-hosted git repository.
moonchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 113f5fbb16 Fix hdrHeap/hdrStrHeap allocator inuse metric underflow
(#13218)
113f5fbb16 is described below
commit 113f5fbb1667df56799e9878c71d6cd286695a49
Author: Mo Chen <[email protected]>
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 <[email protected]>
---
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