bryancall commented on code in PR #13257:
URL: https://github.com/apache/trafficserver/pull/13257#discussion_r3398037994
##########
src/iocore/cache/RamCacheCLFUS.cc:
##########
@@ -298,6 +285,31 @@ RamCacheCLFUS::get(CryptoHash *key, Ptr<IOBufferData>
*ret_data, uint64_t auxkey
ram_hit_state = RAM_HIT_COMPRESS_LIBLZMA;
break;
}
+#endif
+#ifdef HAVE_LZ4_H
+ case CACHE_COMPRESSION_LZ4: {
+ int l = static_cast<int>(e->len);
+ if (l != LZ4_decompress_safe(e->data->data(), b,
e->compressed_len, l)) {
+ goto Lfailed;
+ }
+ ram_hit_state = RAM_HIT_COMPRESS_LZ4;
+ break;
+ }
+#endif
+#ifdef HAVE_ZSTD_H
+ case CACHE_COMPRESSION_ZSTD: {
+ size_t l = static_cast<size_t>(e->len);
+ ZSTD_DCtx *dctx = zstd_dctx();
+ if (dctx == nullptr) {
+ goto Lfailed;
+ }
Review Comment:
A null `dctx` here is a *thread/allocator* condition, not entry corruption,
but `goto Lfailed` destroys an otherwise-valid compressed entry — so a
transient allocation failure evicts good cache entries. Consider treating a
null `dctx` as a miss (`goto Lerror`, leaving the entry intact) rather than
`Lfailed`.
More broadly: both the null-`dctx` and the `ZSTD_isError(ll) || l != ll`
paths fall into `Lfailed` with only the debug-gated `Z_ERR` trace, so a genuine
decompression/integrity error is indistinguishable from an ordinary cache miss
in production. A rate-limited `Warning` + a distinct metric at the shared
`Lfailed` label (reading the entry fields into locals before `_destroy(e)`)
would make corruption visible. Note this is the same pattern the existing
fastlz/libz paths already have, so it's a pre-existing gap now extended to two
more codecs — worth fixing once for all of them.
##########
src/iocore/cache/unit_tests/test_RamCacheCLFUS.cc:
##########
@@ -0,0 +1,229 @@
+/** @file
+
+ Catch-based unit tests for RAM cache (CLFUS) compression roundtrips.
+
+ @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 "main.h"
+
+#include "../RamCacheCLFUS.h"
+#include "../P_CacheInternal.h"
+
+#include "iocore/cache/Cache.h"
+#include "tscore/ink_config.h"
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+// Required by main.h
+int cache_vols = 1;
+bool reuse_existing_cache = false;
+
+namespace
+{
+
+// A compression backend to exercise, along with the RAM_HIT_* state get()
+// should report once a compressible object has been stored compressed.
+struct CompressionCase {
+ int config; // CACHE_COMPRESSION_*
+ int expected_hit; // RAM_HIT_COMPRESS_* reported by get() for
compressible data
+ const char *name;
+};
+
+std::vector<CompressionCase>
+compression_cases()
+{
+ std::vector<CompressionCase> cases{
+ {CACHE_COMPRESSION_NONE, RAM_HIT_COMPRESS_NONE, "none" },
+ {CACHE_COMPRESSION_FASTLZ, RAM_HIT_COMPRESS_FASTLZ, "fastlz"},
+ {CACHE_COMPRESSION_LIBZ, RAM_HIT_COMPRESS_LIBZ, "libz" },
+ };
+#ifdef HAVE_LZMA_H
+ cases.push_back({CACHE_COMPRESSION_LIBLZMA, RAM_HIT_COMPRESS_LIBLZMA,
"liblzma"});
+#endif
+#ifdef HAVE_LZ4_H
+ cases.push_back({CACHE_COMPRESSION_LZ4, RAM_HIT_COMPRESS_LZ4, "lz4"});
+#endif
+#ifdef HAVE_ZSTD_H
+ cases.push_back({CACHE_COMPRESSION_ZSTD, RAM_HIT_COMPRESS_ZSTD, "zstd"});
+#endif
Review Comment:
These cases are gated on `HAVE_LZ4_H`/`HAVE_ZSTD_H`, and
`find_package(LZ4/ZSTD)` is non-`REQUIRED`, so a build without the dev libs (or
zstd < 1.4.0) skips the lz4/zstd cases and the suite still passes green — i.e.
the test can't fail for the two backends it was written to protect. The CI
Dockerfiles do add the libs, but nothing asserts they were actually compiled
in. A skip-with-`WARN` (or a static check on the CI image) would make a missing
backend visible in the test output instead of silently reducing coverage.
##########
src/iocore/cache/unit_tests/test_RamCacheCLFUS.cc:
##########
@@ -0,0 +1,229 @@
+/** @file
+
+ Catch-based unit tests for RAM cache (CLFUS) compression roundtrips.
+
+ @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 "main.h"
+
+#include "../RamCacheCLFUS.h"
+#include "../P_CacheInternal.h"
+
+#include "iocore/cache/Cache.h"
+#include "tscore/ink_config.h"
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+// Required by main.h
+int cache_vols = 1;
+bool reuse_existing_cache = false;
+
+namespace
+{
+
+// A compression backend to exercise, along with the RAM_HIT_* state get()
+// should report once a compressible object has been stored compressed.
+struct CompressionCase {
+ int config; // CACHE_COMPRESSION_*
+ int expected_hit; // RAM_HIT_COMPRESS_* reported by get() for
compressible data
+ const char *name;
+};
+
+std::vector<CompressionCase>
+compression_cases()
+{
+ std::vector<CompressionCase> cases{
+ {CACHE_COMPRESSION_NONE, RAM_HIT_COMPRESS_NONE, "none" },
+ {CACHE_COMPRESSION_FASTLZ, RAM_HIT_COMPRESS_FASTLZ, "fastlz"},
+ {CACHE_COMPRESSION_LIBZ, RAM_HIT_COMPRESS_LIBZ, "libz" },
+ };
+#ifdef HAVE_LZMA_H
+ cases.push_back({CACHE_COMPRESSION_LIBLZMA, RAM_HIT_COMPRESS_LIBLZMA,
"liblzma"});
+#endif
+#ifdef HAVE_LZ4_H
+ cases.push_back({CACHE_COMPRESSION_LZ4, RAM_HIT_COMPRESS_LZ4, "lz4"});
+#endif
+#ifdef HAVE_ZSTD_H
+ cases.push_back({CACHE_COMPRESSION_ZSTD, RAM_HIT_COMPRESS_ZSTD, "zstd"});
+#endif
+ return cases;
+}
+
+// Minimal CacheDisk wiring needed to construct a StripeSM. Mirrors the helper
+// in test_Stripe.cc.
+void
+init_disk(CacheDisk &disk)
+{
+ disk.path = static_cast<char *>(ats_malloc(1));
+ disk.path[0] = '\0';
+ disk.disk_stripes = static_cast<DiskStripe
**>(ats_malloc(sizeof(DiskStripe *)));
+ disk.disk_stripes[0] = nullptr;
+ disk.header = static_cast<DiskHeader
*>(ats_malloc(sizeof(DiskHeader)));
+ disk.header->num_volumes = 0;
+}
+
+// The CLFUS get/put/compress paths touch only these metrics and the stripe
+// mutex, so that is all the stripe needs for these tests.
+void
+wire_stripe(StripeSM &stripe, CacheVol &cache_vol)
+{
+ stripe.cache_vol = &cache_vol;
+
+ cache_rsb.ram_cache_bytes =
ts::Metrics::Gauge::createPtr("unit_test.clfus.ram_cache.bytes");
+ cache_rsb.ram_cache_hits =
ts::Metrics::Counter::createPtr("unit_test.clfus.ram_cache.hits");
+ cache_rsb.ram_cache_misses =
ts::Metrics::Counter::createPtr("unit_test.clfus.ram_cache.misses");
+ cache_vol.vol_rsb.ram_cache_bytes =
ts::Metrics::Gauge::createPtr("unit_test.clfus.vol.ram_cache.bytes");
+ cache_vol.vol_rsb.ram_cache_hits =
ts::Metrics::Counter::createPtr("unit_test.clfus.vol.ram_cache.hits");
+ cache_vol.vol_rsb.ram_cache_misses =
ts::Metrics::Counter::createPtr("unit_test.clfus.vol.ram_cache.misses");
+}
+
+Ptr<IOBufferData>
+make_buffer(const std::vector<char> &bytes)
+{
+ int64_t idx = iobuffer_size_to_index(bytes.size(),
MAX_BUFFER_SIZE_INDEX);
+ Ptr<IOBufferData> data{make_ptr(new_IOBufferData(idx, MEMALIGNED))};
+ std::memcpy(data->data(), bytes.data(), bytes.size());
+ return data;
+}
+
+// Highly compressible: a short repeating pattern.
+std::vector<char>
+compressible_bytes(std::size_t len)
+{
+ std::vector<char> bytes(len);
+ for (std::size_t i = 0; i < len; i++) {
+ bytes[i] = static_cast<char>('A' + (i % 26));
+ }
+ return bytes;
+}
+
+// Effectively incompressible: a deterministic xorshift byte stream.
+std::vector<char>
+incompressible_bytes(std::size_t len)
+{
+ std::vector<char> bytes(len);
+ uint32_t state = 0x9e3779b9;
+ for (std::size_t i = 0; i < len; i++) {
+ state ^= state << 13;
+ state ^= state >> 17;
+ state ^= state << 5;
+ bytes[i] = static_cast<char>(state & 0xff);
+ }
+ return bytes;
+}
+
+// Store payload under a fresh key, force a synchronous compression pass with
+// `config`, then read it back. Returns the RAM_HIT_* state reported by get()
+// and the bytes that were returned.
+int
+store_compress_get(StripeSM &stripe, int config, const std::vector<char>
&payload, std::vector<char> &out)
+{
+ // Initialize with compression disabled so init() does not schedule the
+ // background compressor (which would retain a pointer to this stack object).
+ cache_config_ram_cache_compress = CACHE_COMPRESSION_NONE;
+ cache_config_ram_cache_compress_percent = 100;
+ cache_config_ram_cache_use_seen_filter = 0;
+
+ RamCacheCLFUS rc;
+ rc.init(1 << 20, &stripe);
+
+ Ptr<IOBufferData> in = make_buffer(payload);
+ uint32_t len = static_cast<uint32_t>(payload.size());
+
+ static uint64_t salt = 0;
+ ++salt;
+ CryptoHash key;
+ key.u64[0] = 0xc0ffee00 + salt;
+ key.u64[1] = 0xdeadbeef + salt;
+
+ REQUIRE(rc.put(&key, in.get(), len) == 1);
+
+ cache_config_ram_cache_compress = config;
+ rc.compress_entries(this_ethread());
+
+ Ptr<IOBufferData> ret;
+ int hit = rc.get(&key, &ret);
+ REQUIRE(ret.get() != nullptr);
+ out.assign(ret->data(), ret->data() + len);
+ return hit;
+}
+
+} // namespace
+
+TEST_CASE("CLFUS compressible objects roundtrip cleanly",
"[cache][ramcache][compress]")
+{
+ CacheDisk disk;
+ init_disk(disk);
+ StripeSM stripe{&disk, 10, 0};
+ CacheVol cache_vol;
+ wire_stripe(stripe, cache_vol);
+
+ auto payload = compressible_bytes(8192);
+ const CompressionCase c = GENERATE(from_range(compression_cases()));
+ INFO("compression backend: " << c.name);
+
+ std::vector<char> out;
+ int hit = store_compress_get(stripe, c.config, payload, out);
+
+ CHECK(hit == c.expected_hit);
+ CHECK(out == payload);
Review Comment:
This asserts the read-back bytes and the reported `RAM_HIT_*` state, but not
that compression actually happened — a backend that "compresses" to a
same-or-larger blob but still tags the type would pass. Adding
`CHECK(e->compressed_len < len)` for the compressible case asserts the
feature's actual contract (it saves memory). A larger payload (say >= 128 KB)
would also exercise the `*_compressBound()` math and the `uint32_t` casts in
`compress_entries()`, which the 8 KB / 1-byte cases don't reach.
##########
src/iocore/cache/RamCacheCLFUS.cc:
##########
@@ -35,6 +36,45 @@
#ifdef HAVE_LZMA_H
#include <lzma.h>
#endif
+#ifdef HAVE_LZ4_H
+#include <lz4.h>
+#endif
+#ifdef HAVE_ZSTD_H
+#include <zstd.h>
+#include <memory>
+constexpr int CLFUS_ZSTD_LEVEL = 3;
+
+namespace
+{
+
+// One-shot ZSTD_compress/ZSTD_decompress allocate and free a context on every
+// call, so reuse a per-thread context instead. May return nullptr if zstd
+// fails to allocate one. The compression level is a sticky parameter set once
+// here; no explicit ZSTD_CCtx_reset() is needed because ZSTD_compress2()
+// starts a new session on every call (resets are only for interrupting the
+// streaming API or changing sticky parameters).
+ZSTD_CCtx *
+zstd_cctx()
+{
+ thread_local std::unique_ptr<ZSTD_CCtx, size_t (*)(ZSTD_CCtx *)> ctx = [] {
+ std::unique_ptr<ZSTD_CCtx, size_t (*)(ZSTD_CCtx *)> c{ZSTD_createCCtx(),
ZSTD_freeCCtx};
+ if (c && ZSTD_isError(ZSTD_CCtx_setParameter(c.get(),
ZSTD_c_compressionLevel, CLFUS_ZSTD_LEVEL))) {
+ c.reset();
+ }
+ return c;
+ }();
+ return ctx.get();
Review Comment:
The context is a `thread_local` initialized once via the IIFE lambda, so a
failed `ZSTD_createCCtx()`/`ZSTD_CCtx_setParameter()` (e.g. OOM) is **sticky
for the life of the thread** — the lambda never re-runs, so `zstd_cctx()`
returns `nullptr` on every subsequent call on that thread.
Downstream that's silent: in `compress_entries()` a null `cctx` sets `failed
= true`, which marks the entry `incompressible` and stores it uncompressed and
never retried. So a transient allocation failure permanently degrades every
object on that thread to uncompressed, with no log line or metric to explain
the lost compression. Consider a rate-limited `Warning` (and ideally a distinct
metric) the first time a context can't be allocated, so this is diagnosable
rather than invisible. Same applies to `zstd_dctx()` on the get path (see the
get() comment).
##########
include/iocore/cache/Cache.h:
##########
@@ -43,8 +43,18 @@ static constexpr ts::ModuleVersion CACHE_MODULE_VERSION(1,
0);
#define CACHE_COMPRESSION_FASTLZ 1
#define CACHE_COMPRESSION_LIBZ 2
#define CACHE_COMPRESSION_LIBLZMA 3
-
-enum { RAM_HIT_COMPRESS_NONE = 1, RAM_HIT_COMPRESS_FASTLZ,
RAM_HIT_COMPRESS_LIBZ, RAM_HIT_COMPRESS_LIBLZMA, RAM_HIT_LAST_ENTRY };
+#define CACHE_COMPRESSION_LZ4 4
+#define CACHE_COMPRESSION_ZSTD 5
+
+enum {
+ RAM_HIT_COMPRESS_NONE = 1,
+ RAM_HIT_COMPRESS_FASTLZ,
+ RAM_HIT_COMPRESS_LIBZ,
+ RAM_HIT_COMPRESS_LIBLZMA,
+ RAM_HIT_COMPRESS_LZ4,
+ RAM_HIT_COMPRESS_ZSTD,
+ RAM_HIT_LAST_ENTRY
+};
Review Comment:
`CACHE_COMPRESSION_*` (0..5) and `RAM_HIT_COMPRESS_*` (1..6) are two
parallel integer sequences kept in sync by hand with a +1 offset, and the
`compressed : 3` bitfield in `RamCacheCLFUSEntry` must hold the max
`CACHE_COMPRESSION_*` value. Both are correct today but unguarded. A couple of
`static_assert`s would pin the invariants at compile time, e.g.
`static_assert(RAM_HIT_COMPRESS_ZSTD == CACHE_COMPRESSION_ZSTD + 1);` and
`static_assert(CACHE_COMPRESSION_ZSTD < (1u << 3));` next to the bitfield — so
the next codec addition can't silently desync or overflow the field.
##########
doc/developer-guide/cache-architecture/ram-cache.en.rst:
##########
@@ -147,17 +147,23 @@ since we need to make a copy anyway. Those not tagged
``copy`` are inserted
uncompressed in the hope that they can be reused in uncompressed form. This is
a compile time option and may be something we want to change.
-There are 3 algorithms and levels of compression (speed on an Intel i7 920
-series processor using one thread):
+There are 5 algorithms and levels of compression (speed on an Intel Xeon Gold
+6338 processor using lzbench and the silesia XML corpus):
======= ================ ==================
====================================
Method Compression Rate Decompression Rate Notes
======= ================ ==================
====================================
-fastlz 173 MB/sec 442 MB/sec Basically free since disk or
network
- will limit first; ~53% final size.
-libz 55 MB/sec 234 MB/sec Almost free, particularly
- decompression; ~37% final size.
-liblzma 3 MB/sec 50 MB/sec Expensive; ~27% final size.
+fastlz 452 MB/sec 913 MB/sec Effectively obsolete; prefer lz4.
+ Basically free since disk or
network
+ will limit first; ~26% final size.
+libz 54 MB/sec 536 MB/sec Effectively obsolete; prefer zstd.
+ Almost free, particularly
+ decompression; ~13% final size.
+liblzma 5 MB/sec 291 MB/sec Expensive; ~8% final size.
+lz4 727 MB/sec 3458 MB/sec Basically free since disk or
network
+ will limit first; 23% final size
Review Comment:
Small doc nits: the lz4 row reads `23% final size` (no leading `~`, no
trailing period) while every other row uses the `~NN%.` form. Also worth noting
the zstd numbers are level-3 specific (`CLFUS_ZSTD_LEVEL = 3`); a note like
"zstd at level 3" ties the figures to the code constant so they don't silently
rot if the level changes. (Unrelated, while you're here: the `compressed` row a
few lines up documents the flag as `uncompressible`, but the struct field is
`incompressible`.)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]