From facc790d70c63ae68c8743d6be9fdd1218cc5c7a Mon Sep 17 00:00:00 2001 From: Rayan-and-beyond <263488867+Rayan-and-beyond@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:55:43 +0000 Subject: [PATCH 1/2] fix(cpp): complete and adopt bounded LRU cache --- cpp/src/common/cache/lru_cache.h | 123 ++++++++++++++---------- cpp/src/file/tsfile_io_reader.cc | 77 ++++++--------- cpp/src/file/tsfile_io_reader.h | 36 +++++-- cpp/src/reader/meta_data_querier.cc | 13 --- cpp/src/reader/meta_data_querier.h | 10 -- cpp/test/common/cache/lru_cache_test.cc | 120 +++++++++++++++++++++++ cpp/test/reader/tsfile_reader_test.cc | 59 ++++++++++++ 7 files changed, 308 insertions(+), 130 deletions(-) create mode 100644 cpp/test/common/cache/lru_cache_test.cc diff --git a/cpp/src/common/cache/lru_cache.h b/cpp/src/common/cache/lru_cache.h index 10786841d..047f5c4e5 100644 --- a/cpp/src/common/cache/lru_cache.h +++ b/cpp/src/common/cache/lru_cache.h @@ -21,10 +21,11 @@ #define COMMON_CACHE_LRU_CACHE_H #include +#include #include +#include #include - -#include "utils/errno_define.h" +#include namespace common { @@ -38,10 +39,20 @@ struct KeyValuePair { }; /** - * The LRU Cache class templated by - * Key - key type - * Value - value type - * MapType - an associative container like std::unordered_map + * Least-recently-used cache. + * + * The cache is not internally synchronized. A caller that shares one cache + * across threads must serialize every operation, including the full lifetime + * of a pointer/reference returned by getPtr(), getRef(), or tryGetRef(). + * + * maxSize is the normal retained-entry limit. elasticity allows the cache to + * grow temporarily to maxSize + elasticity; the insertion that would exceed + * that hard limit prunes least-recently-used entries back to maxSize. A + * maxSize of 0 means unbounded and elasticity is ignored. + * + * Pointers/references returned by non-copying lookups remain valid until that + * entry is updated, removed, or evicted, or until clear()/destruction. Any + * insertion can evict an entry when the cache is bounded. */ template node_type; typedef std::list> list_type; typedef Map map_type; - /** - * the maxSize is the soft limit of entries and (maxSize + elasticity) is - * the hard limit the cache is allowed to grow till (maxSize + elasticity) - * and is pruned back to maxSize entries set maxSize = 0 for an unbounded - * cache (but in that case, you're better off using a std::unordered_map - * directly anyway! :) - */ + explicit Cache(size_t maxSize = 64, size_t elasticity = 10) : maxSize_(maxSize), elasticity_(elasticity) {} virtual ~Cache() = default; + size_t size() const { return cache_.size(); } bool empty() const { return cache_.empty(); } + void clear() { cache_.clear(); entries_.clear(); } + void insert(const Key& k, Value v) { const auto iter = cache_.find(k); if (iter != cache_.end()) { - iter->second->value = v; + iter->second->value = std::move(v); entries_.splice(entries_.begin(), entries_, iter->second); return; } @@ -79,39 +87,56 @@ class Cache { cache_[k] = entries_.begin(); prune(); } - /** - for backward compatibility. redirects to tryGetCopy() - */ - bool tryGet(const Key& kIn, Value& vOut) { return tryGetCopy(kIn, vOut); } - bool tryGetCopy(const Key& kIn, Value& vOut) { - Value tmp; - if (!tryGetRef_nolock(kIn, tmp)) { + /** Backward-compatible copying lookup. */ + bool tryGet(const Key& k, Value& vOut) { return tryGetCopy(k, vOut); } + + bool tryGetCopy(const Key& k, Value& vOut) { + const Value* value = getPtr(k); + if (value == nullptr) { return false; } - vOut = tmp; + vOut = *value; return true; } - bool tryGetRef(const Key& kIn, Value& vOut) { - return tryGetRef_nolock(kIn, vOut); - } /** - * The const reference returned here is only - * guaranteed to be valid till the next insert/delete - * in multi-threaded apps use getCopy() to be threadsafe + * Non-copying lookup. On success vOut points at the cached value and the + * entry is promoted to most-recently-used. */ - const Value& getRef(const Key& k) { return get_nolock(k); } + bool tryGetRef(const Key& k, const Value*& vOut) { + vOut = getPtr(k); + return vOut != nullptr; + } /** - added for backward compatibility + * Legacy overload retained for source compatibility. Despite its historic + * name it copies; new code should use the pointer overload or getPtr(). */ + bool tryGetRef(const Key& k, Value& vOut) { return tryGetCopy(k, vOut); } + + /** Returns nullptr on miss and promotes a hit without copying the value. */ + const Value* getPtr(const Key& k) { + const auto iter = cache_.find(k); + if (iter == cache_.end()) { + return nullptr; + } + entries_.splice(entries_.begin(), entries_, iter->second); + return &iter->second->value; + } + + const Value& getRef(const Key& k) { + const Value* value = getPtr(k); + if (value == nullptr) { + throw std::out_of_range("LRU cache key not found"); + } + return *value; + } + + /** Backward-compatible copying lookup. */ Value get(const Key& k) { return getCopy(k); } - /** - * returns a copy of the stored object (if found) - * safe to use/recommended in multi-threaded apps - */ - Value getCopy(const Key& k) { return get_nolock(k); } + + Value getCopy(const Key& k) { return getRef(k); } bool remove(const Key& k) { auto iter = cache_.find(k); @@ -122,29 +147,30 @@ class Cache { cache_.erase(iter); return true; } + bool contains(const Key& k) const { return cache_.find(k) != cache_.end(); } size_t getMaxSize() const { return maxSize_; } size_t getElasticity() const { return elasticity_; } - size_t getMaxAllowedSize() const { return maxSize_ + elasticity_; } + size_t getMaxAllowedSize() const { + if (maxSize_ == 0) { + return 0; + } + const size_t max = std::numeric_limits::max(); + if (elasticity_ > max - maxSize_) { + return max; + } + return maxSize_ + elasticity_; + } + template void cwalk(F& f) const { std::for_each(entries_.begin(), entries_.end(), f); } protected: - bool tryGetRef_nolock(const Key& kIn, Value& vOut) { - const auto iter = cache_.find(kIn); - if (iter == cache_.end()) { - return false; - } - entries_.splice(entries_.begin(), entries_, iter->second); - vOut = iter->second->value; - return true; - } size_t prune() { - size_t maxAllowed = maxSize_ + elasticity_; - if (maxSize_ == 0 || cache_.size() < maxAllowed) { + if (maxSize_ == 0 || cache_.size() <= getMaxAllowedSize()) { return 0; } size_t count = 0; @@ -157,7 +183,6 @@ class Cache { } private: - // Disallow copying. Cache(const Cache&) = delete; Cache& operator=(const Cache&) = delete; @@ -168,4 +193,4 @@ class Cache { }; } // namespace common -#endif // COMMON_CACHE_LRU_CACHE_H \ No newline at end of file +#endif // COMMON_CACHE_LRU_CACHE_H diff --git a/cpp/src/file/tsfile_io_reader.cc b/cpp/src/file/tsfile_io_reader.cc index 9b4d8aef4..500fc9c64 100644 --- a/cpp/src/file/tsfile_io_reader.cc +++ b/cpp/src/file/tsfile_io_reader.cc @@ -56,8 +56,10 @@ void TsFileIOReader::reset() { } read_file_ = nullptr; tsfile_meta_page_arena_.destroy(); - device_node_cache_.clear(); - device_node_cache_pa_.destroy(); + { + std::lock_guard lk(device_node_cache_mu_); + device_node_cache_.clear(); + } tsfile_meta_ready_ = false; } } @@ -623,22 +625,18 @@ std::string TsFileIOReader::device_node_cache_key( int TsFileIOReader::get_cached_device_node(std::shared_ptr device_id, common::PageArena& pa, CachedDeviceNode& out) { + (void)pa; std::string dev_name = device_node_cache_key(device_id); { std::lock_guard lk(device_node_cache_mu_); - auto it = device_node_cache_.find(dev_name); - if (it != device_node_cache_.end()) { - out = it->second; + if (device_node_cache_.tryGetCopy(dev_name, out)) { return E_OK; } } - // Read the device meta index outside the lock — load_device_index_entry() - // and the file read can block on I/O, and we don't want to serialize all - // concurrent first-time lookups behind one slow disk fetch. Two callers - // racing on the same missing device may both do the read; that's wasted - // work but not corruption — the second insert is dropped below. + // Read the device meta index outside the lock. Concurrent misses may do + // duplicate I/O, but the cache is re-checked before insertion below. int ret = E_OK; std::shared_ptr device_index_entry; int64_t device_ie_end_offset = 0; @@ -652,10 +650,6 @@ int TsFileIOReader::get_cached_device_node(std::shared_ptr device_id, end_offset = device_ie_end_offset; ASSERT(start_offset < end_offset); const int64_t read_size_i64 = end_offset - start_offset; - // read_file_->read() takes int32_t; a meta index node larger than 2 GiB - // is implausible but explicitly reject it instead of silently truncating - // the read length and corrupting the parse. Distinguish the two cases: - // an inverted/empty range is corruption, an oversized one is an overflow. if (read_size_i64 <= 0) { return E_TSFILE_CORRUPTED; } @@ -665,14 +659,6 @@ int TsFileIOReader::get_cached_device_node(std::shared_ptr device_id, const int32_t read_size = static_cast(read_size_i64); int32_t ret_read_len = 0; - // Read into a heap-owned buffer outside the lock. The previous - // implementation allocated data_buf inside device_node_cache_pa_ before - // the read happened — every failed read or parse left that allocation - // pinned forever in the shared arena, and repeated disk errors on the - // same device let a long-lived reader grow it without bound. Using a - // unique_ptr here means the read buffer is released on every failure - // path, and only the small MetaIndexNode allocations inside the lock - // share the arena. std::unique_ptr data_buf(new (std::nothrow) char[read_size]); if (data_buf == nullptr) { return E_OOM; @@ -684,37 +670,32 @@ int TsFileIOReader::get_cached_device_node(std::shared_ptr device_id, return E_FILE_READ_ERR; } - CachedDeviceNode cached; + // Give every cached node its own arena. The CachedDeviceNode keeps this + // arena alive for users that copied the entry, while LRU eviction releases + // the cache's ownership and therefore reclaims unused metadata pages. + CachedDeviceNode candidate; + candidate.arena = std::make_shared(); + candidate.arena->init(512, common::MOD_TSFILE_READER); + void* m_idx_node_buf = candidate.arena->alloc(sizeof(MetaIndexNode)); + if (IS_NULL(m_idx_node_buf)) { + return E_OOM; + } + auto* top_node_ptr = new (m_idx_node_buf) MetaIndexNode(candidate.arena.get()); + candidate.top_node = std::shared_ptr( + top_node_ptr, MetaIndexNode::self_deleter); + if (RET_FAIL(candidate.top_node->deserialize_from(data_buf.get(), read_size))) { + return ret; + } + candidate.is_aligned = is_aligned_device(candidate.top_node); + { - // Allocations into device_node_cache_pa_ and the map insert must be - // serialized — PageArena is not thread-safe, and unordered_map's - // rehash invalidates concurrent lookups. std::lock_guard lk(device_node_cache_mu_); - // Re-check: another thread may have populated the entry while we - // were doing I/O. - auto it = device_node_cache_.find(dev_name); - if (it != device_node_cache_.end()) { - out = it->second; + if (device_node_cache_.tryGetCopy(dev_name, out)) { return E_OK; } - - void* m_idx_node_buf = - device_node_cache_pa_.alloc(sizeof(MetaIndexNode)); - if (IS_NULL(m_idx_node_buf)) { - return E_OOM; - } - auto* top_node_ptr = - new (m_idx_node_buf) MetaIndexNode(&device_node_cache_pa_); - auto top_node = std::shared_ptr( - top_node_ptr, MetaIndexNode::self_deleter); - if (RET_FAIL(top_node->deserialize_from(data_buf.get(), read_size))) { - return ret; - } - cached.top_node = top_node; - cached.is_aligned = is_aligned_device(top_node); - device_node_cache_.emplace(std::move(dev_name), cached); + device_node_cache_.insert(dev_name, candidate); + out = candidate; } - out = cached; return E_OK; } diff --git a/cpp/src/file/tsfile_io_reader.h b/cpp/src/file/tsfile_io_reader.h index 2f98a753c..43a5d8678 100644 --- a/cpp/src/file/tsfile_io_reader.h +++ b/cpp/src/file/tsfile_io_reader.h @@ -25,6 +25,7 @@ #include #include +#include "common/cache/lru_cache.h" #include "common/tsblock/tsblock.h" #include "file/random_access_read_file.h" #include "reader/chunk_reader.h" @@ -50,9 +51,9 @@ class TsFileIOReader { tsfile_meta_page_arena_(), tsfile_meta_(&tsfile_meta_page_arena_), tsfile_meta_ready_(false), - read_file_created_(false) { + read_file_created_(false), + device_node_cache_(DEVICE_NODE_CACHE_CAPACITY, 0) { tsfile_meta_page_arena_.init(512, common::MOD_TSFILE_READER); - device_node_cache_pa_.init(512, common::MOD_TSFILE_READER); } // Free only the local source we own (created by init(const std::string&)). @@ -222,10 +223,15 @@ class TsFileIOReader { common::PageArena& pa); struct CachedDeviceNode { + // Declared before top_node so top_node is destroyed first. MetaIndexNode + // and its children live in this arena. + std::shared_ptr arena; std::shared_ptr top_node; - bool is_aligned; + bool is_aligned = false; }; + static constexpr size_t DEVICE_NODE_CACHE_CAPACITY = 64; + // Returns E_OK on hit (out is filled), or an error code on miss / load // failure (E_DEVICE_NOT_EXIST when the device is absent, the propagated // error otherwise). Copying into out keeps the caller safe from rehash / @@ -233,6 +239,17 @@ class TsFileIOReader { int get_cached_device_node(std::shared_ptr device_id, common::PageArena& pa, CachedDeviceNode& out); +#ifdef ENABLE_TEST + public: + size_t TEST_device_node_cache_size() const { + std::lock_guard lk(device_node_cache_mu_); + return device_node_cache_.size(); + } + static size_t TEST_device_node_cache_capacity() { + return DEVICE_NODE_CACHE_CAPACITY; + } +#endif + private: // Build a collision-free key for device_node_cache_. get_device_name() // renders a null tag segment as the literal "null", so a device with a @@ -248,13 +265,12 @@ class TsFileIOReader { TsFileMeta tsfile_meta_; bool tsfile_meta_ready_; bool read_file_created_; - // Cache: device_name → deserialized measurement MetaIndexNode. - // Guarded by device_node_cache_mu_ — multiple SSIs and Result Sets can - // hit the cache concurrently on the same reader, and an unsynchronized - // unordered_map insert would race with a parallel lookup (rehash, - // bucket-list rewrite) and with the underlying PageArena allocation. - common::PageArena device_node_cache_pa_; - std::unordered_map device_node_cache_; + // Bounded LRU: device key -> deserialized measurement MetaIndexNode. Each + // entry owns its arena so eviction releases the retained metadata pages. + // Cache operations are guarded because common::Cache is intentionally not + // internally synchronized. Callers copy CachedDeviceNode while locked, so + // shared ownership keeps an entry alive after a concurrent eviction. + common::Cache device_node_cache_; mutable std::mutex device_node_cache_mu_; }; diff --git a/cpp/src/reader/meta_data_querier.cc b/cpp/src/reader/meta_data_querier.cc index 0accbdde9..3993bbb15 100644 --- a/cpp/src/reader/meta_data_querier.cc +++ b/cpp/src/reader/meta_data_querier.cc @@ -26,25 +26,12 @@ namespace storage { MetadataQuerier::MetadataQuerier(TsFileIOReader* tsfile_io_reader) : io_reader_(tsfile_io_reader) { file_metadata_ = io_reader_->get_tsfile_meta(); - device_chunk_meta_cache_ = std::unique_ptr< - common::Cache>, std::mutex>>( - new common::Cache>, - std::mutex>(CACHED_ENTRY_NUMBER, - CACHED_ENTRY_NUMBER / 10)); } MetadataQuerier::~MetadataQuerier() {} std::vector> MetadataQuerier::get_chunk_metadata_list(const Path& path) const { - // std::vector> chunk_meta_list; - // if (device_chunk_meta_cache_->tryGet(path.device_, chunk_meta_list)) { - // return chunk_meta_list; - // } else { - // io_reader_->get_chunk_metadata_list(path.device_, path.measurement_, - // chunk_meta_list); - // } // return io_reader_->get_chunk_metadata_list(path); ASSERT(false); return {}; diff --git a/cpp/src/reader/meta_data_querier.h b/cpp/src/reader/meta_data_querier.h index be575323f..8bb98b41a 100644 --- a/cpp/src/reader/meta_data_querier.h +++ b/cpp/src/reader/meta_data_querier.h @@ -20,9 +20,6 @@ #ifndef READER_META_DATA_QUERIER_H #define READER_META_DATA_QUERIER_H -#include - -#include "common/cache/lru_cache.h" #include "common/device_id.h" #include "device_meta_iterator.h" #include "file/tsfile_io_reader.h" @@ -32,8 +29,6 @@ namespace storage { class MetadataQuerier : public IMetadataQuerier { public: - static constexpr int CACHED_ENTRY_NUMBER = 1000; - enum class LocateStatus { BEFORE, IN, AFTER }; explicit MetadataQuerier(TsFileIOReader* tsfile_io_reader); @@ -71,11 +66,6 @@ class MetadataQuerier : public IMetadataQuerier { private: TsFileIOReader* io_reader_; TsFileMeta* file_metadata_; - std::unique_ptr< - common::Cache*/ - std::vector>, std::mutex>> - device_chunk_meta_cache_; - int load_chunk_meta(const std::pair& key, std::vector& chunk_meta_list); diff --git a/cpp/test/common/cache/lru_cache_test.cc b/cpp/test/common/cache/lru_cache_test.cc new file mode 100644 index 000000000..b0777a9d9 --- /dev/null +++ b/cpp/test/common/cache/lru_cache_test.cc @@ -0,0 +1,120 @@ +/* + * 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 "common/cache/lru_cache.h" + +#include + +#include +#include + +namespace common { + +TEST(LruCacheTest, StrictCapacityPromotesAndEvicts) { + Cache cache(2, 0); + cache.insert(1, "one"); + cache.insert(2, "two"); + + const std::string* one = cache.getPtr(1); + ASSERT_NE(one, nullptr); + EXPECT_EQ(*one, "one"); + + cache.insert(3, "three"); + EXPECT_TRUE(cache.contains(1)); + EXPECT_FALSE(cache.contains(2)); + EXPECT_TRUE(cache.contains(3)); + EXPECT_EQ(cache.size(), 2u); +} + +TEST(LruCacheTest, UpdateReplacesValueAndPromotes) { + Cache cache(2, 0); + cache.insert(1, "one"); + cache.insert(2, "two"); + cache.insert(1, "updated"); + cache.insert(3, "three"); + + EXPECT_EQ(cache.getRef(1), "updated"); + EXPECT_FALSE(cache.contains(2)); +} + +TEST(LruCacheTest, ElasticityPrunesAfterHardLimitIsExceeded) { + Cache cache(2, 2); + for (int i = 1; i <= 4; ++i) { + cache.insert(i, i); + } + EXPECT_EQ(cache.size(), 4u); + EXPECT_EQ(cache.getMaxAllowedSize(), 4u); + + cache.insert(5, 5); + EXPECT_EQ(cache.size(), 2u); + EXPECT_TRUE(cache.contains(5)); + EXPECT_TRUE(cache.contains(4)); + EXPECT_FALSE(cache.contains(3)); +} + +TEST(LruCacheTest, CopyAndReferenceLookupsAreExplicit) { + Cache cache(2, 0); + cache.insert(1, "one"); + + std::string copied; + EXPECT_TRUE(cache.tryGetCopy(1, copied)); + EXPECT_EQ(copied, "one"); + + const std::string* ref = nullptr; + EXPECT_TRUE(cache.tryGetRef(1, ref)); + ASSERT_NE(ref, nullptr); + EXPECT_EQ(*ref, "one"); + EXPECT_EQ(cache.getCopy(1), "one"); + EXPECT_THROW(cache.getRef(99), std::out_of_range); +} + +TEST(LruCacheTest, NonCopyingLookupSupportsMoveOnlyValues) { + Cache> cache(1, 0); + cache.insert(1, std::unique_ptr(new int(7))); + + const std::unique_ptr* value = nullptr; + ASSERT_TRUE(cache.tryGetRef(1, value)); + ASSERT_NE(value, nullptr); + ASSERT_NE(value->get(), nullptr); + EXPECT_EQ(**value, 7); + + cache.insert(1, std::unique_ptr(new int(9))); + value = cache.getPtr(1); + ASSERT_NE(value, nullptr); + EXPECT_EQ(**value, 9); +} + +TEST(LruCacheTest, RemoveClearAndUnboundedMode) { + Cache bounded(2, 0); + bounded.insert(1, 1); + EXPECT_TRUE(bounded.remove(1)); + EXPECT_FALSE(bounded.remove(1)); + bounded.insert(2, 2); + bounded.clear(); + EXPECT_TRUE(bounded.empty()); + + Cache unbounded(0, 100); + for (int i = 0; i < 200; ++i) { + unbounded.insert(i, i); + } + EXPECT_EQ(unbounded.size(), 200u); + EXPECT_EQ(unbounded.getMaxAllowedSize(), 0u); +} + +} // namespace common diff --git a/cpp/test/reader/tsfile_reader_test.cc b/cpp/test/reader/tsfile_reader_test.cc index 314d69c6d..1cfbc898b 100644 --- a/cpp/test/reader/tsfile_reader_test.cc +++ b/cpp/test/reader/tsfile_reader_test.cc @@ -493,6 +493,65 @@ TEST_F(TsFileReaderTest, ReadsThroughRandomAccessReadFile) { reader.close(); } +TEST_F(TsFileReaderTest, DeviceNodeCacheIsBoundedAndReloadsEvictedDevices) { + const size_t capacity = TsFileIOReader::TEST_device_node_cache_capacity(); + const size_t device_count = capacity + 8; + const std::string measurement = "value"; + + for (size_t i = 0; i < device_count; ++i) { + const std::string device = "root.cache.d" + std::to_string(i); + ASSERT_EQ(tsfile_writer_->register_timeseries( + device, MeasurementSchema(measurement, INT32, PLAIN, + UNCOMPRESSED)), + E_OK); + TsRecord record(static_cast(i), device); + record.add_point(measurement, static_cast(i)); + ASSERT_EQ(tsfile_writer_->write_record(record), E_OK); + } + ASSERT_EQ(tsfile_writer_->flush(), E_OK); + ASSERT_EQ(tsfile_writer_->close(), E_OK); + + TsFileIOReader io_reader; + ASSERT_EQ(io_reader.init(file_name_), E_OK); + PageArena pa; + pa.init(512, MOD_TSFILE_READER); + + auto load_device = [&](size_t i) { + const std::string device = "root.cache.d" + std::to_string(i); + auto device_id = std::make_shared(device); + TsFileSeriesScanIterator* ssi = nullptr; + ASSERT_EQ(io_reader.alloc_ssi(device_id, measurement, ssi, pa), E_OK); + ASSERT_NE(ssi, nullptr); + io_reader.revert_ssi(ssi); + pa.reset(); + EXPECT_LE(io_reader.TEST_device_node_cache_size(), capacity); + }; + + for (size_t i = 0; i < capacity; ++i) { + load_device(i); + } + ASSERT_EQ(io_reader.TEST_device_node_cache_size(), capacity); + const int64_t memory_at_capacity = + ModStat::get_instance().get_stat(MOD_TSFILE_READER); + ASSERT_GT(memory_at_capacity, 0); + + // Filling beyond capacity must evict old nodes and reclaim their per-entry + // arenas rather than retaining more reader metadata memory. + for (size_t i = capacity; i < device_count; ++i) { + load_device(i); + } + EXPECT_EQ(io_reader.TEST_device_node_cache_size(), capacity); + EXPECT_EQ(ModStat::get_instance().get_stat(MOD_TSFILE_READER), + memory_at_capacity); + + // d0 was the least-recently-used entry and has been evicted. Loading it + // again must still work, evict another entry, and keep memory flat. + load_device(0); + EXPECT_EQ(io_reader.TEST_device_node_cache_size(), capacity); + EXPECT_EQ(ModStat::get_instance().get_stat(MOD_TSFILE_READER), + memory_at_capacity); +} + TEST_F(TsFileReaderTest, ResultSetMetadata) { std::string device_path = "device1"; std::string measurement_name = "temperature"; From 38d1a87d351fa29af473a2e2f8864c492739c7d5 Mon Sep 17 00:00:00 2001 From: Rayan-and-beyond <263488867+Rayan-and-beyond@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:34:48 +0000 Subject: [PATCH 2/2] style(cpp): apply spotless formatting --- cpp/src/file/tsfile_io_reader.cc | 6 ++++-- cpp/src/file/tsfile_io_reader.h | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cpp/src/file/tsfile_io_reader.cc b/cpp/src/file/tsfile_io_reader.cc index 500fc9c64..0859dd2fb 100644 --- a/cpp/src/file/tsfile_io_reader.cc +++ b/cpp/src/file/tsfile_io_reader.cc @@ -680,10 +680,12 @@ int TsFileIOReader::get_cached_device_node(std::shared_ptr device_id, if (IS_NULL(m_idx_node_buf)) { return E_OOM; } - auto* top_node_ptr = new (m_idx_node_buf) MetaIndexNode(candidate.arena.get()); + auto* top_node_ptr = + new (m_idx_node_buf) MetaIndexNode(candidate.arena.get()); candidate.top_node = std::shared_ptr( top_node_ptr, MetaIndexNode::self_deleter); - if (RET_FAIL(candidate.top_node->deserialize_from(data_buf.get(), read_size))) { + if (RET_FAIL( + candidate.top_node->deserialize_from(data_buf.get(), read_size))) { return ret; } candidate.is_aligned = is_aligned_device(candidate.top_node); diff --git a/cpp/src/file/tsfile_io_reader.h b/cpp/src/file/tsfile_io_reader.h index 43a5d8678..7955f032a 100644 --- a/cpp/src/file/tsfile_io_reader.h +++ b/cpp/src/file/tsfile_io_reader.h @@ -223,8 +223,8 @@ class TsFileIOReader { common::PageArena& pa); struct CachedDeviceNode { - // Declared before top_node so top_node is destroyed first. MetaIndexNode - // and its children live in this arena. + // Declared before top_node so top_node is destroyed first. + // MetaIndexNode and its children live in this arena. std::shared_ptr arena; std::shared_ptr top_node; bool is_aligned = false;