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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new e2a836ec154 branch-4.2: [fix](file-cache) recycle busy blocks that 
were marked deleting #68348 (#69141)
e2a836ec154 is described below

commit e2a836ec154c94d069bca68be469802c7d8b62b0
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 16 09:28:56 2026 +0800

    branch-4.2: [fix](file-cache) recycle busy blocks that were marked deleting 
#68348 (#69141)
    
    Cherry-picked from #68348
    
    Co-authored-by: Xin Liao <[email protected]>
---
 be/src/io/cache/block_file_cache.cpp       | 107 +++++-
 be/src/io/cache/block_file_cache.h         |  29 +-
 be/test/io/cache/block_file_cache_test.cpp | 558 +++++++++++++++++++++++++++++
 3 files changed, 683 insertions(+), 11 deletions(-)

diff --git a/be/src/io/cache/block_file_cache.cpp 
b/be/src/io/cache/block_file_cache.cpp
index c5ebc7cdbfb..e1c7fa3aad1 100644
--- a/be/src/io/cache/block_file_cache.cpp
+++ b/be/src/io/cache/block_file_cache.cpp
@@ -406,6 +406,10 @@ BlockFileCache::BlockFileCache(const std::string& 
cache_base_path,
             _cache_base_path.c_str(), "file_cache_lru_dump_latency_us");
     _recycle_keys_length_recorder = std::make_shared<bvar::LatencyRecorder>(
             _cache_base_path.c_str(), "file_cache_recycle_keys_length");
+    _deleting_block_count_metrics = std::make_shared<bvar::Status<size_t>>(
+            _cache_base_path.c_str(), "file_cache_deleting_block_count", 0);
+    _recycle_deleting_blocks_metrics = std::make_shared<bvar::Adder<size_t>>(
+            _cache_base_path.c_str(), "file_cache_recycle_deleting_blocks");
     _need_update_lru_blocks_length_recorder = 
std::make_shared<bvar::LatencyRecorder>(
             _cache_base_path.c_str(), 
"file_cache_need_update_lru_blocks_length");
     _need_update_lru_blocks_produce_metrics = 
std::make_shared<bvar::Adder<size_t>>(
@@ -993,6 +997,63 @@ bool BlockFileCache::is_block_deleting(const 
FileBlockSPtr& block) const {
     return block->_is_deleting;
 }
 
+template <class T>
+    requires IsXLock<T>
+void BlockFileCache::mark_cell_deleting(FileBlockCell& cell, T& /* cache_lock 
*/) {
+    cell.file_block->set_deleting();
+    // The block stays in _files until its last reference is dropped. Remember 
it so that
+    // run_background_gc() can finish the job if that reference is not 
released through
+    // FileBlock::release_cache_reference(). remove() forgets it again when it 
erases the cell.
+    _deleting_blocks.emplace(cell.file_block->get_hash_value(), 
cell.file_block->offset());
+}
+
+size_t BlockFileCache::recycle_deleting_blocks(size_t batch_limit) {
+    if (batch_limit == 0) {
+        return 0;
+    }
+    size_t recycled = 0;
+    size_t pending = 0;
+    {
+        SCOPED_CACHE_LOCK(_mutex, this);
+        std::vector<FileBlockSPtr> to_remove;
+        for (const auto& [hash, offset] : _deleting_blocks) {
+            if (to_remove.size() >= batch_limit) {
+                break;
+            }
+            auto* cell = get_cell(hash, offset, cache_lock);
+            if (cell == nullptr || !cell->file_block->is_deleting()) 
[[unlikely]] {
+                // Cannot happen: remove() erases the key together with the 
cell.
+                DCHECK(false) << "stale deleting block, hash=" << 
hash.to_string()
+                              << " offset=" << offset;
+                continue;
+            }
+            if (!cell->releasable() ||
+                cell->file_block->state_unsafe() == 
FileBlock::State::DOWNLOADING) {
+                // Still in use, or still being downloaded. Look at it again 
in the next round.
+                continue;
+            }
+            to_remove.push_back(cell->file_block);
+        }
+        // remove() erases the key of every block it removes, so it must not 
run while
+        // _deleting_blocks is being iterated above.
+        for (auto& file_block : to_remove) {
+            std::lock_guard block_lock(file_block->_mutex);
+            // Deleting the file itself happens asynchronously: the storage 
must not be touched
+            // while the cache lock is held.
+            remove(file_block, cache_lock, block_lock, false);
+            ++recycled;
+        }
+        pending = _deleting_blocks.size();
+    }
+    if (recycled > 0) {
+        *_recycle_deleting_blocks_metrics << recycled;
+        LOG(INFO) << "recycled deleting blocks, path=" << _cache_base_path << 
" num=" << recycled
+                  << " pending=" << pending;
+    }
+    _deleting_block_count_metrics->set_value(pending);
+    return recycled;
+}
+
 std::string BlockFileCache::clear_file_cache_async() {
     return clear_file_cache_impl(false);
 }
@@ -1024,6 +1085,11 @@ std::string BlockFileCache::clear_file_cache_impl(bool 
sync_remove) {
             }
         }
 
