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


##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -851,23 +848,24 @@ FileBlocks BlockFileCache::split_range_into_cells(const 
UInt128Wrapper& hash,
     while (current_pos < end_pos_non_included) {
         current_size = std::min(remaining_size, _max_file_block_size);
         remaining_size -= current_size;
-        state = try_reserve(hash, context, current_pos, current_size, 
cache_lock)
+        state = try_reserve(hash, block_context, current_pos, current_size, 
cache_lock)

Review Comment:
   [P2] Skip reservation when metadata already forced no-cache
   
   The mixed-image guard above can set `state` to `SKIP_CACHE`, but this still 
calls `try_reserve()` for every hole. A successful reservation may 
synchronously evict real LRU cells and add `(hash, offset, size)` to the query 
context; the following branch then creates only a detached `SKIP_CACHE` block, 
so no cache cell owns or releases that reservation. Reading a hole in an 
inconsistent startup image can therefore evict useful data and consume query 
quota even though the hole is intentionally not cached. Please bypass 
`try_reserve()` when `state` is already `SKIP_CACHE`, and cover a 
full-cache/query-limit case.



##########
be/src/io/cache/fs_file_cache_storage.cpp:
##########
@@ -707,37 +972,54 @@ void 
FSFileCacheStorage::load_cache_info_into_memory(BlockFileCache* _mgr) const
         batch_load_buffer.clear();
     };
 
