github-actions[bot] commented on code in PR #68348:
URL: https://github.com/apache/doris/pull/68348#discussion_r4261865827


##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -990,6 +994,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) {

Review Comment:
   [P2] Bound busy-key examination in each GC sweep. batch_limit limits only 
to_remove.size(), so busy entries do not consume the budget. If 
clear/try_release/reset_capacity marks a large set whose readers or TTL work 
still retain references, every GC round scans that entire set and does a 
get_cell() lookup for each entry while holding the cache-wide mutex. With the 
default 100ms interval this repeats ten times per second even when no block can 
be removed, blocking normal cache lookups and reservations; the previous GC 
drained its bounded queue outside this mutex. Bound examined candidates as well 
as removals and resume fairly across rounds rather than restarting the full 
busy-set scan.



##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -1029,8 +1095,11 @@ 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="
-                          << " offset=" << cell->file_block->offset();
-                cell->file_block->set_deleting();
+                          << cell->file_block->get_hash_value().to_string()
+                          << " offset=" << cell->file_block->offset()
+                          << " use_count=" << cell->file_block.use_count() << 
" state="
+                          << 
FileBlock::state_to_string(cell->file_block->state_unsafe());

Review Comment:
   [P2] Lock the block before logging its download state. A clear can run while 
a query still owns and downloads this block, which is exactly why this branch 
sees !releasable(). This newly added state_unsafe() call reads the non-atomic 
_download_state with only the cache mutex held, whereas get_or_set_downloader() 
and full-size finalize()/set_downloaded() write it under only 
FileBlock::_mutex. The operations can therefore overlap and form a C++ data 
race. Use state() or take the block mutex for the snapshot, preserving the 
existing cache-then-block lock order.



##########
be/test/io/cache/block_file_cache_test.cpp:
##########
@@ -3700,6 +3700,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_gc_interval = config::file_cache_background_gc_interval_ms;
+    // Park the background GC thread: the explicit recycle_deleting_blocks() 
calls below must be
+    // the only thing reclaiming blocks, otherwise the assertions race with it.
+    config::file_cache_background_gc_interval_ms = 3600 * 1000;

Review Comment:
   [P2] Park GC without introducing an hour-long shutdown wait. This pattern is 
used by all five new sweep tests, and the restoration Defer is declared before 
cache, so the one-hour interval remains active while ~BlockFileCache() joins 
GC. run_background_gc() samples the interval before acquiring _close_mtx and 
then waits without a predicate: if it is descheduled there, the destructor can 
set _close and notify first, after which GC misses the notification and waits 
the sampled hour. wait_until_cache_ready() only checks storage opening and does 
not synchronize this worker. This can stall BE unit tests for an hour. Keep the 
short interval and disable automatic removals with a zero QPS budget, or park 
through a synchronized hook; alternatively make the close wait predicate 
observe _close.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to