| /** @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. |
| */ |
| |
| // Clocked Least Frequently Used by Size (CLFUS) replacement policy |
| // See https://docs.trafficserver.apache.org/en/latest/developer-guide/cache-architecture/ram-cache.en.html |
| |
| #include "RamCacheCLFUS.h" |
| #include "P_RamCache.h" |
| #include "P_CacheInternal.h" |
| #include "StripeSM.h" |
| #include "iocore/eventsystem/IOBuffer.h" |
| #include "iocore/eventsystem/Tasks.h" |
| #include "fastlz/fastlz.h" |
| #include "tscore/CryptoHash.h" |
| |
| #include <zlib.h> |
| #ifdef HAVE_LZMA_H |
| #include <lzma.h> |
| #endif |
| #ifdef HAVE_LZ4_H |
| #include <lz4.h> |
| #endif |
| #ifdef HAVE_ZSTD_H |
| #include <zstd.h> |
| // ZSTD_getErrorCode() and the ZSTD_error_* codes live here, not in zstd.h. |
| #include <zstd_errors.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; that failure is sticky for the life of the thread, so |
| // warn when it happens. 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(); |
| } |
| if (!c) { |
| Warning("unable to allocate zstd compression context; RAM cache entries will not be compressed on this thread"); |
| } |
| return c; |
| }(); |
| return ctx.get(); |
| } |
| |
| ZSTD_DCtx * |
| zstd_dctx() |
| { |
| thread_local std::unique_ptr<ZSTD_DCtx, size_t (*)(ZSTD_DCtx *)> ctx = [] { |
| std::unique_ptr<ZSTD_DCtx, size_t (*)(ZSTD_DCtx *)> c{ZSTD_createDCtx(), ZSTD_freeDCtx}; |
| if (!c) { |
| Warning("unable to allocate zstd decompression context; compressed RAM cache entries will miss on this thread"); |
| } |
| return c; |
| }(); |
| return ctx.get(); |
| } |
| |
| } // end anonymous namespace |
| #endif |
| |
| // #define CHECK_ACOUNTING 1 // very expensive double checking of all sizes |
| |
| constexpr double required_compression = 0.9; |
| constexpr double required_shrink = 0.8; |
| constexpr uint32_t history_hysteria = 10; |
| |
| #ifdef HAVE_LZMA_H |
| constexpr uint32_t lzma_base_memlimit = 64 * 1024 * 1024; |
| #endif |
| |
| constexpr uint32_t average_value_over = 100; |
| constexpr uint32_t requeue_limit = 100; |
| |
| #ifdef DEBUG |
| |
| namespace |
| { |
| |
| DbgCtl dbg_ctl_ram_cache{"ram_cache"}; |
| DbgCtl dbg_ctl_ram_cache_compare{"ram_cache_compare"}; |
| |
| } // end anonymous namespace |
| |
| #endif |
| |
| constexpr uint64_t |
| requeue_hits(const uint64_t hits) |
| { |
| return hits ? (hits - 1) : 0; |
| } |
| |
| int64_t |
| RamCacheCLFUS::size() const |
| { |
| int64_t s = 0; |
| for (int i = 0; i < 2; i++) { |
| forl_LL(RamCacheCLFUSEntry, e, this->_lru[i]) |
| { |
| s += sizeof(*e); |
| if (e->data) { |
| s += sizeof(*e->data); |
| s += e->data->block_size(); |
| } |
| } |
| } |
| return s; |
| } |
| |
| class RamCacheCLFUSCompressor : public Continuation |
| { |
| public: |
| RamCacheCLFUS *rc; |
| int mainEvent(int event, Event *e); |
| |
| RamCacheCLFUSCompressor(RamCacheCLFUS *arc) : rc(arc) { SET_HANDLER(&RamCacheCLFUSCompressor::mainEvent); } |
| }; |
| |
| int |
| RamCacheCLFUSCompressor::mainEvent(int /* event ATS_UNUSED */, Event *e) |
| { |
| // The codec is validated once in ink_cache_init(), before any cache exists. |
| if (cache_config_ram_cache_compress_percent) { |
| rc->compress_entries(e->ethread); |
| } |
| return EVENT_CONT; |
| } |
| |
| ClassAllocator<RamCacheCLFUSEntry, false> ramCacheCLFUSEntryAllocator("RamCacheCLFUSEntry"); |
| |
| static const int bucket_sizes[] = {127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, |
| 65521, 131071, 262139, 524287, 1048573, 2097143, 4194301, 8388593, 16777213, |
| 33554393, 67108859, 134217689, 268435399, 536870909, 1073741789, 2147483647}; |
| |
| // Only safe when init() did not schedule the background compressor, i.e. when |
| // cache_config_ram_cache_compress was CACHE_COMPRESSION_NONE at init() time. |
| // That scheduled RamCacheCLFUSCompressor holds a raw back-pointer to this |
| // object and nothing cancels it, so destroying a cache that has one would |
| // leave it dangling. Cancelling the event here would not be enough either: the |
| // continuation carries no mutex, so EventProcessor::schedule leaves the event |
| // with none and it can be running compress_entries() on an ET_TASK thread |
| // while this destructor runs. Making that safe means giving the compressor its |
| // own ProxyMutex and cancelling the retained Event under it -- not the stripe |
| // mutex, because Mutex_unlock() only decrements nthread_holding, so a |
| // continuation dispatched holding stripe->mutex would keep the stripe locked |
| // across the codec call and defeat the lock drop in compress_entries(). Not |
| // worth doing while production never destroys a RamCacheCLFUS -- these live |
| // for the lifetime of their StripeSM. Unit tests that construct one directly |
| // must init() with compression off and drive compress_entries() synchronously. |
| RamCacheCLFUS::~RamCacheCLFUS() |
| { |
| // Entries are pool-allocated without running their destructor, so release the |
| // data reference explicitly before returning each one to the allocator, then |
| // free the hash table and the seen filter. |
| // History entries (lru[1]) hold no data and were never counted. |
| while (RamCacheCLFUSEntry *e = this->_lru[0].dequeue()) { |
| this->_bytes -= e->size + entry_overhead; |
| ts::Metrics::Gauge::decrement(cache_rsb.ram_cache_bytes, e->size); |
| ts::Metrics::Gauge::decrement(stripe->cache_vol->vol_rsb.ram_cache_bytes, e->size); |
| this->_objects--; |
| e->data = nullptr; |
| THREAD_FREE(e, ramCacheCLFUSEntryAllocator, this_thread()); |
| } |
| while (RamCacheCLFUSEntry *e = this->_lru[1].dequeue()) { |
| this->_history--; |
| e->data = nullptr; |
| THREAD_FREE(e, ramCacheCLFUSEntryAllocator, this_thread()); |
| } |
| ats_free(this->_bucket); |
| ats_free(this->_seen); |
| } |
| |
| void |
| RamCacheCLFUS::_resize_hashtable() |
| { |
| int anbuckets = bucket_sizes[this->_ibuckets]; |
| DDbg(dbg_ctl_ram_cache, "resize hashtable %d", anbuckets); |
| int64_t s = anbuckets * sizeof(DList(RamCacheCLFUSEntry, hash_link)); |
| DList(RamCacheCLFUSEntry, hash_link) *new_bucket = static_cast<DList(RamCacheCLFUSEntry, hash_link) *>(ats_malloc(s)); |
| memset(static_cast<void *>(new_bucket), 0, s); |
| if (this->_bucket) { |
| for (int64_t i = 0; i < this->_nbuckets; i++) { |
| RamCacheCLFUSEntry *e = nullptr; |
| while ((e = this->_bucket[i].pop())) { |
| new_bucket[e->key.slice32(3) % anbuckets].push(e); |
| } |
| } |
| ats_free(this->_bucket); |
| } |
| this->_bucket = new_bucket; |
| this->_nbuckets = anbuckets; |
| ats_free(this->_seen); |
| if (cache_config_ram_cache_use_seen_filter) { |
| int size = bucket_sizes[this->_ibuckets] * sizeof(uint16_t); |
| this->_seen = static_cast<uint16_t *>(ats_malloc(size)); |
| memset(this->_seen, 0, size); |
| } |
| } |
| |
| void |
| RamCacheCLFUS::init(int64_t abytes, StripeSM *astripe) |
| { |
| ink_assert(astripe != nullptr); |
| stripe = astripe; |
| this->_max_bytes = abytes; |
| DDbg(dbg_ctl_ram_cache, "initializing ram_cache %" PRId64 " bytes", abytes); |
| if (!this->_max_bytes) { |
| return; |
| } |
| this->_resize_hashtable(); |
| if (cache_config_ram_cache_compress) { |
| eventProcessor.schedule_every(new RamCacheCLFUSCompressor(this), HRTIME_SECOND, ET_TASK); |
| } |
| } |
| |
| #ifdef CHECK_ACOUNTING |
| static void |
| check_accounting(RamCacheCLFUS *c) |
| { |
| int64_t x = 0, xsize = 0, h = 0; |
| RamCacheCLFUSEntry *y = c->lru[0].head; |
| while (y) { |
| x++; |
| xsize += y->size + RamCacheCLFUS::entry_overhead; |
| y = y->lru_link.next; |
| } |
| y = c->lru[1].head; |
| while (y) { |
| h++; |
| y = y->lru_link.next; |
| } |
| ink_assert(x == c->objects); |
| ink_assert(xsize == c->bytes); |
| ink_assert(h == c->history); |
| } |
| #else |
| #define check_accounting(_c) |
| #endif |
| |
| namespace |
| { |
| |
| // Record a RAM cache decompression failure. This is data corruption or a codec |
| // error rather than an ordinary miss, so it has to be visible outside a debug |
| // build: a warning carrying the codec's own diagnosis, which tells a corrupt |
| // frame apart from a bookkeeping error in e->len, plus the global and |
| // per-volume counters. Call while the entry is still intact. Throttled through |
| // the site-wide facility so it honors proxy.config.log.throttling_interval_msec |
| // and reports its own suppression count. |
| void |
| note_decompress_failure(StripeSM *stripe, const CryptoHash *key, const RamCacheCLFUSEntry *e, const char *detail) |
| { |
| SiteThrottledWarning("RAM cache decompression failed: type %d len %u compressed_len %u key %X: %s; entry dropped", |
| static_cast<int>(e->flag_bits.compressed), e->len, e->compressed_len, key->slice32(3), detail); |
| ts::Metrics::Counter::increment(cache_rsb.ram_cache_decompress_failures); |
| ts::Metrics::Counter::increment(stripe->cache_vol->vol_rsb.ram_cache_decompress_failures); |
| } |
| |
| } // end anonymous namespace |
| |
| int |
| RamCacheCLFUS::get(CryptoHash *key, Ptr<IOBufferData> *ret_data, uint64_t auxkey) |
| { |
| if (!this->_max_bytes) { |
| return 0; |
| } |
| int64_t i = key->slice32(3) % this->_nbuckets; |
| RamCacheCLFUSEntry *e = this->_bucket[i].head; |
| char *b = nullptr; |
| while (e) { |
| if (e->key == *key && e->auxkey == auxkey) { |
| this->_move_compressed(e); |
| if (!e->flag_bits.lru) { // in memory |
| if (cache_value(e) > this->_average_value) { |
| this->_lru[e->flag_bits.lru].remove(e); |
| this->_lru[e->flag_bits.lru].enqueue(e); |
| } |
| e->hits++; |
| uint32_t ram_hit_state = RAM_HIT_COMPRESS_NONE; |
| if (e->flag_bits.compressed) { |
| b = static_cast<char *>(ats_malloc(e->len)); |
| switch (e->flag_bits.compressed) { |
| default: |
| note_decompress_failure(stripe, key, e, "no decoder for this compression type"); |
| goto Lfailed; |
| case CACHE_COMPRESSION_FASTLZ: { |
| int l = static_cast<int>(e->len); |
| int rc = fastlz_decompress(e->data->data(), e->compressed_len, b, l); |
| if (l != rc) { |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "fastlz_decompress produced %d bytes, expected %d", rc, l); |
| note_decompress_failure(stripe, key, e, detail); |
| goto Lfailed; |
| } |
| ram_hit_state = RAM_HIT_COMPRESS_FASTLZ; |
| break; |
| } |
| case CACHE_COMPRESSION_LIBZ: { |
| uLongf l = e->len; |
| int rc = uncompress(reinterpret_cast<Bytef *>(b), &l, reinterpret_cast<Bytef *>(e->data->data()), e->compressed_len); |
| if (Z_OK != rc) { |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "uncompress: %s", zError(rc)); |
| note_decompress_failure(stripe, key, e, detail); |
| goto Lfailed; |
| } |
| ram_hit_state = RAM_HIT_COMPRESS_LIBZ; |
| break; |
| } |
| #ifdef HAVE_LZMA_H |
| case CACHE_COMPRESSION_LIBLZMA: { |
| size_t l = static_cast<size_t>(e->len), ipos = 0, opos = 0; |
| uint64_t memlimit = e->len * 2 + lzma_base_memlimit; |
| lzma_ret rc = lzma_stream_buffer_decode(&memlimit, 0, nullptr, reinterpret_cast<uint8_t *>(e->data->data()), &ipos, |
| e->compressed_len, reinterpret_cast<uint8_t *>(b), &opos, l); |
| if (LZMA_OK != rc) { |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "lzma_stream_buffer_decode returned %d, wrote %zu of %zu output bytes", |
| static_cast<int>(rc), opos, l); |
| note_decompress_failure(stripe, key, e, detail); |
| goto Lfailed; |
| } |
| ram_hit_state = RAM_HIT_COMPRESS_LIBLZMA; |
| break; |
| } |
| #endif |
| #ifdef HAVE_LZ4_H |
| case CACHE_COMPRESSION_LZ4: { |
| int l = static_cast<int>(e->len); |
| int rc = LZ4_decompress_safe(e->data->data(), b, e->compressed_len, l); |
| if (l != rc) { |
| // A negative return is a malformed frame; a smaller non-negative |
| // one means e->len disagrees with the frame's content. |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "LZ4_decompress_safe returned %d, expected %d", rc, l); |
| note_decompress_failure(stripe, key, e, detail); |
| 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) { |
| // This thread can't decompress, but the entry itself is fine: |
| // miss instead of evicting it. |
| ats_free(b); |
| goto Lerror; |
| } |
| size_t ll = ZSTD_decompressDCtx(dctx, b, l, e->data->data(), e->compressed_len); |
| if (ZSTD_isError(ll)) { |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "ZSTD_decompressDCtx: %s", ZSTD_getErrorName(ll)); |
| note_decompress_failure(stripe, key, e, detail); |
| goto Lfailed; |
| } |
| if (l != ll) { |
| char detail[128]; |
| snprintf(detail, sizeof(detail), "ZSTD_decompressDCtx produced %zu bytes, expected %zu", ll, l); |
| note_decompress_failure(stripe, key, e, detail); |
| goto Lfailed; |
| } |
| ram_hit_state = RAM_HIT_COMPRESS_ZSTD; |
| break; |
| } |
| #endif |
| } |
| IOBufferData *data = new_xmalloc_IOBufferData(b, e->len); |
| data->_mem_type = DEFAULT_ALLOC; |
| if (!e->flag_bits.copy) { // don't bother if we have to copy anyway |
| int64_t delta = (static_cast<int64_t>(e->compressed_len)) - static_cast<int64_t>(e->size); |
| this->_bytes += delta; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, delta); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, delta); |
| e->size = e->compressed_len; |
| check_accounting(this); |
| e->flag_bits.compressed = 0; |
| e->data = data; |
| } |
| (*ret_data) = data; |
| } else { |
| if (e->flag_bits.copy) { |
| (*ret_data) = copy_data_out(e->data.get(), e->len); |
| } else { |
| (*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); |
| DDbg(dbg_ctl_ram_cache, "get %X %" PRId64 " size %d HIT", key->slice32(3), auxkey, e->size); |
| return ram_hit_state; |
| } else { |
| ts::Metrics::Counter::increment(cache_rsb.ram_cache_misses); |
| ts::Metrics::Counter::increment(stripe->cache_vol->vol_rsb.ram_cache_misses); |
| DDbg(dbg_ctl_ram_cache, "get %X %" PRId64 " HISTORY", key->slice32(3), auxkey); |
| return 0; |
| } |
| } |
| assert(e != e->hash_link.next); |
| e = e->hash_link.next; |
| } |
| DDbg(dbg_ctl_ram_cache, "get %X %" PRId64 " MISS", key->slice32(3), auxkey); |
| Lerror: |
| ts::Metrics::Counter::increment(cache_rsb.ram_cache_misses); |
| ts::Metrics::Counter::increment(stripe->cache_vol->vol_rsb.ram_cache_misses); |
| |
| return 0; |
| Lfailed: |
| // Every branch above reported the failure through note_decompress_failure() |
| // while the entry was still intact; this only tears it down. |
| ats_free(b); |
| this->_destroy(e); |
| DDbg(dbg_ctl_ram_cache, "get %X %" PRId64 " Z_ERR", key->slice32(3), auxkey); |
| goto Lerror; |
| } |
| |
| void |
| RamCacheCLFUS::_tick() |
| { |
| RamCacheCLFUSEntry *e = this->_lru[1].dequeue(); |
| if (!e) { |
| return; |
| } |
| e->hits >>= 1; |
| if (e->hits) { |
| e->hits = requeue_hits(e->hits); |
| this->_lru[1].enqueue(e); |
| } else { |
| goto Lfree; |
| } |
| if (this->_history <= this->_objects + history_hysteria) { |
| return; |
| } |
| e = this->_lru[1].dequeue(); |
| Lfree: |
| if (!e) { // e may be nullptr after e= lru[1].dequeue() |
| return; |
| } |
| e->flag_bits.lru = 0; |
| this->_history--; |
| uint32_t b = e->key.slice32(3) % this->_nbuckets; |
| this->_bucket[b].remove(e); |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d FREED", e->key.slice32(3), e->auxkey, e->size); |
| THREAD_FREE(e, ramCacheCLFUSEntryAllocator, this_thread()); |
| } |
| |
| void |
| RamCacheCLFUS::_victimize(RamCacheCLFUSEntry *e) |
| { |
| this->_objects--; |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d VICTIMIZED", e->key.slice32(3), e->auxkey, e->size); |
| e->data = nullptr; |
| e->flag_bits.lru = 1; |
| this->_lru[1].enqueue(e); |
| this->_history++; |
| } |
| |
| void |
| RamCacheCLFUS::_move_compressed(RamCacheCLFUSEntry *e) |
| { |
| if (e == this->_compressed) { |
| if (this->_compressed->lru_link.next) { |
| this->_compressed = this->_compressed->lru_link.next; |
| } else { |
| this->_ncompressed--; |
| this->_compressed = this->_compressed->lru_link.prev; |
| } |
| } |
| } |
| |
| RamCacheCLFUSEntry * |
| RamCacheCLFUS::_destroy(RamCacheCLFUSEntry *e) |
| { |
| RamCacheCLFUSEntry *ret = e->hash_link.next; |
| this->_move_compressed(e); |
| this->_lru[e->flag_bits.lru].remove(e); |
| if (!e->flag_bits.lru) { |
| this->_objects--; |
| this->_bytes -= e->size + entry_overhead; |
| ts::Metrics::Gauge::decrement(cache_rsb.ram_cache_bytes, e->size); |
| ts::Metrics::Gauge::decrement(stripe->cache_vol->vol_rsb.ram_cache_bytes, e->size); |
| e->data = nullptr; |
| } else { |
| this->_history--; |
| } |
| uint32_t b = e->key.slice32(3) % this->_nbuckets; |
| this->_bucket[b].remove(e); |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " DESTROYED", e->key.slice32(3), e->auxkey); |
| THREAD_FREE(e, ramCacheCLFUSEntryAllocator, this_thread()); |
| return ret; |
| } |
| |
| void |
| RamCacheCLFUS::compress_entries(EThread *thread, int do_at_most) |
| { |
| if (!cache_config_ram_cache_compress) { |
| return; |
| } |
| ink_assert(stripe != nullptr); |
| #ifdef HAVE_ZSTD_H |
| if (cache_config_ram_cache_compress == CACHE_COMPRESSION_ZSTD && zstd_cctx() == nullptr) { |
| // The per-thread context failed to allocate, and that failure is sticky |
| // for the life of the thread this cache's compressor is pinned to, so no |
| // entry can be compressed on this pass or any later one. Skip the pass |
| // rather than walking every entry -- dropping and retaking the stripe lock |
| // and allocating a compressBound()-sized buffer for each -- only to fail |
| // every time. The entries are left untouched: this says nothing about the |
| // data, so they stay eligible. Deliberately not counted in |
| // ram_cache.compress.failure: that counter means entries the codec could |
| // not compress, and incrementing here once per pass would make it climb |
| // once a second per stripe for the life of the thread. The one-time |
| // Warning in zstd_cctx() is what reports this condition. |
| return; |
| } |
| #endif |
| MUTEX_TAKE_LOCK(stripe->mutex, thread); |
| if (!this->_compressed) { |
| this->_compressed = this->_lru[0].head; |
| this->_ncompressed = 0; |
| } |
| float target = (cache_config_ram_cache_compress_percent / 100.0) * this->_objects; |
| int n = 0; |
| char *b = nullptr, *bb = nullptr; |
| while (this->_compressed && target > this->_ncompressed) { |
| RamCacheCLFUSEntry *e = this->_compressed; |
| if (e->flag_bits.incompressible || e->flag_bits.compressed) { |
| goto Lcontinue; |
| } |
| n++; |
| if (do_at_most < n) { |
| break; |
| } |
| { |
| e->compressed_len = e->size; |
| uint32_t l = 0; |
| int ctype = cache_config_ram_cache_compress; |
| switch (ctype) { |
| default: |
| goto Lcontinue; |
| case CACHE_COMPRESSION_FASTLZ: |
| if (e->len < 16) { |
| // fastlz cannot compress inputs this small; decide while the entry |
| // is still lock-protected rather than in the unlocked region below. |
| e->flag_bits.incompressible = 1; |
| goto Lcontinue; |
| } |
| l = static_cast<uint32_t>(static_cast<double>(e->len) * 1.05 + 66); |
| break; |
| case CACHE_COMPRESSION_LIBZ: |
| l = static_cast<uint32_t>(compressBound(e->len)); |
| break; |
| #ifdef HAVE_LZMA_H |
| case CACHE_COMPRESSION_LIBLZMA: |
| l = static_cast<uint32_t>(lzma_stream_buffer_bound(e->len)); |
| break; |
| #endif |
| #ifdef HAVE_LZ4_H |
| case CACHE_COMPRESSION_LZ4: |
| l = static_cast<uint32_t>(LZ4_compressBound(e->len)); |
| break; |
| #endif |
| #ifdef HAVE_ZSTD_H |
| case CACHE_COMPRESSION_ZSTD: |
| l = static_cast<uint32_t>(ZSTD_compressBound(e->len)); |
| break; |
| #endif |
| } |
| // store transient data for lock release |
| Ptr<IOBufferData> edata = e->data; |
| uint32_t elen = e->len; |
| CryptoHash key = e->key; |
| MUTEX_UNTAKE_LOCK(stripe->mutex, thread); |
| b = static_cast<char *>(ats_malloc(l)); |
| bool failed = false; |
| // Distinguishes a transient, data-independent failure (the codec could |
| // not allocate its working memory) from the codec rejecting this data. |
| // Only the latter says anything about the entry. |
| bool transient = false; |
| switch (ctype) { |
| default: |
| // The bound switch above filtered unknown types; this is unreachable, |
| // but must not jump to Lfailed from this unlocked region. |
| failed = true; |
| break; |
| case CACHE_COMPRESSION_FASTLZ: |
| if ((l = fastlz_compress(edata->data(), elen, b)) <= 0) { |
| failed = true; |
| } |
| break; |
| case CACHE_COMPRESSION_LIBZ: { |
| uLongf ll = l; |
| int rc = compress(reinterpret_cast<Bytef *>(b), &ll, reinterpret_cast<Bytef *>(edata->data()), elen); |
| if (Z_OK != rc) { |
| failed = true; |
| transient = (rc == Z_MEM_ERROR); |
| } |
| l = static_cast<int>(ll); |
| break; |
| } |
| #ifdef HAVE_LZMA_H |
| case CACHE_COMPRESSION_LIBLZMA: { |
| size_t pos = 0, ll = l; |
| if (LZMA_OK != lzma_easy_buffer_encode(LZMA_PRESET_DEFAULT, LZMA_CHECK_NONE, nullptr, |
| reinterpret_cast<uint8_t *>(edata->data()), elen, reinterpret_cast<uint8_t *>(b), |
| &pos, ll)) { |
| failed = true; |
| } |
| l = static_cast<int>(pos); |
| break; |
| } |
| #endif |
| #ifdef HAVE_LZ4_H |
| case CACHE_COMPRESSION_LZ4: { |
| int ll = l; |
| if ((l = LZ4_compress_default(edata->data(), b, elen, ll)) == 0) { |
| failed = true; |
| } |
| break; |
| } |
| #endif |
| #ifdef HAVE_ZSTD_H |
| case CACHE_COMPRESSION_ZSTD: { |
| // The pass-level check above already proved this thread has a context, |
| // and the context is thread_local while the pass never changes thread. |
| ZSTD_CCtx *cctx = zstd_cctx(); |
| ink_assert(cctx != nullptr); |
| size_t zret = ZSTD_compress2(cctx, b, l, edata->data(), elen); |
| if (ZSTD_isError(zret)) { |
| failed = true; |
| // ZSTD_createCCtx() allocates only the context struct; the much |
| // larger working buffers are allocated on first use and grow with |
| // the input, so this is the realistic out-of-memory path. |
| transient = (ZSTD_getErrorCode(zret) == ZSTD_error_memory_allocation); |
| } else { |
| l = static_cast<uint32_t>(zret); |
| } |
| break; |
| } |
| #endif |
| } |
| MUTEX_TAKE_LOCK(stripe->mutex, thread); |
| // See if the entry is still around; it may have been freed while the |
| // lock was dropped, so this must be checked before anything writes to |
| // it (including the failure marking below). |
| { |
| uint32_t i = key.slice32(3) % this->_nbuckets; |
| RamCacheCLFUSEntry *ee = this->_bucket[i].head; |
| while (ee) { |
| if (ee->key == key && ee->data == edata) { |
| break; |
| } |
| ee = ee->hash_link.next; |
| } |
| if (!ee || ee != e) { |
| ats_free(b); |
| e = this->_compressed; |
| if (!e) { |
| // The cursor was invalidated while the lock was dropped. |
| break; |
| } |
| goto Lcontinue; |
| } |
| } |
| if (failed) { |
| ts::Metrics::Counter::increment(cache_rsb.ram_cache_compress_failures); |
| ts::Metrics::Counter::increment(stripe->cache_vol->vol_rsb.ram_cache_compress_failures); |
| if (transient) { |
| // An allocation failure inside the codec is not a property of the |
| // data, so do not record it as permanently incompressible; leave the |
| // entry eligible for a later pass. |
| ats_free(b); |
| goto Lcontinue; |
| } |
| goto Lfailed; |
| } |
| if (l > required_compression * e->len) { |
| e->flag_bits.incompressible = true; |
| } |
| if (l > required_shrink * e->size) { |
| goto Lfailed; |
| } |
| if (l < e->len) { |
| e->flag_bits.compressed = cache_config_ram_cache_compress; |
| bb = static_cast<char *>(ats_malloc(l)); |
| memcpy(bb, b, l); |
| ats_free(b); |
| e->compressed_len = l; |
| int64_t delta = (static_cast<int64_t>(l)) - static_cast<int64_t>(e->size); |
| this->_bytes += delta; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, delta); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, delta); |
| e->size = l; |
| } else { |
| ats_free(b); |
| e->flag_bits.compressed = 0; |
| bb = static_cast<char *>(ats_malloc(e->len)); |
| memcpy(bb, e->data->data(), e->len); |
| int64_t delta = (static_cast<int64_t>(e->len)) - static_cast<int64_t>(e->size); |
| this->_bytes += delta; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, delta); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, delta); |
| e->size = e->len; |
| l = e->len; |
| } |
| e->data = new_xmalloc_IOBufferData(bb, l); |
| e->data->_mem_type = DEFAULT_ALLOC; |
| check_accounting(this); |
| } |
| goto Lcontinue; |
| Lfailed: |
| ats_free(b); |
| e->flag_bits.incompressible = 1; |
| Lcontinue:; |
| DDbg(dbg_ctl_ram_cache, "compress %X %" PRId64 " %d %d %d %d %d", e->key.slice32(3), e->auxkey, e->flag_bits.incompressible, |
| e->flag_bits.compressed, e->len, e->compressed_len, this->_ncompressed); |
| if (!e->lru_link.next) { |
| break; |
| } |
| this->_compressed = e->lru_link.next; |
| this->_ncompressed++; |
| } |
| MUTEX_UNTAKE_LOCK(stripe->mutex, thread); |
| return; |
| } |
| |
| void |
| RamCacheCLFUS::_requeue_victims(Que(RamCacheCLFUSEntry, lru_link) & victims) |
| { |
| RamCacheCLFUSEntry *victim = nullptr; |
| while ((victim = victims.dequeue())) { |
| this->_bytes += victim->size + entry_overhead; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, victim->size); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, victim->size); |
| victim->hits = requeue_hits(victim->hits); |
| this->_lru[0].enqueue(victim); |
| } |
| } |
| |
| int |
| RamCacheCLFUS::put(CryptoHash *key, IOBufferData *data, uint32_t len, bool copy, uint64_t auxkey) |
| { |
| if (!this->_max_bytes) { |
| return 0; |
| } |
| uint32_t i = key->slice32(3) % this->_nbuckets; |
| RamCacheCLFUSEntry *e = this->_bucket[i].head; |
| uint32_t size = copy ? len : data->block_size(); |
| double victim_value = 0; |
| while (e) { |
| if (e->key == *key) { |
| if (e->auxkey == auxkey) { |
| break; |
| } else { |
| e = this->_destroy(e); // discard when aux keys conflict |
| continue; |
| } |
| } |
| e = e->hash_link.next; |
| } |
| if (e) { |
| e->hits++; |
| if (!e->flag_bits.lru) { // already in cache |
| this->_move_compressed(e); |
| this->_lru[e->flag_bits.lru].remove(e); |
| this->_lru[e->flag_bits.lru].enqueue(e); |
| if (copy && e->flag_bits.copy && !e->flag_bits.compressed) { |
| // Already an uncompressed private copy of this object: get() never |
| // exposes that buffer, so re-copying it (two requests that both missed |
| // and both read the object from disk) changes nothing. Everything |
| // below is a no-op for such an entry apart from the allocation and |
| // memcpy -- size is already len, so delta is 0 -- except when the |
| // entry is compressed, where the swap is what decompresses it and |
| // skipping it would leave compressed bytes behind a cleared flag. |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d HIT (private, unchanged)", key->slice32(3), auxkey, e->size); |
| return 1; |
| } |
| int64_t delta = (static_cast<int64_t>(size)) - static_cast<int64_t>(e->size); |
| this->_bytes += delta; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, delta); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, delta); |
| if (!copy) { |
| e->size = size; |
| e->data = data; |
| } else { |
| e->data = copy_data_in(data, len); |
| e->size = size; |
| } |
| e->len = len; // get() and the compressor read e->len bytes out of e->data |
| check_accounting(this); |
| e->flag_bits.copy = copy; |
| e->flag_bits.compressed = 0; |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d HIT", key->slice32(3), auxkey, e->size); |
| return 1; |
| } else { |
| this->_lru[1].remove(e); |
| if (cache_value(e) < this->_average_value) { |
| this->_lru[1].enqueue(e); |
| return 0; |
| } |
| } |
| } |
| Que(RamCacheCLFUSEntry, lru_link) victims; |
| RamCacheCLFUSEntry *victim = nullptr; |
| int requeue_count = requeue_limit; |
| if (!this->_lru[1].head) { // initial fill |
| if (this->_bytes + size <= this->_max_bytes) { |
| goto Linsert; |
| } |
| } |
| if (!e && cache_config_ram_cache_use_seen_filter) { |
| uint32_t s = key->slice32(3) % bucket_sizes[this->_ibuckets]; |
| uint16_t k = key->slice32(3) >> 16; |
| uint16_t kk = this->_seen[s]; |
| this->_seen[s] = k; |
| if (this->_history >= this->_objects && kk != k) { |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d UNSEEN", key->slice32(3), auxkey, size); |
| return 0; |
| } |
| } |
| while (true) { |
| victim = this->_lru[0].dequeue(); |
| if (!victim) { |
| if (this->_bytes + size <= this->_max_bytes) { |
| goto Linsert; |
| } |
| if (e) { |
| this->_lru[1].enqueue(e); |
| } |
| this->_requeue_victims(victims); |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " NO VICTIM", key->slice32(3), auxkey); |
| return 0; |
| } |
| this->_average_value = (cache_value(victim) + (this->_average_value * (average_value_over - 1))) / average_value_over; |
| if (cache_value(victim) > this->_average_value && requeue_count-- > 0) { |
| this->_lru[0].enqueue(victim); |
| continue; |
| } |
| this->_bytes -= victim->size + entry_overhead; |
| ts::Metrics::Gauge::decrement(cache_rsb.ram_cache_bytes, victim->size); |
| ts::Metrics::Gauge::decrement(stripe->cache_vol->vol_rsb.ram_cache_bytes, victim->size); |
| victims.enqueue(victim); |
| if (victim == this->_compressed) { |
| this->_compressed = nullptr; |
| } else { |
| this->_ncompressed--; |
| } |
| victim_value += cache_value(victim); |
| this->_tick(); |
| if (!e) { |
| goto Lhistory; |
| } else { // e from history |
| DDbg(dbg_ctl_ram_cache_compare, "put %f %f", victim_value, cache_value(e)); |
| if (this->_bytes + victim->size + size > this->_max_bytes && victim_value > cache_value(e)) { |
| this->_requeue_victims(victims); |
| this->_lru[1].enqueue(e); |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d INC %" PRId64 " HISTORY", key->slice32(3), auxkey, e->size, e->hits); |
| return 0; |
| } |
| } |
| if (this->_bytes + size <= this->_max_bytes) { |
| goto Linsert; |
| } |
| } |
| Linsert: |
| while ((victim = victims.dequeue())) { |
| if (this->_bytes + size + victim->size <= this->_max_bytes) { |
| this->_bytes += victim->size + entry_overhead; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, victim->size); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, victim->size); |
| victim->hits = requeue_hits(victim->hits); |
| this->_lru[0].enqueue(victim); |
| } else { |
| this->_victimize(victim); |
| } |
| } |
| if (e) { |
| this->_history--; // move from history |
| } else { |
| e = THREAD_ALLOC(ramCacheCLFUSEntryAllocator, this_ethread()); |
| e->key = *key; |
| e->auxkey = auxkey; |
| e->hits = 1; |
| this->_bucket[i].push(e); |
| if (this->_objects > this->_nbuckets) { |
| ++this->_ibuckets; |
| this->_resize_hashtable(); |
| } |
| } |
| check_accounting(this); |
| e->flags = 0; |
| if (!copy) { |
| e->data = data; |
| } else { |
| e->data = copy_data_in(data, len); |
| } |
| e->flag_bits.copy = copy; |
| this->_bytes += size + entry_overhead; |
| ts::Metrics::Gauge::increment(cache_rsb.ram_cache_bytes, size); |
| ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.ram_cache_bytes, size); |
| e->size = size; |
| this->_objects++; |
| this->_lru[0].enqueue(e); |
| e->len = len; |
| check_accounting(this); |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " size %d INSERTED", key->slice32(3), auxkey, e->size); |
| return 1; |
| Lhistory: |
| this->_requeue_victims(victims); |
| check_accounting(this); |
| e = THREAD_ALLOC(ramCacheCLFUSEntryAllocator, this_ethread()); |
| e->key = *key; |
| e->auxkey = auxkey; |
| e->hits = 1; |
| e->size = data->block_size(); |
| e->flags = 0; |
| this->_bucket[i].push(e); |
| e->flag_bits.lru = 1; |
| this->_lru[1].enqueue(e); |
| this->_history++; |
| DDbg(dbg_ctl_ram_cache, "put %X %" PRId64 " HISTORY", key->slice32(3), auxkey); |
| return 0; |
| } |
| |
| int |
| RamCacheCLFUS::fixup(const CryptoHash *key, uint64_t old_auxkey, uint64_t new_auxkey) |
| { |
| if (!this->_max_bytes) { |
| return 0; |
| } |
| uint32_t i = key->slice32(3) % this->_nbuckets; |
| RamCacheCLFUSEntry *e = this->_bucket[i].head; |
| while (e) { |
| if (e->key == *key && e->auxkey == old_auxkey) { |
| e->auxkey = new_auxkey; |
| return 1; |
| } |
| e = e->hash_link.next; |
| } |
| return 0; |
| } |
| |
| RamCache * |
| new_RamCacheCLFUS() |
| { |
| RamCacheCLFUS *r = new RamCacheCLFUS; |
| return r; |
| } |