This is an automated email from the ASF dual-hosted git repository.

liaoxin01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 81484ac7bcc [fix](file cache) Keep only weak references to file blocks 
in CachedRemoteFileReader (#69705)
81484ac7bcc is described below

commit 81484ac7bcc6db9a03529b09afd87994f0f1e92f
Author: Xin Liao <[email protected]>
AuthorDate: Wed Oct 28 18:01:26 2026 +0800

    [fix](file cache) Keep only weak references to file blocks in 
CachedRemoteFileReader (#69705)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #68348
    
    Problem Summary:
    
    `CachedRemoteFileReader` keeps the downloaded blocks of its file in
    `_cache_file_readers` so later reads can skip `get_or_set()`. Those were
    strong references, so `FileBlockCell::releasable()` had to discount them
    with a single `_owned_by_cached_reader` flag on the block:
    
    ```cpp
    use_count == 1 || (use_count == 2 && _owned_by_cached_reader)
    ```
    
    A flag cannot count readers, and the same segment file is often opened
    by several readers at once (the one in `SegmentCache`, readers that
    bypass it, a concurrent cache miss that loads a second instance). That
    breaks `releasable()` in both directions:
    
    1. Blocks stay busy forever. With two readers `use_count` is 3. When one
    of them is destroyed it clears the flag although the other still holds
    the block, leaving `use_count == 2` with the flag off. Either way a sync
    `clear` (and LRU eviction) can only mark the block deleting, and
    #68348's background recycle cannot reclaim it either, because it relies
    on `releasable()` too. The block, its file and its bytes in
    `ttl_cache_size` stay until the long-lived reader goes away, which for a
    reader in `SegmentCache` means until the tablet is cleaned up. In a
    cloud regression run, 899 blocks (929,565,612 bytes of TTL cache)
    survived every sync clear for over 12 minutes, failing
    `create_table_as_select`, `alter_ttl_1`, `test_ttl_preempt` and
    `test_warm_up_cluster_bigsize`.
    2. Busy blocks look free. `get_blocks_by_key()` also set the flag for
    callers that are not readers (`BlockFileCacheTtlMgr`, cache size
    statistics, the file cache HTTP action), and nothing ever cleared it. A
    block later held only by a `FileBlocksHolder` of a running query then
    counts as releasable, so eviction or clear can delete its file mid-read.
    
    This PR stores `std::weak_ptr` in `_cache_file_readers` and removes the
    flag. Readers no longer count as users, so `releasable()` is just
    `use_count == 1`, however many readers know about a block. A direct read
    locks the weak reference for the duration of the read, which keeps the
    block from being deleted under it, and falls back to `get_or_set()` once
    the cache has dropped the block. `_insert_file_reader()` now replaces a
    stale entry at the same offset instead of keeping it, so direct reads of
    that range work again after it is cached anew.
---
 be/src/io/cache/block_file_cache.cpp               |   1 -
 be/src/io/cache/block_file_cache.h                 |  12 +-
 be/src/io/cache/cached_remote_file_reader.cpp      |  34 +++--
 be/src/io/cache/cached_remote_file_reader.h        |   6 +-
 be/src/io/cache/file_block.h                       |   4 -
 be/test/io/cache/block_file_cache_test.cpp         | 163 ++++++++++++++++++++-
 .../cluster/test_warm_up_cluster_batch.groovy      |  15 +-
 .../cloud_p0/cache/test_file_cache_info.groovy     |   8 +-
 .../suites/cloud_p0/cache/ttl/alter_ttl_4.groovy   |  27 +++-
 .../cloud_p0/cache/ttl/test_ttl_lru_evict.groovy   | 131 +++++++----------
 10 files changed, 283 insertions(+), 118 deletions(-)

diff --git a/be/src/io/cache/block_file_cache.cpp 
b/be/src/io/cache/block_file_cache.cpp
index f0b9164d93d..1fd6555b8d1 100644
--- a/be/src/io/cache/block_file_cache.cpp
+++ b/be/src/io/cache/block_file_cache.cpp
@@ -2706,7 +2706,6 @@ std::map<size_t, FileBlockSPtr> 
BlockFileCache::get_blocks_by_key(const UInt128W
     if (_files.contains(hash)) {
         for (auto& [offset, cell] : _files[hash]) {
             if (cell.file_block->state() == FileBlock::State::DOWNLOADED) {
-                cell.file_block->_owned_by_cached_reader = true;
                 offset_to_block.emplace(offset, cell.file_block);
             }
         }
diff --git a/be/src/io/cache/block_file_cache.h 
b/be/src/io/cache/block_file_cache.h
index 89c03129b18..d3ef128c91a 100644
--- a/be/src/io/cache/block_file_cache.h
+++ b/be/src/io/cache/block_file_cache.h
@@ -159,13 +159,11 @@ struct FileBlockCell {
                         .count();
     }
 
-    /// Pointer to file block is always hold by the cache itself.
-    /// Apart from pointer in cache, it can be hold by cache users, when they 
call
-    /// getorSet(), but cache users always hold it via FileBlocksHolder.
-    bool releasable() const {
-        return (file_block.use_count() == 1 ||
-                (file_block.use_count() == 2 && 
file_block->_owned_by_cached_reader));
-    }
+    /// Pointer to file block is always hold by the cache itself. Every other 
strong reference
+    /// means somebody is still using the block. CachedRemoteFileReader keeps 
only weak references
+    /// in its direct-read map, so however many readers know about a block, 
they do not keep it
+    /// busy.
+    bool releasable() const { return file_block.use_count() == 1; }
 
     size_t size() const { return file_block->_block_range.size(); }
 
diff --git a/be/src/io/cache/cached_remote_file_reader.cpp 
b/be/src/io/cache/cached_remote_file_reader.cpp
index 18eb7c3d00d..dc9a8eede5f 100644
--- a/be/src/io/cache/cached_remote_file_reader.cpp
+++ b/be/src/io/cache/cached_remote_file_reader.cpp
@@ -137,7 +137,9 @@ void CachedRemoteFileReader::_init_doris_table_cache() {
     _cache = FileCacheFactory::instance()->get_by_path(_cache_hash);
     if (_can_read_cache_file_directly()) {
         // this is designed for and test in doris table, external table need 
extra tests
-        _cache_file_readers = _cache->get_blocks_by_key(_cache_hash);
+        for (auto& [offset, block] : _cache->get_blocks_by_key(_cache_hash)) {
+            _cache_file_readers.emplace(offset, block);
+        }
     }
 }
 
@@ -179,15 +181,13 @@ void 
CachedRemoteFileReader::_insert_file_reader(FileBlockSPtr file_block) {
     if (_can_read_cache_file_directly()) {
         std::lock_guard lock(_mtx);
         DCHECK(file_block->state() == FileBlock::State::DOWNLOADED);
-        file_block->_owned_by_cached_reader = true;
-        _cache_file_readers.emplace(file_block->offset(), 
std::move(file_block));
+        // Overwrite rather than emplace: an older entry at this offset may 
point to a block the
+        // cache has already dropped, and keeping it would disable direct 
reads of this range.
+        _cache_file_readers.insert_or_assign(file_block->offset(), 
std::move(file_block));
     }
 }
 
 CachedRemoteFileReader::~CachedRemoteFileReader() {
-    for (auto& it : _cache_file_readers) {
-        it.second->_owned_by_cached_reader = false;
-    }
     static_cast<void>(close());
 }
 
@@ -777,27 +777,33 @@ bool 
CachedRemoteFileReader::_try_read_from_cached_files_directly(
 
     size_t current_offset = offset;
     while (need_read_size != 0 && iter != _cache_file_readers.end()) {
-        if (iter->second->offset() > current_offset ||
-            iter->second->range().right < current_offset) {
+        // Holding the block while reading it makes the cache treat it as 
busy, so its file is not
+        // deleted mid-read. If the block is marked deleting meanwhile, 
run_background_gc()
+        // reclaims it once this reference is gone.
+        FileBlockSPtr block = iter->second.lock();
+        if (block == nullptr) {
+            // The cache has dropped this block. Read the range through the 
cache instead.
+            break;
+        }
+        if (block->offset() > current_offset || block->range().right < 
current_offset) {
             break;
         }
 
-        size_t file_offset = current_offset - iter->second->offset();
-        size_t reserve_bytes = std::min(need_read_size, 
iter->second->range().size() - file_offset);
+        size_t file_offset = current_offset - block->offset();
+        size_t reserve_bytes = std::min(need_read_size, block->range().size() 
- file_offset);
         if (is_dryrun) [[unlikely]] {
             g_skip_local_cache_io_sum_bytes << reserve_bytes;
         } else {
             SCOPED_RAW_TIMER(&stats.local_read_timer);
-            if (!iter->second
-                         ->read(Slice(result.data + (current_offset - offset), 
reserve_bytes),
-                                file_offset)
+            if (!block->read(Slice(result.data + (current_offset - offset), 
reserve_bytes),
+                             file_offset)
                          .ok()) { // TODO: maybe read failed because block 
evict, should handle error
                 break;
             }
             source_read_breakdown.local_bytes += reserve_bytes;
         }
 
-        _cache->add_need_update_lru_block(iter->second);
+        _cache->add_need_update_lru_block(std::move(block));
         need_read_size -= reserve_bytes;
         current_offset += reserve_bytes;
         already_read += reserve_bytes;
diff --git a/be/src/io/cache/cached_remote_file_reader.h 
b/be/src/io/cache/cached_remote_file_reader.h
index 68b112d9ece..1e58a1e4c52 100644
--- a/be/src/io/cache/cached_remote_file_reader.h
+++ b/be/src/io/cache/cached_remote_file_reader.h
@@ -435,7 +435,11 @@ private:
     UInt128Wrapper _cache_hash;
     BlockFileCache* _cache = nullptr;
     std::shared_mutex _mtx;
-    std::map<size_t, FileBlockSPtr> _cache_file_readers;
+    // Downloaded blocks this reader may read directly, keyed by offset. The 
references are weak on
+    // purpose: the cache decides when a block may go by counting strong 
references, and a reader
+    // that lives as long as its segment must not keep a block busy. Several 
readers of the same
+    // file can know about the same block at once.
+    std::map<size_t, std::weak_ptr<FileBlock>> _cache_file_readers;
 };
 
 } // namespace doris::io
diff --git a/be/src/io/cache/file_block.h b/be/src/io/cache/file_block.h
index 3e29f8ddb6a..a5e0eb875dd 100644
--- a/be/src/io/cache/file_block.h
+++ b/be/src/io/cache/file_block.h
@@ -151,10 +151,6 @@ public:
     void set_deleting() { _is_deleting = true; }
     bool is_deleting() const { return _is_deleting; };
 
-public:
-    std::atomic<bool> _owned_by_cached_reader {
-            false}; // pocessed by CachedRemoteFileReader::_cache_file_readers
-
 private:
     enum class CacheReferenceRole {
         HOLDER,
diff --git a/be/test/io/cache/block_file_cache_test.cpp 
b/be/test/io/cache/block_file_cache_test.cpp
index 549efd6b67e..4976fa7020e 100644
--- a/be/test/io/cache/block_file_cache_test.cpp
+++ b/be/test/io/cache/block_file_cache_test.cpp
@@ -3901,7 +3901,7 @@ TEST_F(BlockFileCacheTest, 
recycle_deleting_blocks_reclaims_stranded_busy_blocks
     auto key = io::BlockFileCache::hash("recycle_deleting_blocks");
 
     // A plain shared_ptr, standing in for the reference holders that do not 
release through
-    // FileBlock::release_cache_reference(): the background LRU update queue, 
the block map of
+    // FileBlock::release_cache_reference(): the background LRU update queue, 
the direct read of
     // CachedRemoteFileReader, the block vector of BlockFileCacheTtlMgr.
     std::vector<io::FileBlockSPtr> busy_refs;
     {
@@ -10534,6 +10534,167 @@ TEST_F(BlockFileCacheTest,
     EXPECT_TRUE(dryrun_stats.hit_cache);
 }
 
+namespace {
+
+// Downloads the first two tiny blocks of key and, if reader is given, 
registers them for its
+// direct reads.
+void cache_first_two_blocks(io::BlockFileCache* cache, const UInt128Wrapper& 
key,
+                            const std::string& content, 
CachedRemoteFileReader* reader) {
+    ReadStatistics cache_stats;
+    auto context = create_cached_remote_reader_context(&cache_stats);
+    FileBlocksHolder holder = cache->get_or_set(key, 0, 8, context);
+    auto blocks = fromHolder(holder);
+    ASSERT_EQ(blocks.size(), 2);
+    for (auto& block : blocks) {
+        append_cached_remote_reader_block(block, content);
+        if (reader != nullptr) {
+            reader->_insert_file_reader(block);
+        }
+    }
+}
+
+void expect_first_two_blocks_releasable(io::BlockFileCache* cache, const 
UInt128Wrapper& key) {
+    std::lock_guard<std::mutex> cache_lock(cache->_mutex);
+    for (size_t offset : {0, 4}) {
+        auto* cell = cache->get_cell(key, offset, cache_lock);
+        ASSERT_NE(cell, nullptr) << "offset=" << offset;
+        EXPECT_TRUE(cell->releasable()) << "offset=" << offset;
+    }
+}
+
+} // namespace
+
+// Several readers of one segment file, such as the one in SegmentCache and a 
temporary one that
+// bypasses it, know about the same downloaded blocks. None of them may keep 
those blocks busy, or
+// a sync clear can only mark the blocks deleting and they stay on disk while 
any reader lives.
+TEST_F(BlockFileCacheTest, 
cached_remote_file_readers_of_same_file_do_not_keep_blocks_busy) {
+    const bool old_enable_direct = config::enable_read_cache_file_directly;
+    config::enable_read_cache_file_directly = true;
+    Defer restore {[&]() { config::enable_read_cache_file_directly = 
old_enable_direct; }};
+
+    const std::string content = "abcdefghijklmnop";
+    const fs::path file_path =
+            
create_cached_remote_reader_test_file("cached_remote_readers_not_busy", 
content);
+    Defer cleanup_file {[&]() {
+        std::error_code ec;
+        fs::remove(file_path, ec);
+    }};
+
+    const fs::path cache_path = caches_dir / 
"cached_remote_readers_not_busy_cache";
+    clear_cached_remote_reader_factory();
+    Defer cleanup_cache {[&]() {
+        std::error_code ignore;
+        fs::remove_all(cache_path, ignore);
+        clear_cached_remote_reader_factory();
+    }};
+
+    auto* cache =
+            create_cached_remote_reader_test_cache(cache_path, 
kCachedRemoteReaderTinyBlockSize);
+
+    // A reader of a doris table finds the cached blocks by the file name.
+    const auto key = io::BlockFileCache::hash(file_path.filename().native());
+    cache_first_two_blocks(cache, key, content, nullptr);
+
+    io::FileReaderOptions opts;
+    opts.cache_type = io::FileCachePolicy::FILE_BLOCK_CACHE;
+    opts.is_doris_table = true;
+    opts.tablet_id = 10086;
+    // Each reader closes its own local reader when it goes away.
+    FileReaderSPtr long_lived_local_reader;
+    FileReaderSPtr temporary_local_reader;
+    ASSERT_TRUE(global_local_filesystem()
+                        ->open_file(file_path.string(), 
&long_lived_local_reader)
+                        .ok());
+    ASSERT_TRUE(
+            global_local_filesystem()->open_file(file_path.string(), 
&temporary_local_reader).ok());
+    auto long_lived_reader =
+            std::make_shared<CachedRemoteFileReader>(long_lived_local_reader, 
opts);
+    auto temporary_reader = 
std::make_shared<CachedRemoteFileReader>(temporary_local_reader, opts);
+    ASSERT_EQ(long_lived_reader->_cache_file_readers.size(), 2);
+    ASSERT_EQ(temporary_reader->_cache_file_readers.size(), 2);
+    expect_first_two_blocks_releasable(cache, key);
+
+    // The temporary reader going away must not change how the other one is 
accounted for.
+    temporary_reader.reset();
+    expect_first_two_blocks_releasable(cache, key);
+
+    auto msg = cache->clear_file_cache_sync();
+    EXPECT_NE(msg.find("num_cells_to_delete=2"), std::string::npos) << msg;
+    EXPECT_NE(msg.find("num_cells_wait_recycle=0"), std::string::npos) << msg;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache->_mutex);
+        EXPECT_EQ(cache->get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_EQ(cache->get_cell(key, 4, cache_lock), nullptr);
+    }
+
+    // The reader still has entries for the dropped blocks, but must not read 
through them.
+    std::string buffer(6, '#');
+    size_t bytes_read = 0;
+    size_t already_read = 0;
+    ReadStatistics read_stats;
+    SourceReadBreakdown source_read_breakdown;
+    EXPECT_FALSE(long_lived_reader->_try_read_from_cached_files_directly(
+            1, Slice(buffer.data(), buffer.size()), buffer.size(), false, 
read_stats,
+            source_read_breakdown, already_read, &bytes_read));
+    EXPECT_EQ(already_read, 0);
+    EXPECT_EQ(buffer, "######");
+
+    // Once the range is cached again, the new blocks replace the stale 
entries.
+    cache_first_two_blocks(cache, key, content, long_lived_reader.get());
+    expect_first_two_blocks_releasable(cache, key);
+    EXPECT_TRUE(long_lived_reader->_try_read_from_cached_files_directly(
+            1, Slice(buffer.data(), buffer.size()), buffer.size(), false, 
read_stats,
+            source_read_breakdown, already_read, &bytes_read));
+    EXPECT_EQ(buffer, "bcdefg");
+}
+
+// Looking blocks up by key must not change whether the cache considers them 
in use. A block held
+// by a reader through FileBlocksHolder must stay busy, so clear can only mark 
it deleting.
+TEST_F(BlockFileCacheTest, 
get_blocks_by_key_does_not_make_busy_blocks_releasable) {
+    const std::string content = "abcd";
+    const fs::path cache_path = caches_dir / "get_blocks_by_key_busy_cache";
+    clear_cached_remote_reader_factory();
+    Defer cleanup_cache {[&]() {
+        std::error_code ignore;
+        fs::remove_all(cache_path, ignore);
+        clear_cached_remote_reader_factory();
+    }};
+
+    auto* cache =
+            create_cached_remote_reader_test_cache(cache_path, 
kCachedRemoteReaderTinyBlockSize);
+    const auto key = io::BlockFileCache::hash("get_blocks_by_key_busy");
+    ReadStatistics cache_stats;
+    auto context = create_cached_remote_reader_context(&cache_stats);
+
+    auto holder = std::make_unique<FileBlocksHolder>(cache->get_or_set(key, 0, 
4, context));
+    ASSERT_EQ(holder->file_blocks.size(), 1);
+    append_cached_remote_reader_block(holder->file_blocks.front(), content);
+
+    ASSERT_EQ(cache->get_blocks_by_key(key).size(), 1);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache->_mutex);
+        auto* cell = cache->get_cell(key, 0, cache_lock);
+        ASSERT_NE(cell, nullptr);
+        EXPECT_FALSE(cell->releasable());
+    }
+
+    auto msg = cache->clear_file_cache_sync();
+    EXPECT_NE(msg.find("num_cells_wait_recycle=1"), std::string::npos) << msg;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache->_mutex);
+        auto* cell = cache->get_cell(key, 0, cache_lock);
+        ASSERT_NE(cell, nullptr);
+        EXPECT_TRUE(cell->file_block->is_deleting());
+    }
+
+    // The holder is the last user, so releasing it finishes the deletion.
+    holder.reset();
+    {
+        std::lock_guard<std::mutex> cache_lock(cache->_mutex);
+        EXPECT_EQ(cache->get_cell(key, 0, cache_lock), nullptr);
+    }
+}
+
 TEST_F(BlockFileCacheTest, 
file_block_appendv_persists_segmented_payload_to_fs_cache) {
     const fs::path cache_path = caches_dir / "file_block_appendv_fs_cache";
     clear_cached_remote_reader_factory();
diff --git 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
index 9244e7f9e24..d31750bef02 100644
--- 
a/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
+++ 
b/regression-test/suites/cloud_p0/cache/multi_cluster/warm_up/cluster/test_warm_up_cluster_batch.groovy
@@ -211,11 +211,16 @@ suite("test_warm_up_cluster_batch", "nonConcurrent") {
                 }
                 int batch = (round + 1) / 2
                 def hotspotBeforeQueries = getHotspotCounts()
-                // One real scan per table is enough to advance the hotspot 
counters for this batch.
-                tableNames.each { tableName ->
-                    def result = sql(dataQueries[tableName])
-                    assertTrue(!result.isEmpty() && result[0].every { it != 
null },
-                            "Data query should read nonempty ${tableName}")
+                // After its first report, BE reports a partition again only 
when its queries per
+                // day grew by at least 5 or its last access moved by at least 
60 seconds since that
+                // report (get_return_partitions() in 
cloud_tablet_hotspot.cpp). Each scan adds one,
+                // and batches can be less than 60 seconds apart, so scan each 
table 5 times.
+                for (int i = 0; i < 5; i++) {
+                    tableNames.each { tableName ->
+                        def result = sql(dataQueries[tableName])
+                        assertTrue(!result.isEmpty() && result[0].every { it 
!= null },
+                                "Data query should read nonempty ${tableName}")
+                    }
                 }
                 // Require hotspot counters for these new table/partition IDs 
to advance beyond
                 // the pre-query snapshot, without assuming identical FE and 
BE timestamps.
diff --git a/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy 
b/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
index 26c4964bb72..51034cce2ed 100644
--- a/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
+++ b/regression-test/suites/cloud_p0/cache/test_file_cache_info.groovy
@@ -37,7 +37,7 @@ suite("test_file_cache_info", "nonConcurrent") {
         Long tabletId = null
         def lastState = [:]
         def clearResponses = [:]
-        def originalQueryCache = sql("select @@enable_sql_cache, 
@@enable_query_cache")[0].collect { value ->
+        def originalQueryCache = sql("select @@enable_sql_cache, 
@@enable_query_cache, @@enable_page_cache")[0].collect { value ->
             String setting = value.toString().toLowerCase(Locale.ROOT)
             assertTrue(setting in ["true", "false", "0", "1"], "Unexpected 
cache setting: ${value}")
             setting
@@ -168,6 +168,9 @@ suite("test_file_cache_info", "nonConcurrent") {
             sql "use @${clusterName}"
             sql "set enable_sql_cache = false"
             sql "set enable_query_cache = false"
+            // The reread after clear must go through the file cache. Pages of 
a five-row table
+            // stay in the BE page cache, and serving them from memory would 
never refill it.
+            sql "set enable_page_cache = false"
             sql "drop table if exists ${tableName} force"
             sql """CREATE TABLE ${tableName} (
                 c_custkey INT, c_name STRING, c_address STRING, c_city STRING,
@@ -214,7 +217,8 @@ suite("test_file_cache_info", "nonConcurrent") {
         } finally {
             def cleanupErrors = []
             try { sql "drop table if exists ${tableName} force" } catch 
(Throwable t) { cleanupErrors.add(t) }
-            [enable_sql_cache: originalQueryCache[0], enable_query_cache: 
originalQueryCache[1]].each { name, value ->
+            [enable_sql_cache: originalQueryCache[0], enable_query_cache: 
originalQueryCache[1],
+             enable_page_cache: originalQueryCache[2]].each { name, value ->
                 try { sql "set ${name} = ${value}" } catch (Throwable t) { 
cleanupErrors.add(t) }
             }
             if (!cleanupErrors.isEmpty()) {
diff --git a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy 
b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
index c78a140ba47..3ec3864f6f5 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/alter_ttl_4.groovy
@@ -42,7 +42,7 @@ suite("alter_ttl_4", "nonConcurrent") {
         def pendingLoads = [] as Set
         def lastState = [:]
         def cacheDifference = [:]
-        def originalQueryCache = sql("select @@enable_sql_cache, 
@@enable_query_cache")[0].collect { value ->
+        def originalQueryCache = sql("select @@enable_sql_cache, 
@@enable_query_cache, @@enable_page_cache")[0].collect { value ->
             String setting = value.toString().toLowerCase(Locale.ROOT)
             assertTrue(setting in ["true", "false", "0", "1"], "Unexpected 
cache setting: ${value}")
             setting
@@ -206,6 +206,8 @@ suite("alter_ttl_4", "nonConcurrent") {
             sql "use @${clusterName}"
             sql "set enable_sql_cache = false"
             sql "set enable_query_cache = false"
+            // Scans of the evicted target must read its files again instead 
of pages kept in memory.
+            sql "set enable_page_cache = false"
             [(targetTable): initialTtl, (pressureTable): 0L].each { table, ttl 
->
                 sql "drop table if exists ${table} force"
                 String ddl = new 
File("${context.file.parent}/../ddl/customer_ttl.sql").text
@@ -291,14 +293,22 @@ suite("alter_ttl_4", "nonConcurrent") {
             logMetricDelta("Pressure", metricsBeforePressure, 
metricsAfterPressure)
             assertTrue(!evictedFiles.isEmpty(), "No target cache eviction 
before pressure limit: " +
                     "imported_rows=${pressureRows}/${maxPressureRows}, 
metrics=${metricsAfterPressure}")
+            def coverageAfterPressure = coverage(getCache())
 
             assertEquals(dataBaseline, scanTarget())
-            // Re-reading an evicted file may split blocks differently. 
Compare merged byte ranges.
-            def reloaded = waitForCache("reloaded NORMAL cache coverage", 
"normal") { blocks ->
-                def current = coverage(blocks)
-                cacheDifference = [missing: missingCoverage(baselineCoverage, 
current).take(10),
-                                   extra: missingCoverage(current, 
baselineCoverage).take(10)]
-                current == baselineCoverage
+            // The scan reads the data pages of the evicted files again, but 
not their footers and
+            // indexes, which stay in memory with the open segments. So the 
reloaded ranges need not
+            // match what the load cached: require the evicted files to have 
more cached than right
+            // after the eviction.
+            def cachedBytes = { Map fileRanges ->
+                evictedFiles.keySet().sum(0L) { file -> (fileRanges[file] ?: 
[]).sum(0L) { it[1] - it[0] } }
+            }
+            long bytesAfterPressure = cachedBytes(coverageAfterPressure)
+            def reloaded = waitForCache("reloaded NORMAL cache of evicted 
files", "normal") { blocks ->
+                long bytesNow = cachedBytes(coverage(blocks))
+                cacheDifference = [evicted_files: evictedFiles.size(),
+                                   cached_bytes_after_pressure: 
bytesAfterPressure, cached_bytes_now: bytesNow]
+                bytesNow > bytesAfterPressure
             }
             def metricsBeforeExtension = getMetrics()
             long now = sql("select unix_timestamp()")[0][0] as Long
@@ -322,7 +332,8 @@ suite("alter_ttl_4", "nonConcurrent") {
             [targetTable, pressureTable].each { table ->
                 try { sql "drop table if exists ${table} force" } catch 
(Throwable t) { cleanupErrors.add(t) }
             }
-            [enable_sql_cache: originalQueryCache[0], enable_query_cache: 
originalQueryCache[1]].each { name, value ->
+            [enable_sql_cache: originalQueryCache[0], enable_query_cache: 
originalQueryCache[1],
+             enable_page_cache: originalQueryCache[2]].each { name, value ->
                 try { sql "set ${name} = ${value}" } catch (Throwable t) { 
cleanupErrors.add(t) }
             }
             if (!cleanupErrors.isEmpty()) {
diff --git 
a/regression-test/suites/cloud_p0/cache/ttl/test_ttl_lru_evict.groovy 
b/regression-test/suites/cloud_p0/cache/ttl/test_ttl_lru_evict.groovy
index f452d071e04..e69e59f2e6a 100644
--- a/regression-test/suites/cloud_p0/cache/ttl/test_ttl_lru_evict.groovy
+++ b/regression-test/suites/cloud_p0/cache/ttl/test_ttl_lru_evict.groovy
@@ -58,7 +58,10 @@ suite("test_ttl_lru_evict") {
     logger.info("getS3SK:${getS3SK()}");
     logger.info("getS3Endpoint:${getS3Endpoint()}");
 
-    def ttlProperties = """ PROPERTIES("file_cache_ttl_seconds"="150") """
+    // Long enough that the first loads are still TTL when the TTL queue 
overflows. How many loads
+    // that takes depends on the cache capacity of the environment.
+    def ttlSeconds = 300
+    def ttlProperties = """ 
PROPERTIES("file_cache_ttl_seconds"="${ttlSeconds}") """
     String[][] backends = sql """ show backends """
     String backendId;
     def backendIdToBackendIP = [:]
@@ -206,65 +209,53 @@ suite("test_ttl_lru_evict") {
         }
     }
 
-    clearFileCache.call() {
-        respCode, body -> {}
-    }
-
-    long ttl_cache_evict_size_begin = 0;
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_evict_size")) {
-                    if (line.startsWith("#")) {
+    // Values of every metric line whose name contains the given name, one per 
cache path.
+    def getMetricValues = { String name ->
+        def values = []
+        getMetricsMethod.call() {
+            respCode, body ->
+                assertEquals("${respCode}".toString(), "200")
+                for (String line in "${body}".toString().split('\n')) {
+                    if (line.startsWith("#") || !line.contains(name)) {
                         continue
                     }
-                    def i = line.indexOf(' ')
-                    ttl_cache_evict_size_begin = line.substring(i).toLong()
-                    flag1 = true
+                    values.add(line.substring(line.indexOf(' 
')).trim().toLong())
                 }
-            }
-            assertTrue(flag1)
+        }
+        assertTrue(!values.isEmpty(), "metric ${name} not found")
+        return values
     }
 
-    // load for MANY times to this dup table to make cache full enough to evict
-    // It will takes huge amount of time, so it should be p1/p2 level case.
-    // But someone told me it is the right place. Never mind just do it!
+    clearFileCache.call() {
+        respCode, body -> {}
+    }
 
-    for (int i = 0; i < 10; i++) {
+    long ttl_cache_evict_size_begin = 
getMetricValues("ttl_cache_evict_size")[0]
+    long ttl_cache_size_begin = getMetricValues("ttl_cache_size")[0]
+    long cache_capacity = getMetricValues("file_cache_capacity").sum()
+
+    // first, we test whether ttl evict actively.
+    // Load to this dup table until the TTL queue overflows. A fixed number of 
loads only
+    // overflows it on a small enough cache, so keep loading, bounded by the 
cache capacity.
+    long evicted = 0
+    long loads = 0
+    long max_loads = 10
+    while (evicted <= 238860800 && loads < max_loads) {
         load_customer_once("customer_ttl")
+        loads++
+        if (loads == 1) {
+            long bytes_per_load = getMetricValues("ttl_cache_size")[0] - 
ttl_cache_size_begin
+            assertTrue(bytes_per_load > 0, "the first load did not add any TTL 
cache")
+            // The TTL queue is part of the capacity, so this many loads 
overflow it for sure.
+            max_loads = Math.max(max_loads, 
cache_capacity.intdiv(bytes_per_load) + 2)
+            logger.info("cache capacity: ${cache_capacity}, TTL bytes per 
load: ${bytes_per_load}, " +
+                    "max loads: ${max_loads}")
+        }
+        evicted = getMetricValues("ttl_cache_evict_size")[0] - 
ttl_cache_evict_size_begin
+        logger.info("ttl evict diff after ${loads} loads: ${evicted}")
     }
-    sleep(10000) // 10s
-    // first, we test whether ttl evict actively
-    long ttl_cache_evict_size_end = 0;
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_evict_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    ttl_cache_evict_size_end = line.substring(i).toLong()
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
-    }
-    // Note: this only applies when the case is run
-    // sequentially, coz we don't know what other cases are
-    // doing with TTL cache
-    logger.info("ttl evict diff:" + (ttl_cache_evict_size_end - 
ttl_cache_evict_size_begin).toString())
-    assertTrue((ttl_cache_evict_size_end - ttl_cache_evict_size_begin) > 
238860800)
+    assertTrue(evicted > 238860800,
+            "TTL cache evicted only ${evicted} bytes after ${loads} loads, 
capacity ${cache_capacity}")
 
     // then we test skip_cache count when doing query when ttl cache is full
     // we expect it to be rare
@@ -324,29 +315,19 @@ suite("test_ttl_lru_evict") {
 
     // finally, we test whether LRU queue clean itself up when all ttl
     // cache evict after expiration
-    sleep(200000)
-    getMetricsMethod.call() {
-        respCode, body ->
-            assertEquals("${respCode}".toString(), "200")
-            String out = "${body}".toString()
-            def strs = out.split('\n')
-            Boolean flag1 = false;
-            for (String line in strs) {
-                if (flag1) break;
-                if (line.contains("ttl_cache_lru_queue_size")) {
-                    if (line.startsWith("#")) {
-                        continue
-                    }
-                    def i = line.indexOf(' ')
-                    // all ttl will expire, so the ttl LRU queue set to 0
-                    // Note: this only applies when the case is run
-                    // sequentially, coz we don't know what other cases are
-                    // doing with TTL cache
-                    assertEquals(line.substring(i).toLong(), 0)
-                    flag1 = true
-                }
-            }
-            assertTrue(flag1)
+    // Note: this only applies when the case is run
+    // sequentially, coz we don't know what other cases are
+    // doing with TTL cache
+    long ttl_queue_size = -1
+    long deadline = System.currentTimeMillis() + (ttlSeconds + 120) * 1000L
+    while (System.currentTimeMillis() < deadline) {
+        // all ttl will expire, so the ttl LRU queue set to 0
+        ttl_queue_size = getMetricValues("ttl_cache_lru_queue_size")[0]
+        if (ttl_queue_size == 0) {
+            break
+        }
+        sleep(10000)
     }
+    assertEquals(0, ttl_queue_size)
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to