+        // Drop the pending LRU updates before the scan below, not after it: 
every block still
+        // queued there carries an extra reference that would make the block 
look busy and send
+        // it down the wait-for-recycle path for no reason.
+        clear_need_update_lru_blocks();
+
         // Do not erase while walking _files above: remove() may erase the 
current map element.
         //
         // sync_remove only changes how already releasable DOWNLOADED blocks 
are deleted from
@@ -1032,8 +1098,9 @@ std::string BlockFileCache::clear_file_cache_impl(bool 
sync_remove) {
         for (auto& cell : deleting_cells) {
             if (!cell->releasable()) {
                 LOG(INFO) << "cell is not releasable, hash="
+                          << cell->file_block->get_hash_value().to_string()
                           << " offset=" << cell->file_block->offset();
-                cell->file_block->set_deleting();
+                mark_cell_deleting(*cell, cache_lock);
                 ++num_cells_wait_recycle;
                 continue;
             }
@@ -1044,7 +1111,6 @@ std::string BlockFileCache::clear_file_cache_impl(bool 
sync_remove) {
                 ++num_cells_to_delete;
             }
         }
-        clear_need_update_lru_blocks();
     }
 
     std::stringstream ss;
@@ -1321,7 +1387,7 @@ size_t BlockFileCache::try_release() {
             if (cell.releasable()) {
                 trash.emplace_back(&cell);
             } else {
-                cell.file_block->set_deleting();
+                mark_cell_deleting(cell, cache_lock);
             }
         }
     }
@@ -1554,7 +1620,7 @@ void BlockFileCache::remove_if_cached(const 
UInt128Wrapper& file_key) {
             if (cell.releasable()) {
                 to_remove.push_back(&cell);
             } else {
-                cell.file_block->set_deleting();
+                mark_cell_deleting(cell, cache_lock);
             }
         }
         remove_file_blocks(to_remove, cache_lock, true, reason);
@@ -1579,7 +1645,7 @@ void BlockFileCache::remove_if_cached_async(const 
UInt128Wrapper& file_key) {
             if (cell.releasable()) {
                 to_remove.push_back(&cell);
             } else {
-                cell.file_block->set_deleting();
+                mark_cell_deleting(cell, cache_lock);
             }
         }
         remove_file_blocks(to_remove, cache_lock, false, reason);