-    auto scan_file_cache = [&](std::filesystem::directory_iterator& key_it) {
+    auto scan_file_cache = [&](const std::vector<std::string>& key_paths) {
         TEST_SYNC_POINT_CALLBACK("BlockFileCache::TmpFile1");
-        for (; key_it != std::filesystem::directory_iterator(); ++key_it) {
-            auto key_with_suffix = key_it->path().filename().native();
-            auto delim_pos = key_with_suffix.find('_');
-            DCHECK(delim_pos != std::string::npos);
-            std::string key_str = key_with_suffix.substr(0, delim_pos);
-            std::string expiration_time_str = key_with_suffix.substr(delim_pos 
+ 1);
-            auto hash = 
UInt128Wrapper(vectorized::unhex_uint<uint128_t>(key_str.c_str()));
+        for (const auto& key_path_str : key_paths) {
+            std::filesystem::path key_path(key_path_str);
             std::error_code ec;
-            std::filesystem::directory_iterator offset_it(key_it->path(), ec);
-            if (ec) [[unlikely]] {
-                LOG(WARNING) << "filesystem error, failed to remove file, 
file=" << key_it->path()
-                             << " error=" << ec.message();
+            if (!std::filesystem::is_directory(key_path, ec)) {
+                if (ec) {
+                    auto st = Status::IOError("failed to stat cache key 
directory {}, error={}",
+                                              key_path.native(), ec.message());
+                    LOG(WARNING) << st;
+                    remember_load_error(st);
+                }
+                continue;
+            }
+
+            UInt128Wrapper hash;
+            uint64_t expiration_time = 0;
+            if (!parse_key_dir_name(key_path.filename().native(), &hash, 
&expiration_time)) {
+                continue;
+            }
+
+            std::vector<std::string> offset_paths;
+            auto list_st = collect_directory_entries(key_path, offset_paths);

Review Comment:
   [P2] Keep startup directory discovery memory-bounded
   
   This rewrite collects every block path in a key directory before the 
10,000-entry `batch_load_buffer` can process anything; the prefix loop 
similarly retains every key path in that prefix. The base loader streamed both 
directory iterators, so a highly fragmented multi-TiB object can now allocate 
millions of copied full paths (hundreds of MiB) during startup. This is a 
separate instance from the existing periodic-scan enumeration thread, which 
explicitly cited the startup loader as bounded. Please preserve the new error 
accounting while consuming directory entries in bounded batches at both levels.



##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2292,417 @@ void BlockFileCache::run_background_ttl_gc() {
     }
 }
 
+void BlockFileCache::run_background_disk_scan_repair() {
+    Thread::set_self_name("run_disk_scan");
+    if (config::file_cache_disk_scan_initial_jitter_ms > 0) {
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution<int64_t> dist(
+                0, std::max<int64_t>(0, 
config::file_cache_disk_scan_initial_jitter_ms));
+        std::unique_lock close_lock(_close_mtx);
+        _close_cv.wait_for(close_lock, std::chrono::milliseconds(dist(gen)),
+                           [this] { return 
_close.load(std::memory_order_relaxed); });
+    }
+
+    while (!_close) {
+        if (!_async_open_done) {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock, std::chrono::milliseconds(100),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            continue;
+        }
+
+        if (config::enable_file_cache_disk_scan_repair) {
+            auto result = run_disk_scan_repair_once();
+            if (result.repaired_files > 0 || result.repaired_dirs > 0) {
+                LOG(INFO) << "File cache disk scan repair finished. path=" << 
_cache_base_path
+                          << " repaired_files=" << result.repaired_files
+                          << " repaired_dirs=" << result.repaired_dirs
+                          << " skipped_candidates=" << 
result.skipped_candidates;
+            }
+        }
+
+        int64_t interval_ms = config::file_cache_disk_scan_interval_ms;
+        TEST_SYNC_POINT_CALLBACK("BlockFileCache::set_disk_scan_sleep_time", 
&interval_ms);
+        {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock,
+                               
std::chrono::milliseconds(std::max<int64_t>(interval_ms, 1)),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            if (_close) {
+                break;
+            }
+        }
+    }
+}
+
+DiskScanRoundResult BlockFileCache::run_disk_scan_repair_once() {
+    DiskScanRoundResult result;
+    if (!config::enable_file_cache_disk_scan_repair ||
+        _storage->get_type() != FileCacheStorageType::DISK || 
!_async_open_done ||
+        !_async_open_success) {
+        return result;
+    }
+    auto should_cancel = [this]() {
+        return _close.load(std::memory_order_relaxed) ||
+               !config::enable_file_cache_disk_scan_repair;
+    };
+
+    _disk_scan_scan_limiter->reset(std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   0);
+    _disk_scan_repair_limiter->reset(
+            std::max<int64_t>(0, config::file_cache_disk_scan_repair_rate_qps),
+            std::max<int64_t>(0, 
config::file_cache_disk_scan_repair_rate_qps), 0);
+
+    int64_t duration_ns = 0;
+    {
+        SCOPED_RAW_TIMER(&duration_ns);
+        std::vector<DiskScanRepairAction> actions;
+        actions.reserve(std::min<int64_t>(
+                std::max<int64_t>(1, 
config::file_cache_disk_scan_max_pending_repairs), 1024));
+        std::vector<DiskScanKeyDirEntry> ttl_dirs_for_hash;
+
+        std::string last_prefix;
+        auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_key_dirs;
+            if (last_prefix != entry.prefix) {
+                last_prefix = entry.prefix;
+                ++result.scanned_prefix_dirs;
+            }
+            if (!ttl_dirs_for_hash.empty() && ttl_dirs_for_hash.front().hash 
!= entry.hash) {
+                finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            }
+            run_disk_scan_checkers(entry, &actions, &ttl_dirs_for_hash, 
&result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto on_block_file = [&](const DiskScanBlockFileEntry& entry) -> 
Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_files;
+            run_disk_scan_checkers(entry, &actions, &result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto st = _storage->scan_disk_cache(on_key_dir, on_block_file,
+                                            _disk_scan_scan_limiter.get(), 
should_cancel);
+        if (!st.ok() && !st.is<ErrorCode::CANCELLED>()) {
+            LOG(WARNING) << "Failed to scan file cache disk. path=" << 
_cache_base_path
+                         << ", error=" << st;
+        }
+        if (!should_cancel()) {
+            finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            drain_disk_scan_actions(&actions, &result);
+        } else {
+            result.skipped_candidates += actions.size();
+            actions.clear();
+        }
+    }
+
+    *_disk_scan_latency_us << (duration_ns / 1000);
+    *_disk_scan_rounds << 1;
+    *_disk_scan_scanned_prefix_dirs << result.scanned_prefix_dirs;
+    *_disk_scan_scanned_key_dirs << result.scanned_key_dirs;
+    *_disk_scan_scanned_files << result.scanned_files;
+    *_disk_scan_candidates << result.candidates;
+    *_disk_scan_repaired_files << result.repaired_files;
+    *_disk_scan_repaired_dirs << result.repaired_dirs;
+    *_disk_scan_skipped_candidates << result.skipped_candidates;
+    *_disk_scan_deleted_bytes << result.deleted_bytes;
+    return result;
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanKeyDirEntry& entry,
+                                            std::vector<DiskScanRepairAction>* 
/* actions */,
+                                            std::vector<DiskScanKeyDirEntry>* 
ttl_dirs_for_hash,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+        return;
+    }
+    if (entry.expiration_time > 0) {
+        DCHECK(ttl_dirs_for_hash->empty() || ttl_dirs_for_hash->front().hash 
== entry.hash);
+        ttl_dirs_for_hash->push_back(entry);
+    }
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanBlockFileEntry& 
entry,
+                                            std::vector<DiskScanRepairAction>* 
actions,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_disk_memory_checker) {
+        return;
+    }
+
+    if (is_younger_than_grace(entry.identity, 
config::file_cache_disk_scan_grace_seconds)) {
+        return;
+    }
+
+    if (entry.is_tmp) {
+        bool skip = false;
+        {
+            SCOPED_CACHE_LOCK(_mutex, this);
+            auto file_iter = _files.find(entry.hash);
+            if (file_iter != _files.end()) {
+                auto block_iter = file_iter->second.find(entry.offset);
+                if (block_iter != file_iter->second.end()) {
+                    std::lock_guard 
block_lock(block_iter->second.file_block->_mutex);
+                    auto state = 
block_iter->second.file_block->state_unlock(block_lock);
+                    skip = state == FileBlock::State::EMPTY ||
+                           state == FileBlock::State::DOWNLOADING;
+                }
+            }
+        }
+        if (skip || _storage->has_active_writer(entry.hash, entry.offset)) {
+            return;
+        }
+        actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                            entry.expiration_time, entry.file_path, 
entry.key_dir, entry.identity,
+                            DiskScanRepairReason::OLD_TMP_FILE});
+        ++result->candidates;
+        return;
+    }
+
+    bool disk_only = false;
+    {
+        SCOPED_CACHE_LOCK(_mutex, this);
+        auto file_iter = _files.find(entry.hash);
+        disk_only = file_iter == _files.end() || 
!file_iter->second.contains(entry.offset);
+    }
+    if (!disk_only) {
+        return;
+    }
+    actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                        entry.expiration_time, entry.file_path, entry.key_dir, 
entry.identity,
+                        DiskScanRepairReason::DISK_ONLY_FILE});
+    ++result->candidates;
+}
+
+void BlockFileCache::finalize_disk_scan_ttl_checker(
+        std::vector<DiskScanKeyDirEntry>* ttl_dirs_for_hash,
+        std::vector<DiskScanRepairAction>* actions, DiskScanRoundResult* 
result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker || 
ttl_dirs_for_hash->empty()) {
+        ttl_dirs_for_hash->clear();
+        return;
+    }
+
+    auto& dirs = *ttl_dirs_for_hash;
+    auto ttl_group_size = dirs.size();
+    TEST_SYNC_POINT_CALLBACK("BlockFileCache::finalize_disk_scan_ttl_group", 
&ttl_group_size);
+    const auto hash = dirs.front().hash;
+    std::sort(dirs.begin(), dirs.end(), [](const auto& lhs, const auto& rhs) {
+        return lhs.expiration_time < rhs.expiration_time;
+    });
+    dirs.erase(std::unique(dirs.begin(), dirs.end(),
+                           [](const auto& lhs, const auto& rhs) {
+                               return lhs.expiration_time == 
rhs.expiration_time;
+                           }),
+               dirs.end());
+    uint64_t canonical_expiration_time = 0;
+    if (dirs.size() > 1 &&
+        disk_scan_hash_has_stable_canonical_expiration(hash, 
&canonical_expiration_time)) {
+        bool canonical_on_disk = false;
+        for (const auto& dir : dirs) {
+            canonical_on_disk |= dir.expiration_time == 
canonical_expiration_time;
+        }
+        if (canonical_on_disk) {
+            for (const auto& dir : dirs) {
+                if (dir.expiration_time == canonical_expiration_time) {
+                    continue;
+                }
+                actions->push_back({DiskScanRepairActionType::DELETE_DIR, 
hash, std::nullopt,
+                                    dir.expiration_time, dir.key_dir, 
dir.key_dir, std::nullopt,
+                                    DiskScanRepairReason::TTL_DUPLICATE_DIR});
+                ++result->candidates;
+                if (actions->size() >=
+                    static_cast<size_t>(std::max<int64_t>(
+                            1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                    drain_disk_scan_actions(actions, result);
+                }
+            }
+        }
+    }
+    ttl_dirs_for_hash->clear();
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time) {
+    SCOPED_CACHE_LOCK(_mutex, this);
+    return disk_scan_hash_has_stable_canonical_expiration_unlocked(hash, 
canonical_expiration_time,
+                                                                   cache_lock);
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration_unlocked(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time,
+        std::lock_guard<std::mutex>& /* cache_lock */) {
+    auto file_iter = _files.find(hash);
+    if (file_iter == _files.end() || file_iter->second.empty()) {
+        return false;
+    }
+
+    bool has_canonical = false;
+    for (auto& [_, cell] : file_iter->second) {
+        std::lock_guard block_lock(cell.file_block->_mutex);
+        if (cell.file_block->state_unlock(block_lock) != 
FileBlock::State::DOWNLOADED) {
+            return false;
+        }
+        auto expiration_time = cell.file_block->expiration_time();
+        if (!has_canonical) {
+            *canonical_expiration_time = expiration_time;
+            has_canonical = true;
+        } else if (*canonical_expiration_time != expiration_time) {
+            return false;
+        }
+    }
+    return has_canonical;
+}
+
+void 
BlockFileCache::drain_disk_scan_actions(std::vector<DiskScanRepairAction>* 
actions,
+                                             DiskScanRoundResult* result) {
+    for (const auto& action : *actions) {
+        if (_close || !config::enable_file_cache_disk_scan_repair) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        auto should_cancel = [this]() {
+            return _close.load(std::memory_order_relaxed) ||
+                   !config::enable_file_cache_disk_scan_repair;
+        };
+        _disk_scan_repair_limiter->add(1, should_cancel);
+        if (should_cancel()) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        std::filesystem::path quarantine_path;
+        auto st = quarantine_disk_scan_action(action, &quarantine_path);
+        if (!st.ok()) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        if (action.type == DiskScanRepairActionType::DELETE_FILE) {
+            st = _storage->delete_file_for_disk_scan(quarantine_path);
+            if (st.ok()) {
+                ++result->repaired_files;
+                if (action.observed_identity) {
+                    result->deleted_bytes += action.observed_identity->size;
+                }
+            } else {
+                LOG(WARNING) << "Failed to delete quarantined file cache file. 
path="
+                             << quarantine_path << ", error=" << st;
+                ++result->skipped_candidates;
+            }
+        } else {
+            st = _storage->delete_dir_for_disk_scan(quarantine_path);

Review Comment:
   [P1] Rate-limit and cancel recursive quarantine deletion
   
   A whole stale TTL key directory is represented by one action and consumes 
one repair token, but this call forwards to `std::filesystem::remove_all()`. A 
multi-TiB object can put millions of block files behind that single token; once 
deletion starts, neither shutdown nor the global/per-checker switches are 
observed until the entire tree is gone, so repair QPS is bypassed and the cache 
destructor can still block on the worker join. This is after quarantine, 
separate from the existing traversal/limiter-wait thread. Please unlink 
children in cancellable, rate-limited batches and make a partially drained 
quarantine recoverable by a later round.



##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2292,417 @@ void BlockFileCache::run_background_ttl_gc() {
     }
 }
 
+void BlockFileCache::run_background_disk_scan_repair() {
+    Thread::set_self_name("run_disk_scan");
+    if (config::file_cache_disk_scan_initial_jitter_ms > 0) {
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution<int64_t> dist(
+                0, std::max<int64_t>(0, 
config::file_cache_disk_scan_initial_jitter_ms));
+        std::unique_lock close_lock(_close_mtx);
+        _close_cv.wait_for(close_lock, std::chrono::milliseconds(dist(gen)),
+                           [this] { return 
_close.load(std::memory_order_relaxed); });
+    }
+
+    while (!_close) {
+        if (!_async_open_done) {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock, std::chrono::milliseconds(100),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            continue;
+        }
+
+        if (config::enable_file_cache_disk_scan_repair) {
+            auto result = run_disk_scan_repair_once();
+            if (result.repaired_files > 0 || result.repaired_dirs > 0) {
+                LOG(INFO) << "File cache disk scan repair finished. path=" << 
_cache_base_path
+                          << " repaired_files=" << result.repaired_files
+                          << " repaired_dirs=" << result.repaired_dirs
+                          << " skipped_candidates=" << 
result.skipped_candidates;
+            }
+        }
+
+        int64_t interval_ms = config::file_cache_disk_scan_interval_ms;
+        TEST_SYNC_POINT_CALLBACK("BlockFileCache::set_disk_scan_sleep_time", 
&interval_ms);
+        {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock,
+                               
std::chrono::milliseconds(std::max<int64_t>(interval_ms, 1)),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            if (_close) {
+                break;
+            }
+        }
+    }
+}
+
+DiskScanRoundResult BlockFileCache::run_disk_scan_repair_once() {
+    DiskScanRoundResult result;
+    if (!config::enable_file_cache_disk_scan_repair ||
+        _storage->get_type() != FileCacheStorageType::DISK || 
!_async_open_done ||
+        !_async_open_success) {
+        return result;
+    }
+    auto should_cancel = [this]() {
+        return _close.load(std::memory_order_relaxed) ||
+               !config::enable_file_cache_disk_scan_repair;
+    };
+
+    _disk_scan_scan_limiter->reset(std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   0);
+    _disk_scan_repair_limiter->reset(
+            std::max<int64_t>(0, config::file_cache_disk_scan_repair_rate_qps),
+            std::max<int64_t>(0, 
config::file_cache_disk_scan_repair_rate_qps), 0);
+
+    int64_t duration_ns = 0;
+    {
+        SCOPED_RAW_TIMER(&duration_ns);
+        std::vector<DiskScanRepairAction> actions;
+        actions.reserve(std::min<int64_t>(
+                std::max<int64_t>(1, 
config::file_cache_disk_scan_max_pending_repairs), 1024));
+        std::vector<DiskScanKeyDirEntry> ttl_dirs_for_hash;
+
+        std::string last_prefix;
+        auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_key_dirs;
+            if (last_prefix != entry.prefix) {
+                last_prefix = entry.prefix;
+                ++result.scanned_prefix_dirs;
+            }
+            if (!ttl_dirs_for_hash.empty() && ttl_dirs_for_hash.front().hash 
!= entry.hash) {
+                finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            }
+            run_disk_scan_checkers(entry, &actions, &ttl_dirs_for_hash, 
&result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto on_block_file = [&](const DiskScanBlockFileEntry& entry) -> 
Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_files;
+            run_disk_scan_checkers(entry, &actions, &result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto st = _storage->scan_disk_cache(on_key_dir, on_block_file,
+                                            _disk_scan_scan_limiter.get(), 
should_cancel);
+        if (!st.ok() && !st.is<ErrorCode::CANCELLED>()) {
+            LOG(WARNING) << "Failed to scan file cache disk. path=" << 
_cache_base_path
+                         << ", error=" << st;
+        }
+        if (!should_cancel()) {
+            finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            drain_disk_scan_actions(&actions, &result);
+        } else {
+            result.skipped_candidates += actions.size();
+            actions.clear();
+        }
+    }
+
+    *_disk_scan_latency_us << (duration_ns / 1000);
+    *_disk_scan_rounds << 1;
+    *_disk_scan_scanned_prefix_dirs << result.scanned_prefix_dirs;
+    *_disk_scan_scanned_key_dirs << result.scanned_key_dirs;
+    *_disk_scan_scanned_files << result.scanned_files;
+    *_disk_scan_candidates << result.candidates;
+    *_disk_scan_repaired_files << result.repaired_files;
+    *_disk_scan_repaired_dirs << result.repaired_dirs;
+    *_disk_scan_skipped_candidates << result.skipped_candidates;
+    *_disk_scan_deleted_bytes << result.deleted_bytes;
+    return result;
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanKeyDirEntry& entry,
+                                            std::vector<DiskScanRepairAction>* 
/* actions */,
+                                            std::vector<DiskScanKeyDirEntry>* 
ttl_dirs_for_hash,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+        return;
+    }
+    if (entry.expiration_time > 0) {
+        DCHECK(ttl_dirs_for_hash->empty() || ttl_dirs_for_hash->front().hash 
== entry.hash);
+        ttl_dirs_for_hash->push_back(entry);
+    }
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanBlockFileEntry& 
entry,
+                                            std::vector<DiskScanRepairAction>* 
actions,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_disk_memory_checker) {
+        return;
+    }
+
+    if (is_younger_than_grace(entry.identity, 
config::file_cache_disk_scan_grace_seconds)) {
+        return;
+    }
+
+    if (entry.is_tmp) {
+        bool skip = false;
+        {
+            SCOPED_CACHE_LOCK(_mutex, this);
+            auto file_iter = _files.find(entry.hash);
+            if (file_iter != _files.end()) {
+                auto block_iter = file_iter->second.find(entry.offset);
+                if (block_iter != file_iter->second.end()) {
+                    std::lock_guard 
block_lock(block_iter->second.file_block->_mutex);
+                    auto state = 
block_iter->second.file_block->state_unlock(block_lock);
+                    skip = state == FileBlock::State::EMPTY ||
+                           state == FileBlock::State::DOWNLOADING;
+                }
+            }
+        }
+        if (skip || _storage->has_active_writer(entry.hash, entry.offset)) {
+            return;
+        }
+        actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                            entry.expiration_time, entry.file_path, 
entry.key_dir, entry.identity,
+                            DiskScanRepairReason::OLD_TMP_FILE});
+        ++result->candidates;
+        return;
+    }
+
+    bool disk_only = false;
+    {
+        SCOPED_CACHE_LOCK(_mutex, this);
+        auto file_iter = _files.find(entry.hash);
+        disk_only = file_iter == _files.end() || 
!file_iter->second.contains(entry.offset);
+    }
+    if (!disk_only) {
+        return;
+    }
+    actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                        entry.expiration_time, entry.file_path, entry.key_dir, 
entry.identity,
+                        DiskScanRepairReason::DISK_ONLY_FILE});
+    ++result->candidates;
+}
+
+void BlockFileCache::finalize_disk_scan_ttl_checker(
+        std::vector<DiskScanKeyDirEntry>* ttl_dirs_for_hash,
+        std::vector<DiskScanRepairAction>* actions, DiskScanRoundResult* 
result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker || 
ttl_dirs_for_hash->empty()) {
+        ttl_dirs_for_hash->clear();
+        return;
+    }
+
+    auto& dirs = *ttl_dirs_for_hash;
+    auto ttl_group_size = dirs.size();
+    TEST_SYNC_POINT_CALLBACK("BlockFileCache::finalize_disk_scan_ttl_group", 
&ttl_group_size);
+    const auto hash = dirs.front().hash;
+    std::sort(dirs.begin(), dirs.end(), [](const auto& lhs, const auto& rhs) {
+        return lhs.expiration_time < rhs.expiration_time;
+    });
+    dirs.erase(std::unique(dirs.begin(), dirs.end(),
+                           [](const auto& lhs, const auto& rhs) {
+                               return lhs.expiration_time == 
rhs.expiration_time;
+                           }),
+               dirs.end());
+    uint64_t canonical_expiration_time = 0;
+    if (dirs.size() > 1 &&
+        disk_scan_hash_has_stable_canonical_expiration(hash, 
&canonical_expiration_time)) {
+        bool canonical_on_disk = false;
+        for (const auto& dir : dirs) {
+            canonical_on_disk |= dir.expiration_time == 
canonical_expiration_time;
+        }
+        if (canonical_on_disk) {
+            for (const auto& dir : dirs) {
+                if (dir.expiration_time == canonical_expiration_time) {
+                    continue;
+                }
+                actions->push_back({DiskScanRepairActionType::DELETE_DIR, 
hash, std::nullopt,
+                                    dir.expiration_time, dir.key_dir, 
dir.key_dir, std::nullopt,
+                                    DiskScanRepairReason::TTL_DUPLICATE_DIR});
+                ++result->candidates;
+                if (actions->size() >=
+                    static_cast<size_t>(std::max<int64_t>(
+                            1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                    drain_disk_scan_actions(actions, result);
+                }
+            }
+        }
+    }
+    ttl_dirs_for_hash->clear();
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time) {
+    SCOPED_CACHE_LOCK(_mutex, this);
+    return disk_scan_hash_has_stable_canonical_expiration_unlocked(hash, 
canonical_expiration_time,
+                                                                   cache_lock);
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration_unlocked(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time,
+        std::lock_guard<std::mutex>& /* cache_lock */) {
+    auto file_iter = _files.find(hash);
+    if (file_iter == _files.end() || file_iter->second.empty()) {
+        return false;
+    }
+
+    bool has_canonical = false;
+    for (auto& [_, cell] : file_iter->second) {
+        std::lock_guard block_lock(cell.file_block->_mutex);
+        if (cell.file_block->state_unlock(block_lock) != 
FileBlock::State::DOWNLOADED) {
+            return false;
+        }
+        auto expiration_time = cell.file_block->expiration_time();
+        if (!has_canonical) {
+            *canonical_expiration_time = expiration_time;
+            has_canonical = true;
+        } else if (*canonical_expiration_time != expiration_time) {
+            return false;
+        }
+    }
+    return has_canonical;
+}
+
+void 
BlockFileCache::drain_disk_scan_actions(std::vector<DiskScanRepairAction>* 
actions,
+                                             DiskScanRoundResult* result) {
+    for (const auto& action : *actions) {
+        if (_close || !config::enable_file_cache_disk_scan_repair) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        auto should_cancel = [this]() {
+            return _close.load(std::memory_order_relaxed) ||
+                   !config::enable_file_cache_disk_scan_repair;
+        };
+        _disk_scan_repair_limiter->add(1, should_cancel);
+        if (should_cancel()) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        std::filesystem::path quarantine_path;
+        auto st = quarantine_disk_scan_action(action, &quarantine_path);
+        if (!st.ok()) {
+            ++result->skipped_candidates;
+            continue;
+        }
+        if (action.type == DiskScanRepairActionType::DELETE_FILE) {
+            st = _storage->delete_file_for_disk_scan(quarantine_path);
+            if (st.ok()) {
+                ++result->repaired_files;
+                if (action.observed_identity) {
+                    result->deleted_bytes += action.observed_identity->size;
+                }
+            } else {
+                LOG(WARNING) << "Failed to delete quarantined file cache file. 
path="
+                             << quarantine_path << ", error=" << st;
+                ++result->skipped_candidates;
+            }
+        } else {
+            st = _storage->delete_dir_for_disk_scan(quarantine_path);
+            if (st.ok()) {
+                ++result->repaired_dirs;
+            } else {
+                LOG(WARNING) << "Failed to delete quarantined file cache 
directory. path="
+                             << quarantine_path << ", error=" << st;
+                ++result->skipped_candidates;
+            }
+        }
+    }
+    actions->clear();
+}
+
+bool BlockFileCache::disk_scan_file_is_deletable(const DiskScanRepairAction& 
action) {
+    SCOPED_CACHE_LOCK(_mutex, this);
+    return disk_scan_file_is_deletable_unlocked(action, cache_lock);
+}
+
+bool BlockFileCache::disk_scan_file_is_deletable_unlocked(
+        const DiskScanRepairAction& action, std::lock_guard<std::mutex>& /* 
cache_lock */) {
+    if (!action.offset) {
+        return false;
+    }
+    auto file_iter = _files.find(action.hash);
+    if (file_iter != _files.end()) {
+        auto block_iter = file_iter->second.find(*action.offset);
+        if (block_iter != file_iter->second.end()) {
+            if (action.reason != DiskScanRepairReason::OLD_TMP_FILE) {
+                return false;
+            }
+            std::lock_guard block_lock(block_iter->second.file_block->_mutex);
+            auto state = 
block_iter->second.file_block->state_unlock(block_lock);
+            if (state == FileBlock::State::EMPTY || state == 
FileBlock::State::DOWNLOADING) {
+                return false;
+            }
+        }
+    }
+    if (_storage->has_active_writer(action.hash, *action.offset)) {
+        return false;
+    }
+    auto identity = get_current_identity(action.path);
+    if (!identity) {
+        return false;
+    }
+    if (action.observed_identity && !same_identity(*action.observed_identity, 
*identity)) {
+        return false;
+    }
+    return !is_younger_than_grace(*identity, 
config::file_cache_disk_scan_grace_seconds);
+}
+
+bool BlockFileCache::disk_scan_dir_is_deletable(const DiskScanRepairAction& 
action) {
+    SCOPED_CACHE_LOCK(_mutex, this);
+    return disk_scan_dir_is_deletable_unlocked(action, cache_lock);
+}
+
+bool BlockFileCache::disk_scan_dir_is_deletable_unlocked(const 
DiskScanRepairAction& action,
+                                                         
std::lock_guard<std::mutex>& cache_lock) {
+    uint64_t canonical_expiration_time = 0;
+    if (!disk_scan_hash_has_stable_canonical_expiration_unlocked(
+                action.hash, &canonical_expiration_time, cache_lock)) {
+        return false;
+    }
+    if (_storage->has_active_writer_for_hash(action.hash)) {
+        return false;
+    }
+    if (action.expiration_time == canonical_expiration_time) {
+        return false;
+    }
+    std::filesystem::path canonical_path =
+            action.path.parent_path() /
+            (action.hash.to_string() + "_" + 
std::to_string(canonical_expiration_time));
+    bool exists = false;
+    auto st = _storage->exists_for_disk_scan(canonical_path, &exists);
+    
TEST_SYNC_POINT_CALLBACK("BlockFileCache::disk_scan_canonical_path_exists", 
&st, &exists);
+    if (!st.ok()) {
+        LOG(WARNING) << "Failed to check canonical file cache directory. 
path=" << canonical_path
+                     << ", error=" << st;
+        return false;
+    }
+    return exists;
+}
+
+Status BlockFileCache::quarantine_disk_scan_action(const DiskScanRepairAction& 
action,
+                                                   std::filesystem::path* 
quarantine_path) {
+    TEST_SYNC_POINT_CALLBACK("BlockFileCache::before_disk_scan_quarantine", 
&action);
+    SCOPED_CACHE_LOCK(_mutex, this);
+    bool deletable = action.type == DiskScanRepairActionType::DELETE_FILE
+                             ? disk_scan_file_is_deletable_unlocked(action, 
cache_lock)
+                             : disk_scan_dir_is_deletable_unlocked(action, 
cache_lock);
+    if (!deletable) {
+        return Status::InternalError("disk scan repair action is no longer 
safe");
+    }
+
+    static std::atomic<uint64_t> quarantine_id {0};
+    *quarantine_path = action.path.parent_path() /
+                       (action.path.filename().native() + 
".disk_scan_quarantine." +

Review Comment:
   [P2] Recover quarantines left by a crash or delete failure
   
   The rename happens before deletion, but these generated siblings have no 
recovery path if deletion fails or the BE exits in between. A quarantined key 
directory is rejected by `parse_key_dir_name()` and becomes invisible forever, 
while a file such as `0.disk_scan_quarantine.7` is permissively parsed as 
offset 0 by `parse_filename_suffix_to_cache_type()` and can be loaded as a 
`DOWNLOADED` cell whose canonical filename does not exist; later repair then 
skips it because that offset is in `_files`. The process-static ID also resets 
after restart. Please use a dedicated sweepable quarantine namespace (or 
explicitly retry these names), make normal filename parsing require full 
numeric/exact-suffix consumption, and add delete-error plus crash/restart 
coverage.



##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2292,417 @@ void BlockFileCache::run_background_ttl_gc() {
     }
 }
 
+void BlockFileCache::run_background_disk_scan_repair() {
+    Thread::set_self_name("run_disk_scan");
+    if (config::file_cache_disk_scan_initial_jitter_ms > 0) {
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution<int64_t> dist(
+                0, std::max<int64_t>(0, 
config::file_cache_disk_scan_initial_jitter_ms));
+        std::unique_lock close_lock(_close_mtx);
+        _close_cv.wait_for(close_lock, std::chrono::milliseconds(dist(gen)),
+                           [this] { return 
_close.load(std::memory_order_relaxed); });
+    }
+
+    while (!_close) {
+        if (!_async_open_done) {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock, std::chrono::milliseconds(100),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            continue;
+        }
+
+        if (config::enable_file_cache_disk_scan_repair) {
+            auto result = run_disk_scan_repair_once();
+            if (result.repaired_files > 0 || result.repaired_dirs > 0) {
+                LOG(INFO) << "File cache disk scan repair finished. path=" << 
_cache_base_path
+                          << " repaired_files=" << result.repaired_files
+                          << " repaired_dirs=" << result.repaired_dirs
+                          << " skipped_candidates=" << 
result.skipped_candidates;
+            }
+        }
+
+        int64_t interval_ms = config::file_cache_disk_scan_interval_ms;
+        TEST_SYNC_POINT_CALLBACK("BlockFileCache::set_disk_scan_sleep_time", 
&interval_ms);
+        {
+            std::unique_lock close_lock(_close_mtx);
+            _close_cv.wait_for(close_lock,
+                               
std::chrono::milliseconds(std::max<int64_t>(interval_ms, 1)),
+                               [this] { return 
_close.load(std::memory_order_relaxed); });
+            if (_close) {
+                break;
+            }
+        }
+    }
+}
+
+DiskScanRoundResult BlockFileCache::run_disk_scan_repair_once() {
+    DiskScanRoundResult result;
+    if (!config::enable_file_cache_disk_scan_repair ||
+        _storage->get_type() != FileCacheStorageType::DISK || 
!_async_open_done ||
+        !_async_open_success) {
+        return result;
+    }
+    auto should_cancel = [this]() {
+        return _close.load(std::memory_order_relaxed) ||
+               !config::enable_file_cache_disk_scan_repair;
+    };
+
+    _disk_scan_scan_limiter->reset(std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   std::max<int64_t>(0, 
config::file_cache_disk_scan_scan_rate_qps),
+                                   0);
+    _disk_scan_repair_limiter->reset(
+            std::max<int64_t>(0, config::file_cache_disk_scan_repair_rate_qps),
+            std::max<int64_t>(0, 
config::file_cache_disk_scan_repair_rate_qps), 0);
+
+    int64_t duration_ns = 0;
+    {
+        SCOPED_RAW_TIMER(&duration_ns);
+        std::vector<DiskScanRepairAction> actions;
+        actions.reserve(std::min<int64_t>(
+                std::max<int64_t>(1, 
config::file_cache_disk_scan_max_pending_repairs), 1024));
+        std::vector<DiskScanKeyDirEntry> ttl_dirs_for_hash;
+
+        std::string last_prefix;
+        auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_key_dirs;
+            if (last_prefix != entry.prefix) {
+                last_prefix = entry.prefix;
+                ++result.scanned_prefix_dirs;
+            }
+            if (!ttl_dirs_for_hash.empty() && ttl_dirs_for_hash.front().hash 
!= entry.hash) {
+                finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            }
+            run_disk_scan_checkers(entry, &actions, &ttl_dirs_for_hash, 
&result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto on_block_file = [&](const DiskScanBlockFileEntry& entry) -> 
Status {
+            if (should_cancel()) {
+                return Status::Cancelled("file cache disk scan cancelled");
+            }
+            ++result.scanned_files;
+            run_disk_scan_checkers(entry, &actions, &result);
+            if (actions.size() >= static_cast<size_t>(std::max<int64_t>(
+                                          1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                drain_disk_scan_actions(&actions, &result);
+            }
+            return Status::OK();
+        };
+        auto st = _storage->scan_disk_cache(on_key_dir, on_block_file,
+                                            _disk_scan_scan_limiter.get(), 
should_cancel);
+        if (!st.ok() && !st.is<ErrorCode::CANCELLED>()) {
+            LOG(WARNING) << "Failed to scan file cache disk. path=" << 
_cache_base_path
+                         << ", error=" << st;
+        }
+        if (!should_cancel()) {
+            finalize_disk_scan_ttl_checker(&ttl_dirs_for_hash, &actions, 
&result);
+            drain_disk_scan_actions(&actions, &result);
+        } else {
+            result.skipped_candidates += actions.size();
+            actions.clear();
+        }
+    }
+
+    *_disk_scan_latency_us << (duration_ns / 1000);
+    *_disk_scan_rounds << 1;
+    *_disk_scan_scanned_prefix_dirs << result.scanned_prefix_dirs;
+    *_disk_scan_scanned_key_dirs << result.scanned_key_dirs;
+    *_disk_scan_scanned_files << result.scanned_files;
+    *_disk_scan_candidates << result.candidates;
+    *_disk_scan_repaired_files << result.repaired_files;
+    *_disk_scan_repaired_dirs << result.repaired_dirs;
+    *_disk_scan_skipped_candidates << result.skipped_candidates;
+    *_disk_scan_deleted_bytes << result.deleted_bytes;
+    return result;
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanKeyDirEntry& entry,
+                                            std::vector<DiskScanRepairAction>* 
/* actions */,
+                                            std::vector<DiskScanKeyDirEntry>* 
ttl_dirs_for_hash,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+        return;
+    }
+    if (entry.expiration_time > 0) {
+        DCHECK(ttl_dirs_for_hash->empty() || ttl_dirs_for_hash->front().hash 
== entry.hash);
+        ttl_dirs_for_hash->push_back(entry);
+    }
+}
+
+void BlockFileCache::run_disk_scan_checkers(const DiskScanBlockFileEntry& 
entry,
+                                            std::vector<DiskScanRepairAction>* 
actions,
+                                            DiskScanRoundResult* result) {
+    if (!config::file_cache_disk_scan_enable_disk_memory_checker) {
+        return;
+    }
+
+    if (is_younger_than_grace(entry.identity, 
config::file_cache_disk_scan_grace_seconds)) {
+        return;
+    }
+
+    if (entry.is_tmp) {
+        bool skip = false;
+        {
+            SCOPED_CACHE_LOCK(_mutex, this);
+            auto file_iter = _files.find(entry.hash);
+            if (file_iter != _files.end()) {
+                auto block_iter = file_iter->second.find(entry.offset);
+                if (block_iter != file_iter->second.end()) {
+                    std::lock_guard 
block_lock(block_iter->second.file_block->_mutex);
+                    auto state = 
block_iter->second.file_block->state_unlock(block_lock);
+                    skip = state == FileBlock::State::EMPTY ||
+                           state == FileBlock::State::DOWNLOADING;
+                }
+            }
+        }
+        if (skip || _storage->has_active_writer(entry.hash, entry.offset)) {
+            return;
+        }
+        actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                            entry.expiration_time, entry.file_path, 
entry.key_dir, entry.identity,
+                            DiskScanRepairReason::OLD_TMP_FILE});
+        ++result->candidates;
+        return;
+    }
+
+    bool disk_only = false;
+    {
+        SCOPED_CACHE_LOCK(_mutex, this);
+        auto file_iter = _files.find(entry.hash);
+        disk_only = file_iter == _files.end() || 
!file_iter->second.contains(entry.offset);
+    }
+    if (!disk_only) {
+        return;
+    }
+    actions->push_back({DiskScanRepairActionType::DELETE_FILE, entry.hash, 
entry.offset,
+                        entry.expiration_time, entry.file_path, entry.key_dir, 
entry.identity,
+                        DiskScanRepairReason::DISK_ONLY_FILE});
+    ++result->candidates;
+}
+
+void BlockFileCache::finalize_disk_scan_ttl_checker(
+        std::vector<DiskScanKeyDirEntry>* ttl_dirs_for_hash,
+        std::vector<DiskScanRepairAction>* actions, DiskScanRoundResult* 
result) {
+    if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker || 
ttl_dirs_for_hash->empty()) {
+        ttl_dirs_for_hash->clear();
+        return;
+    }
+
+    auto& dirs = *ttl_dirs_for_hash;
+    auto ttl_group_size = dirs.size();
+    TEST_SYNC_POINT_CALLBACK("BlockFileCache::finalize_disk_scan_ttl_group", 
&ttl_group_size);
+    const auto hash = dirs.front().hash;
+    std::sort(dirs.begin(), dirs.end(), [](const auto& lhs, const auto& rhs) {
+        return lhs.expiration_time < rhs.expiration_time;
+    });
+    dirs.erase(std::unique(dirs.begin(), dirs.end(),
+                           [](const auto& lhs, const auto& rhs) {
+                               return lhs.expiration_time == 
rhs.expiration_time;
+                           }),
+               dirs.end());
+    uint64_t canonical_expiration_time = 0;
+    if (dirs.size() > 1 &&
+        disk_scan_hash_has_stable_canonical_expiration(hash, 
&canonical_expiration_time)) {
+        bool canonical_on_disk = false;
+        for (const auto& dir : dirs) {
+            canonical_on_disk |= dir.expiration_time == 
canonical_expiration_time;
+        }
+        if (canonical_on_disk) {
+            for (const auto& dir : dirs) {
+                if (dir.expiration_time == canonical_expiration_time) {
+                    continue;
+                }
+                actions->push_back({DiskScanRepairActionType::DELETE_DIR, 
hash, std::nullopt,
+                                    dir.expiration_time, dir.key_dir, 
dir.key_dir, std::nullopt,
+                                    DiskScanRepairReason::TTL_DUPLICATE_DIR});
+                ++result->candidates;
+                if (actions->size() >=
+                    static_cast<size_t>(std::max<int64_t>(
+                            1, 
config::file_cache_disk_scan_max_pending_repairs))) {
+                    drain_disk_scan_actions(actions, result);
+                }
+            }
+        }
+    }
+    ttl_dirs_for_hash->clear();
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time) {
+    SCOPED_CACHE_LOCK(_mutex, this);
+    return disk_scan_hash_has_stable_canonical_expiration_unlocked(hash, 
canonical_expiration_time,
+                                                                   cache_lock);
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration_unlocked(
+        const UInt128Wrapper& hash, uint64_t* canonical_expiration_time,
+        std::lock_guard<std::mutex>& /* cache_lock */) {
+    auto file_iter = _files.find(hash);
+    if (file_iter == _files.end() || file_iter->second.empty()) {
+        return false;
+    }
+
+    bool has_canonical = false;
+    for (auto& [_, cell] : file_iter->second) {
+        std::lock_guard block_lock(cell.file_block->_mutex);
+        if (cell.file_block->state_unlock(block_lock) != 
FileBlock::State::DOWNLOADED) {
+            return false;
+        }
+        auto expiration_time = cell.file_block->expiration_time();
+        if (!has_canonical) {
+            *canonical_expiration_time = expiration_time;
+            has_canonical = true;
+        } else if (*canonical_expiration_time != expiration_time) {
+            return false;
+        }
+    }
+    return has_canonical;
+}
+
+void 
BlockFileCache::drain_disk_scan_actions(std::vector<DiskScanRepairAction>* 
actions,
+                                             DiskScanRoundResult* result) {
+    for (const auto& action : *actions) {
+        if (_close || !config::enable_file_cache_disk_scan_repair) {

Review Comment:
   [P2] Stop queued repairs when their checker is disabled
   
   The per-checker switches are mutable, but after discovery this drain path 
checks only `_close` and the global repair flag. If an operator disables 
`file_cache_disk_scan_enable_disk_memory_checker` or 
`file_cache_disk_scan_enable_ttl_duplicate_checker`, up to the full pending 
batch can keep waiting on the limiter and deleting files/directories for that 
disabled checker. Please make cancellation reason-aware before and during the 
limiter wait, and add a test that flips each checker flag after its actions 
have been queued.



##########
be/test/io/cache/block_file_cache_test.cpp:
##########
@@ -206,6 +209,110 @@ void complete_into_memory(const io::FileBlocksHolder& 
holder) {
     }
 }
 
+fs::path key_dir_path(const UInt128Wrapper& key, uint64_t expiration_time) {
+    auto key_str = key.to_string();
+    if constexpr (FSFileCacheStorage::USE_CACHE_VERSION2) {
+        return fs::path(cache_base_path) /
+               key_str.substr(0, FSFileCacheStorage::KEY_PREFIX_LENGTH) /
+               (key_str + "_" + std::to_string(expiration_time));
+    }
+    return fs::path(cache_base_path) / (key_str + "_" + 
std::to_string(expiration_time));
+}
+
+void create_cache_file(const fs::path& file_path, std::string_view content = 
"00000") {
+    
ASSERT_TRUE(global_local_filesystem()->create_directory(file_path.parent_path(),
 false).ok());
+    FileWriterPtr writer;
+    ASSERT_TRUE(global_local_filesystem()->create_file(file_path, 
&writer).ok());
+    ASSERT_TRUE(writer->append(Slice(content.data(), content.size())).ok());
+    ASSERT_TRUE(writer->close().ok());
+}
+
+void rewrite_cache_file(const fs::path& file_path, std::string_view content) {
+    std::ofstream file(file_path, std::ios::binary | std::ios::trunc);
+    ASSERT_TRUE(file.good());
+    file.write(content.data(), content.size());
+    ASSERT_TRUE(file.good());
+}
+
+UInt128Wrapper find_key_with_different_prefix(std::string_view base, 
std::string_view excluded) {
+    for (size_t i = 0; i < 10000; ++i) {
+        auto key = io::BlockFileCache::hash(std::string(base) + "_" + 
std::to_string(i));
+        if (key.to_string().substr(0, FSFileCacheStorage::KEY_PREFIX_LENGTH) 
!= excluded) {
+            return key;
+        }
+    }
+    CHECK(false) << "failed to find key with a different prefix";
+}
+
+class DiskScanConfigGuard {
+public:
+    DiskScanConfigGuard()
+            : enable(config::enable_file_cache_disk_scan_repair),
+              
initial_jitter_ms(config::file_cache_disk_scan_initial_jitter_ms),
+              scan_rate_qps(config::file_cache_disk_scan_scan_rate_qps),
+              repair_rate_qps(config::file_cache_disk_scan_repair_rate_qps),
+              grace_seconds(config::file_cache_disk_scan_grace_seconds),
+              
max_pending_repairs(config::file_cache_disk_scan_max_pending_repairs),
+              
enable_ttl_duplicate(config::file_cache_disk_scan_enable_ttl_duplicate_checker),
+              
enable_disk_memory(config::file_cache_disk_scan_enable_disk_memory_checker) {
+        config::enable_file_cache_disk_scan_repair = false;
+        config::file_cache_disk_scan_initial_jitter_ms = 0;
+        config::file_cache_disk_scan_scan_rate_qps = 0;
+        config::file_cache_disk_scan_repair_rate_qps = 0;
+        config::file_cache_disk_scan_grace_seconds = 0;
+        config::file_cache_disk_scan_max_pending_repairs = 10000;
+        config::file_cache_disk_scan_enable_ttl_duplicate_checker = true;
+        config::file_cache_disk_scan_enable_disk_memory_checker = true;
+    }
+
+    ~DiskScanConfigGuard() {
+        config::enable_file_cache_disk_scan_repair = enable;
+        config::file_cache_disk_scan_initial_jitter_ms = initial_jitter_ms;
+        config::file_cache_disk_scan_scan_rate_qps = scan_rate_qps;
+        config::file_cache_disk_scan_repair_rate_qps = repair_rate_qps;
+        config::file_cache_disk_scan_grace_seconds = grace_seconds;
+        config::file_cache_disk_scan_max_pending_repairs = max_pending_repairs;
+        config::file_cache_disk_scan_enable_ttl_duplicate_checker = 
enable_ttl_duplicate;
+        config::file_cache_disk_scan_enable_disk_memory_checker = 
enable_disk_memory;
+    }
+
+private:
+    bool enable;
+    int64_t initial_jitter_ms;
+    int64_t scan_rate_qps;
+    int64_t repair_rate_qps;
+    int64_t grace_seconds;
+    int64_t max_pending_repairs;
+    bool enable_ttl_duplicate;
+    bool enable_disk_memory;
+};
+
+io::DiskScanRoundResult run_disk_scan_once_for_test(io::BlockFileCache& cache) 
{
+    config::enable_file_cache_disk_scan_repair = true;

Review Comment:
   [P2] Isolate synchronous scan tests from the background worker
   
   Every initialized cache starts `run_background_disk_scan_repair()`, and this 
guard sets initial jitter to zero. This helper then publishes the global enable 
flag before invoking the same scan synchronously. A background worker delayed 
in the 100 ms startup poll can observe that flag at the same time, quarantine 
the fixture or consume a sync-point callback first, and make the explicit call 
return counts such as zero instead of one. Please let the test-only entry 
bypass the production enable gate without toggling the worker-visible flag, or 
explicitly park/disable the instance's background scan thread.



-- 
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