@@ -1805,7 +1871,8 @@ void BlockFileCache::remove(FileBlockSPtr file_block, T& 
cache_lock, U& block_lo
     auto* cell = get_cell(hash, offset, cache_lock);
     file_block->cell = nullptr;
     // Holder cleanup can race with prior cache metadata cleanup. In that case,
-    // skip the duplicate remove instead of touching a detached or replaced 
cell.
+    // skip the duplicate remove instead of touching a detached or replaced 
cell. That includes
+    // _deleting_blocks: a key found there now belongs to the block that 
replaced this one.
     if (cell == nullptr) {
         LOG(WARNING) << "remove skipped because cache cell is missing. hash=" 
<< hash.to_string()
                      << " offset=" << offset << " size=" << 
file_block->range().size()
@@ -1835,7 +1902,8 @@ void BlockFileCache::remove(FileBlockSPtr file_block, T& 
cache_lock, U& block_lo
                                     : "<null>");
         return;
     }
-    DCHECK(cell->queue_iterator);
+    // No iterator means the block was already taken out of its queue by an 
earlier remove()
+    // that could not erase the cell because the block was still downloading.
     if (cell->queue_iterator) {
         auto& queue = get_queue(file_block->cache_type());
         queue.remove(*cell->queue_iterator, cache_lock);
@@ -1891,7 +1959,11 @@ void BlockFileCache::remove(FileBlockSPtr file_block, T& 
cache_lock, U& block_lo
             }
         }
     } else if (state == FileBlock::State::DOWNLOADING) {
-        file_block->set_deleting();
+        // The block is already out of its LRU queue, but the cell stays in 
_files until the
+        // downloader is done with it. Forget the iterator: it points at a 
freed queue entry now,
+        // and whoever finally erases this cell must not touch it again.
+        cell->queue_iterator.reset();
+        mark_cell_deleting(*cell, cache_lock);
         return;
     } else {
         *_evict_not_downloaded_size_metrics << file_block->range().size();
@@ -1904,6 +1976,10 @@ void BlockFileCache::remove(FileBlockSPtr file_block, T& 
cache_lock, U& block_lo
     auto it = _files.find(hash);
     if (it != _files.end()) {
         it->second.erase(file_block->offset());
+        if (file_block->is_deleting()) {
+            // The key leaves _deleting_blocks together with its cell.
+            _deleting_blocks.erase(AccessKeyAndOffset(hash, offset));
+        }
         if (it->second.empty()) {
             _files.erase(hash);
         }
@@ -2116,8 +2192,12 @@ void BlockFileCache::change_cache_type(const 
UInt128Wrapper& hash, size_t offset
         auto& file_blocks = iter->second;
         if (auto cell_it = file_blocks.find(offset); cell_it != 
file_blocks.end()) {
             FileBlockCell& cell = cell_it->second;
+            // No iterator means remove() already took the block out of its 
queue while it was
+            // downloading. The block is deleting, so keep it out of every 
queue.
+            if (!cell.queue_iterator) {
+                return;
+            }
             auto& cur_queue = get_queue(cell.file_block->cache_type());
-            DCHECK(cell.queue_iterator.has_value());
             cur_queue.remove(*cell.queue_iterator, cache_lock);
             _lru_recorder->record_queue_event(
                     cell.file_block->cache_type(), CacheLRULogType::REMOVE,
@@ -2183,7 +2263,7 @@ std::string BlockFileCache::reset_capacity(size_t 
new_capacity) {
                     queue_released += entry_size;
                     auto* cell = get_cell(entry_key, entry_offset, cache_lock);
                     if (!cell->releasable()) {
-                        cell->file_block->set_deleting();
+                        mark_cell_deleting(*cell, cache_lock);
                         continue;
                     }
                     to_evict.push_back(cell);
@@ -2421,6 +2501,10 @@ void BlockFileCache::run_background_gc() {
             }
         }
 
+        // Blocks that were busy when they were marked deleting, but are not 
referenced any
+        // more, are only reclaimed here.
+        recycle_deleting_blocks(batch_limit);
+
         while (batch_count < batch_limit && _recycle_keys.try_dequeue(key)) {
             int64_t duration_ns = 0;
             Status st;
@@ -2506,6 +2590,9 @@ void BlockFileCache::run_background_block_lru_update() {
                 update_block_lru(block, cache_lock);
             }
         }
+        // Drop the references now instead of at the top of the next round: 
holding them across
+        // the sleep would keep every block in this batch looking busy for a 
whole interval.
+        batch.clear();
         *_update_lru_blocks_latency_us << (duration_ns / 1000);
         *_need_update_lru_blocks_length_recorder << 
_need_update_lru_blocks.size();
     }
diff --git a/be/src/io/cache/block_file_cache.h 
b/be/src/io/cache/block_file_cache.h
index 9b32b4764b4..6cb9513ff3b 100644
--- a/be/src/io/cache/block_file_cache.h
+++ b/be/src/io/cache/block_file_cache.h
@@ -32,6 +32,7 @@
 #include <optional>
 #include <thread>
 #include <unordered_map>
+#include <unordered_set>
 #include <vector>
 
 #include "io/cache/block_file_cache_ttl_mgr.h"
@@ -419,7 +420,8 @@ public:
 private:
     // Shared scan used by both clear modes. It keeps the FileBlock holder 
lifecycle intact:
     // releasable blocks are removed immediately, while blocks held by readers 
are only marked
-    // deleting and are later removed by FileBlocksHolder destruction.
+    // deleting and are later removed by FileBlocksHolder destruction or, when 
the last holder
+    // does not release through FileBlock::release_cache_reference(), by 
run_background_gc().
     std::string clear_file_cache_impl(bool sync_remove);
 
     LRUQueue& get_queue(FileCacheType type);
@@ -536,6 +538,17 @@ private:
 
     void clear_need_update_lru_blocks();
 
+    // Marks a cell that cannot be erased yet as deleting and remembers it in 
_deleting_blocks, so
+    // that the block is still reclaimed when its last reference is dropped 
without going through
+    // FileBlock::release_cache_reference(). This is the only place that sets 
a block deleting.
+    template <class T>
+        requires IsXLock<T>
+    void mark_cell_deleting(FileBlockCell& cell, T& cache_lock);
+
+    // Removes up to batch_limit blocks that were marked deleting and are not 
referenced any
+    // more. Returns how many of them were removed.
+    size_t recycle_deleting_blocks(size_t batch_limit);
+
     // info
     std::string _cache_base_path;
     size_t _capacity = 0;
@@ -581,6 +594,18 @@ private:
     // keys for async remove
     RecycleFileCacheKeys _recycle_keys;
 
+    // Blocks that were busy when something asked to delete them. They keep 
sitting in _files
+    // until their last reference goes away. Only FileBlocksHolder and 
FileBlocksProbeResult
+    // release through FileBlock::release_cache_reference(), which is the one 
path that notices
+    // is_deleting and removes the block; every other reference holder just 
drops its shared_ptr
+    // and would strand the block here forever. run_background_gc() rechecks 
these candidates and
+    // removes the ones that became releasable.
+    //
+    // Invariant: holds exactly the keys of the cells in _files whose block is 
deleting. A key is
+    // added by mark_cell_deleting() and erased by remove() together with its 
cell, so a block
+    // created later at the same offset is never taken for the one that was 
marked.
+    std::unordered_set<AccessKeyAndOffset, KeyAndOffsetHash> _deleting_blocks;
+
     std::unique_ptr<LRUQueueRecorder> _lru_recorder;
     std::unique_ptr<CacheLRUDumper> _lru_dumper;
     std::unique_ptr<BlockFileCacheTtlMgr> _ttl_mgr;
@@ -648,6 +673,8 @@ private:
     std::shared_ptr<bvar::LatencyRecorder> _storage_async_remove_latency_us;
     std::shared_ptr<bvar::LatencyRecorder> _evict_in_advance_latency_us;
     std::shared_ptr<bvar::LatencyRecorder> _recycle_keys_length_recorder;
+    std::shared_ptr<bvar::Status<size_t>> _deleting_block_count_metrics;
+    std::shared_ptr<bvar::Adder<size_t>> _recycle_deleting_blocks_metrics;
     std::shared_ptr<bvar::LatencyRecorder> _update_lru_blocks_latency_us;
     std::shared_ptr<bvar::LatencyRecorder> 
_need_update_lru_blocks_length_recorder;
     std::shared_ptr<bvar::Adder<size_t>> 
_need_update_lru_blocks_produce_metrics;
diff --git a/be/test/io/cache/block_file_cache_test.cpp 
b/be/test/io/cache/block_file_cache_test.cpp
index dbd0b623b97..37cc2068404 100644
--- a/be/test/io/cache/block_file_cache_test.cpp
+++ b/be/test/io/cache/block_file_cache_test.cpp
@@ -3698,6 +3698,564 @@ TEST_F(BlockFileCacheTest, 
clear_file_cache_sync_removes_releasable_blocks_synch
     }
 }
 
+TEST_F(BlockFileCacheTest, 
remove_downloading_block_forgets_its_queue_iterator) {
+    std::string my_cache_path = caches_dir / "remove_downloading_block" / "";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr size_t block_size = 4;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = 64;
+    settings.query_queue_elements = 16;
+    settings.index_queue_size = 64;
+    settings.index_queue_elements = 16;
+    settings.disposable_queue_size = 64;
+    settings.disposable_queue_elements = 16;
+    settings.capacity = 192;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = 64;
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = io::BlockFileCache::hash("remove_downloading_block");
+
+    io::FileBlockSPtr block;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::DOWNLOADED,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        FileBlockTestAccessor::set_state(*cell->file_block, 
io::FileBlock::State::DOWNLOADING);
+        FileBlockTestAccessor::set_downloader_id(*cell->file_block, 1234);
+        block = cell->file_block;
+    }
+    auto& queue = cache.get_queue(io::FileCacheType::NORMAL);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        ASSERT_EQ(queue.get_elements_num(cache_lock), 1);
+    }
+
+    // A block that is still downloading is taken out of its LRU queue, but 
its cell stays in
+    // _files, so the iterator it holds points at a freed queue entry from 
here on.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        std::lock_guard<std::mutex> block_lock(block->_mutex);
+        cache.remove(block, cache_lock, block_lock, false);
+    }
+    {
+        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());
+        EXPECT_EQ(cache._deleting_blocks.count(io::AccessKeyAndOffset(key, 
0)), 1);
+        EXPECT_FALSE(cell->queue_iterator.has_value());
+        EXPECT_EQ(queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(queue.get_capacity(cache_lock), 0);
+    }
+
+    // The download finishes and the last holder releases the block: this 
second remove() is the
+    // one that erases the cell, and it must not touch the queue entry again.
+    FileBlockTestAccessor::set_state(*block, io::FileBlock::State::DOWNLOADED);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        std::lock_guard<std::mutex> block_lock(block->_mutex);
+        cache.remove(block, cache_lock, block_lock, false);
+    }
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache.get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_TRUE(cache._deleting_blocks.empty());
+        EXPECT_EQ(queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(queue.get_capacity(cache_lock), 0);
+    }
+    EXPECT_EQ(cache._cur_cache_size, 0);
+
+    block.reset();
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, 
change_cache_type_keeps_removed_downloading_block_out_of_queues) {
+    std::string my_cache_path = caches_dir / 
"change_cache_type_removed_downloading_block" / "";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr size_t block_size = 4;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = 64;
+    settings.query_queue_elements = 16;
+    settings.index_queue_size = 64;
+    settings.index_queue_elements = 16;
+    settings.disposable_queue_size = 64;
+    settings.disposable_queue_elements = 16;
+    settings.capacity = 192;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = 64;
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = 
io::BlockFileCache::hash("change_cache_type_removed_downloading_block");
+
+    io::FileBlockSPtr block;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::DOWNLOADED,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        FileBlockTestAccessor::set_state(*cell->file_block, 
io::FileBlock::State::DOWNLOADING);
+        FileBlockTestAccessor::set_downloader_id(*cell->file_block, 1234);
+        block = cell->file_block;
+    }
+    // remove() takes a downloading block out of its LRU queue but leaves the 
cell in _files.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        std::lock_guard<std::mutex> block_lock(block->_mutex);
+        cache.remove(block, cache_lock, block_lock, false);
+    }
+
+    // The download finishes, and the block is converted to another cache type 
before its holder
+    // lets go of it, as BlockFileCacheTtlMgr does. It must stay out of every 
queue.
+    FileBlockTestAccessor::set_state(*block, io::FileBlock::State::DOWNLOADED);
+    auto& normal_queue = cache.get_queue(io::FileCacheType::NORMAL);
+    auto& ttl_queue = cache.get_queue(io::FileCacheType::TTL);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        cache.change_cache_type(key, 0, io::FileCacheType::TTL, cache_lock);
+        auto* cell = cache.get_cell(key, 0, cache_lock);
+        ASSERT_NE(cell, nullptr);
+        EXPECT_FALSE(cell->queue_iterator.has_value());
+        EXPECT_EQ(normal_queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(ttl_queue.get_elements_num(cache_lock), 0);
+    }
+
+    // The holder releases the block: erasing the cell must not touch any 
queue either.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        std::lock_guard<std::mutex> block_lock(block->_mutex);
+        cache.remove(block, cache_lock, block_lock, false);
+    }
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache.get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_EQ(normal_queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(normal_queue.get_capacity(cache_lock), 0);
+        EXPECT_EQ(ttl_queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(ttl_queue.get_capacity(cache_lock), 0);
+    }
+    EXPECT_EQ(cache._cur_cache_size, 0);
+
+    block.reset();
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, 
recycle_deleting_blocks_reclaims_stranded_busy_blocks) {
+    std::string my_cache_path = caches_dir / "recycle_deleting_blocks" / "";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr int num_blocks = 8;
+    constexpr size_t block_size = 1;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = num_blocks * block_size * 2;
+    settings.query_queue_elements = num_blocks + 16;
+    settings.index_queue_size = num_blocks * block_size * 2;
+    settings.index_queue_elements = num_blocks + 16;
+    settings.disposable_queue_size = num_blocks * block_size * 2;
+    settings.disposable_queue_elements = num_blocks + 16;
+    settings.capacity = num_blocks * block_size * 2;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = num_blocks * block_size * 2;
+
+    int64_t origin_qps_limit = config::file_cache_remove_block_qps_limit;
+    // Give the background GC thread no removal budget: the explicit 
recycle_deleting_blocks()
+    // calls below must be the only thing reclaiming blocks, otherwise the 
assertions race with
+    // it. The GC interval stays short, so ~BlockFileCache() never waits long 
for that thread.
+    config::file_cache_remove_block_qps_limit = 0;
+    Defer restore_qps_limit {[&] { config::file_cache_remove_block_qps_limit = 
origin_qps_limit; }};
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    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
+    // CachedRemoteFileReader, the block vector of BlockFileCacheTtlMgr.
+    std::vector<io::FileBlockSPtr> busy_refs;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        for (int i = 0; i < num_blocks; ++i) {
+            auto* cell = cache.add_cell(key, context, i * block_size, 
block_size,
+                                        io::FileBlock::State::DOWNLOADED, 
cache_lock);
+            ASSERT_NE(cell, nullptr);
+            busy_refs.push_back(cell->file_block);
+        }
+    }
+
+    std::atomic<int> storage_remove_calls {0};
+    auto sp = SyncPoint::get_instance();
+    SyncPoint::CallbackGuard guard;
+    sp->set_call_back(
+            "FSFileCacheStorage::remove",
+            [&storage_remove_calls](auto&& args) {
+                storage_remove_calls.fetch_add(1);
+                try_any_cast_ret<Status>(args)->second = true;
+            },
+            &guard);
+    sp->enable_processing();
+    Defer defer {[sp] {
+        sp->disable_processing();
+        sp->clear_all_call_backs();
+    }};
+
+    // Every block is busy, so clear() can only mark them and wait.
+    auto msg = cache.clear_file_cache_sync();
+    EXPECT_NE(msg.find("num_cells_wait_recycle=" + 
std::to_string(num_blocks)), std::string::npos);
+    EXPECT_EQ(storage_remove_calls.load(), 0);
+    EXPECT_EQ(cache._cur_cache_size, num_blocks * block_size);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache._deleting_blocks.size(), num_blocks);
+        for (int i = 0; i < num_blocks; ++i) {
+            auto* cell = cache.get_cell(key, i * block_size, cache_lock);
+            ASSERT_NE(cell, nullptr);
+            EXPECT_TRUE(cell->file_block->is_deleting());
+        }
+    }
+
+    // Blocks that are still referenced must survive the sweep.
+    EXPECT_EQ(cache.recycle_deleting_blocks(num_blocks), 0);
+    EXPECT_EQ(cache._cur_cache_size, num_blocks * block_size);
+
+    // Dropping the references this way never reaches 
FileBlock::release_cache_reference(), so
+    // without the sweep below the blocks would stay in _files and on disk 
forever.
+    busy_refs.clear();
+
+    EXPECT_EQ(cache.recycle_deleting_blocks(num_blocks), num_blocks);
+    EXPECT_EQ(cache._cur_cache_size, 0);
+    // The sweep runs under the cache lock, so it hands the files to the async 
recycle queue
+    // that run_background_gc() drains right after.
+    EXPECT_EQ(cache._recycle_keys.size_approx(), num_blocks);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_TRUE(cache._deleting_blocks.empty());
+        for (int i = 0; i < num_blocks; ++i) {
+            EXPECT_EQ(cache.get_cell(key, i * block_size, cache_lock), 
nullptr);
+        }
+    }
+
+    // Nothing left to do, and no block is looked at twice.
+    EXPECT_EQ(cache.recycle_deleting_blocks(num_blocks), 0);
+
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, recycle_deleting_blocks_honours_batch_limit) {
+    std::string my_cache_path = caches_dir / "recycle_deleting_blocks_batch" / 
"";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr int num_blocks = 10;
+    constexpr size_t block_size = 1;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = num_blocks * block_size * 2;
+    settings.query_queue_elements = num_blocks + 16;
+    settings.index_queue_size = num_blocks * block_size * 2;
+    settings.index_queue_elements = num_blocks + 16;
+    settings.disposable_queue_size = num_blocks * block_size * 2;
+    settings.disposable_queue_elements = num_blocks + 16;
+    settings.capacity = num_blocks * block_size * 2;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = num_blocks * block_size * 2;
+
+    int64_t origin_qps_limit = config::file_cache_remove_block_qps_limit;
+    // Give the background GC thread no removal budget: the explicit 
recycle_deleting_blocks()
+    // calls below must be the only thing reclaiming blocks, otherwise the 
assertions race with
+    // it. The GC interval stays short, so ~BlockFileCache() never waits long 
for that thread.
+    config::file_cache_remove_block_qps_limit = 0;
+    Defer restore_qps_limit {[&] { config::file_cache_remove_block_qps_limit = 
origin_qps_limit; }};
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = io::BlockFileCache::hash("recycle_deleting_blocks_batch");
+
+    std::vector<io::FileBlockSPtr> busy_refs;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        for (int i = 0; i < num_blocks; ++i) {
+            auto* cell = cache.add_cell(key, context, i * block_size, 
block_size,
+                                        io::FileBlock::State::DOWNLOADED, 
cache_lock);
+            ASSERT_NE(cell, nullptr);
+            busy_refs.push_back(cell->file_block);
+        }
+    }
+
+    cache.clear_file_cache_sync();
+    busy_refs.clear();
+
+    EXPECT_EQ(cache.recycle_deleting_blocks(0), 0);
+    EXPECT_EQ(cache.recycle_deleting_blocks(4), 4);
+    EXPECT_EQ(cache.recycle_deleting_blocks(4), 4);
+    EXPECT_EQ(cache.recycle_deleting_blocks(4), 2);
+    EXPECT_EQ(cache.recycle_deleting_blocks(4), 0);
+    EXPECT_EQ(cache._cur_cache_size, 0);
+
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, 
recycle_deleting_blocks_ignores_block_recreated_at_same_offset) {
+    std::string my_cache_path = caches_dir / 
"recycle_deleting_blocks_recreated" / "";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr size_t block_size = 4;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = 64;
+    settings.query_queue_elements = 16;
+    settings.index_queue_size = 64;
+    settings.index_queue_elements = 16;
+    settings.disposable_queue_size = 64;
+    settings.disposable_queue_elements = 16;
+    settings.capacity = 192;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = 64;
+
+    int64_t origin_qps_limit = config::file_cache_remove_block_qps_limit;
+    // Give the background GC thread no removal budget: the explicit 
recycle_deleting_blocks()
+    // calls below must be the only thing reclaiming blocks, otherwise the 
assertions race with
+    // it. The GC interval stays short, so ~BlockFileCache() never waits long 
for that thread.
+    config::file_cache_remove_block_qps_limit = 0;
+    Defer restore_qps_limit {[&] { config::file_cache_remove_block_qps_limit = 
origin_qps_limit; }};
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = io::BlockFileCache::hash("recycle_deleting_blocks_recreated");
+
+    // A reader holds the block while the cache is cleared, so the block is 
only marked deleting.
+    io::FileBlocks old_blocks;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::DOWNLOADED,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        old_blocks.push_back(cell->file_block);
+    }
+    cache.clear_file_cache_sync();
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache._deleting_blocks.size(), 1);
+    }
+
+    // The reader releases it through its holder, which removes the block and 
its key.
+    { io::FileBlocksHolder holder(std::move(old_blocks)); }
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache.get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_TRUE(cache._deleting_blocks.empty());
+    }
+
+    // The same offset is read again and cached by a new block before the next 
GC round.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::DOWNLOADED,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        EXPECT_FALSE(cell->file_block->is_deleting());
+    }
+
+    // The sweep must not take the new block for the one that was marked 
deleting.
+    EXPECT_EQ(cache.recycle_deleting_blocks(16), 0);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_NE(cache.get_cell(key, 0, cache_lock), nullptr);
+    }
+    EXPECT_EQ(cache._cur_cache_size, block_size);
+
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, recycle_deleting_blocks_reclaims_empty_blocks) {
+    std::string my_cache_path = caches_dir / "recycle_deleting_blocks_empty" / 
"";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr size_t block_size = 4;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = 64;
+    settings.query_queue_elements = 16;
+    settings.index_queue_size = 64;
+    settings.index_queue_elements = 16;
+    settings.disposable_queue_size = 64;
+    settings.disposable_queue_elements = 16;
+    settings.capacity = 192;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = 64;
+
+    int64_t origin_qps_limit = config::file_cache_remove_block_qps_limit;
+    // Give the background GC thread no removal budget: the explicit 
recycle_deleting_blocks()
+    // calls below must be the only thing reclaiming blocks, otherwise the 
assertions race with
+    // it. The GC interval stays short, so ~BlockFileCache() never waits long 
for that thread.
+    config::file_cache_remove_block_qps_limit = 0;
+    Defer restore_qps_limit {[&] { config::file_cache_remove_block_qps_limit = 
origin_qps_limit; }};
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = io::BlockFileCache::hash("recycle_deleting_blocks_empty");
+
+    // A block nobody started to download, referenced by something that is not 
a holder.
+    io::FileBlockSPtr busy_ref;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::EMPTY,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        busy_ref = cell->file_block;
+    }
+    cache.clear_file_cache_sync();
+    busy_ref.reset();
+
+    // Nobody will ever download it, so the sweep has to remove it.
+    EXPECT_EQ(cache.recycle_deleting_blocks(16), 1);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache.get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_TRUE(cache._deleting_blocks.empty());
+    }
+    EXPECT_EQ(cache._cur_cache_size, 0);
+
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
+TEST_F(BlockFileCacheTest, 
recycle_deleting_blocks_reclaims_block_removed_while_downloading) {
+    std::string my_cache_path = caches_dir / 
"recycle_deleting_blocks_downloading" / "";
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+    fs::create_directories(my_cache_path);
+
+    constexpr size_t block_size = 4;
+    io::FileCacheSettings settings;
+    settings.query_queue_size = 64;
+    settings.query_queue_elements = 16;
+    settings.index_queue_size = 64;
+    settings.index_queue_elements = 16;
+    settings.disposable_queue_size = 64;
+    settings.disposable_queue_elements = 16;
+    settings.capacity = 192;
+    settings.max_file_block_size = 64;
+    settings.max_query_cache_size = 64;
+
+    int64_t origin_qps_limit = config::file_cache_remove_block_qps_limit;
+    // Give the background GC thread no removal budget: the explicit 
recycle_deleting_blocks()
+    // calls below must be the only thing reclaiming blocks, otherwise the 
assertions race with
+    // it. The GC interval stays short, so ~BlockFileCache() never waits long 
for that thread.
+    config::file_cache_remove_block_qps_limit = 0;
+    Defer restore_qps_limit {[&] { config::file_cache_remove_block_qps_limit = 
origin_qps_limit; }};
+
+    io::BlockFileCache cache(my_cache_path, settings);
+    ASSERT_TRUE(cache.initialize());
+    wait_until_cache_ready(cache);
+
+    io::CacheContext context;
+    context.cache_type = io::FileCacheType::NORMAL;
+    auto key = io::BlockFileCache::hash("recycle_deleting_blocks_downloading");
+
+    io::FileBlockSPtr block;
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.add_cell(key, context, 0, block_size, 
io::FileBlock::State::DOWNLOADED,
+                                    cache_lock);
+        ASSERT_NE(cell, nullptr);
+        FileBlockTestAccessor::set_state(*cell->file_block, 
io::FileBlock::State::DOWNLOADING);
+        FileBlockTestAccessor::set_downloader_id(*cell->file_block, 1234);
+        block = cell->file_block;
+    }
+
+    // remove() cannot erase a block that is still downloading, it only marks 
it deleting.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        std::lock_guard<std::mutex> block_lock(block->_mutex);
+        cache.remove(block, cache_lock, block_lock, false);
+    }
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        EXPECT_EQ(cache._deleting_blocks.count(io::AccessKeyAndOffset(key, 
0)), 1);
+    }
+
+    // Drop the reference the way a non-holder does. The sweep still leaves 
the block to its
+    // downloader.
+    block.reset();
+    EXPECT_EQ(cache.recycle_deleting_blocks(16), 0);
+
+    // Once the download is done nobody would remove the block but the sweep.
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto* cell = cache.get_cell(key, 0, cache_lock);
+        ASSERT_NE(cell, nullptr);
+        FileBlockTestAccessor::set_state(*cell->file_block, 
io::FileBlock::State::DOWNLOADED);
+    }
+    EXPECT_EQ(cache.recycle_deleting_blocks(16), 1);
+    {
+        std::lock_guard<std::mutex> cache_lock(cache._mutex);
+        auto& queue = cache.get_queue(io::FileCacheType::NORMAL);
+        EXPECT_EQ(cache.get_cell(key, 0, cache_lock), nullptr);
+        EXPECT_TRUE(cache._deleting_blocks.empty());
+        EXPECT_EQ(queue.get_elements_num(cache_lock), 0);
+        EXPECT_EQ(queue.get_capacity(cache_lock), 0);
+    }
+    EXPECT_EQ(cache._cur_cache_size, 0);
+
+    if (fs::exists(my_cache_path)) {
+        fs::remove_all(my_cache_path);
+    }
+}
+
 TEST_F(BlockFileCacheTest, 
clear_file_cache_sync_factory_rejects_concurrent_sync_clear) {
     std::string my_cache_path = caches_dir / 
"clear_file_cache_sync_factory_busy" / "";
     if (fs::exists(my_cache_path)) {


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

Reply via email to