github-actions[bot] commented on code in PR #64496:
URL: https://github.com/apache/doris/pull/64496#discussion_r3614829924
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -841,6 +818,17 @@ FileBlocks BlockFileCache::split_range_into_cells(const
UInt128Wrapper& hash,
std::lock_guard<std::mutex>&
cache_lock) {
DCHECK(size > 0);
+ CacheContext block_context = context;
+ if (auto file_iter = _files.find(hash);
+ file_iter != _files.end() && !file_iter->second.empty()) {
+ auto& first_block = file_iter->second.begin()->second.file_block;
Review Comment:
[P1] Avoid choosing the TTL expiration by lowest offset
Startup can load disjoint offsets from two expiration directories for the
same hash: it deduplicates only identical hash/offset pairs, while
`_key_to_time` records whichever directory was inserted first. This `begin()`
is ordered by offset instead, so new holes can be written under a different
expiration than TTL GC tracks. The duplicate checker then refuses the hash
because its in-memory expirations already differ, leaving the split state
indefinitely. Please detect this mixed state and either reconcile it against
one explicit canonical expiration or avoid caching new holes until it is
repaired.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
+ ttl_dirs_by_hash;
+
+ std::string last_prefix;
+ auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+ ++result.scanned_key_dirs;
+ if (last_prefix != entry.prefix) {
+ last_prefix = entry.prefix;
+ ++result.scanned_prefix_dirs;
+ }
+ run_disk_scan_checkers(entry, &actions, &ttl_dirs_by_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 {
+ ++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());
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to scan file cache disk. path=" <<
_cache_base_path
+ << ", error=" << st;
+ }
+ finalize_disk_scan_ttl_checker(&ttl_dirs_by_hash, &actions, &result);
+ drain_disk_scan_actions(&actions, &result);
+ }
+
+ *_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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ DiskScanRoundResult* result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ if (entry.expiration_time > 0) {
+ (*ttl_dirs_by_hash)[entry.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);
Review Comment:
[P1] Do not treat a partial startup load as authoritative
`_async_open_done` is set even when `load_cache_info_into_memory()` has
skipped a prefix, key directory, or file after an enumeration/stat error. If
that transient error clears before this scan, the valid persisted files are
visible here but absent from `_files`, so files older than the grace period are
classified as disk-only and deleted. This is after loading "completed", so the
existing gate does not help. Please track a successful/complete startup image
(or retry/reconcile skipped paths) before enabling destructive disk-vs-memory
repair.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
+ ttl_dirs_by_hash;
+
+ std::string last_prefix;
+ auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+ ++result.scanned_key_dirs;
+ if (last_prefix != entry.prefix) {
+ last_prefix = entry.prefix;
+ ++result.scanned_prefix_dirs;
+ }
+ run_disk_scan_checkers(entry, &actions, &ttl_dirs_by_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 {
+ ++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());
Review Comment:
[P1] Make a running scan cancellable during shutdown
Once this call starts, neither callback nor `scan_disk_cache()` observes
`_close`, and token-bucket sleeps are also uninterruptible. The destructor sets
`_close` and then joins this thread, so shutdown waits for the remainder of the
whole cache traversal. At the default 1 MiB block size and 2,000-QPS limit, a
populated 1 TiB cache is about 1,048,576 callbacks, or roughly 8.7 minutes.
Please thread cancellation through enumeration and limiter waits so destruction
can stop a round promptly, and cover it with a shutdown-during-scan test.
##########
be/src/io/cache/fs_file_cache_storage.cpp:
##########
@@ -287,6 +340,142 @@ Status
FSFileCacheStorage::change_key_meta_expiration(const FileCacheKey& key,
return Status::OK();
}
+Status FSFileCacheStorage::scan_disk_cache(DiskScanKeyDirCallback on_key_dir,
+ DiskScanBlockFileCallback
on_block_file,
+ TokenBucketRateLimiterHolder*
scan_limiter) {
+ auto consume_scan_token = [&]() {
+ if (scan_limiter != nullptr) {
+ scan_limiter->add(1);
+ }
+ };
+
+ auto scan_key_dir = [&](const std::string& prefix,
+ const std::filesystem::path& key_dir_path) ->
Status {
+ std::error_code ec;
+ if (!std::filesystem::is_directory(key_dir_path, ec) || ec) {
+ return Status::OK();
+ }
+
+ UInt128Wrapper hash;
+ uint64_t expiration_time = 0;
+ if (!parse_key_dir_name(key_dir_path.filename().native(), &hash,
&expiration_time)) {
+ return Status::OK();
+ }
+
+ DiskScanKeyDirEntry key_dir_entry;
+ key_dir_entry.prefix = prefix;
+ key_dir_entry.hash = hash;
+ key_dir_entry.expiration_time = expiration_time;
+ key_dir_entry.key_dir = key_dir_path;
+ RETURN_IF_ERROR(on_key_dir(key_dir_entry));
+
+ std::vector<std::string> block_files;
+ consume_scan_token();
+ auto st = collect_directory_entries(key_dir_path, block_files);
Review Comment:
[P2] Rate-limit while enumerating each key directory
`collect_directory_entries()` exhausts the directory and stores every full
path before the following loop consumes any per-file tokens. A highly
fragmented object can therefore cause an unthrottled `readdir` burst and a very
large temporary vector despite a low scan QPS (at the default 1 MiB block size,
a 1 TiB object is roughly one million entries). Please stream or batch the
iterator and consume a token before each advancement/inspection, as the startup
loader already does with bounded batches.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
Review Comment:
[P2] Bound TTL checker state during the scan
`file_cache_disk_scan_max_pending_repairs` bounds only `actions`; this map
retains a copied path entry for every positive-expiration key directory until
the entire cache scan returns. A high-cardinality TTL cache can therefore add
hundreds of MB of temporary nodes/vectors/paths each round. In the v2 layout
every directory for a hash is confined to its sorted three-digit prefix, so
completed hash or prefix groups can be finalized and erased incrementally.
Please bound this accumulator independently of the repair-action queue and add
a high-cardinality test.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
+ ttl_dirs_by_hash;
+
+ std::string last_prefix;
+ auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+ ++result.scanned_key_dirs;
+ if (last_prefix != entry.prefix) {
+ last_prefix = entry.prefix;
+ ++result.scanned_prefix_dirs;
+ }
+ run_disk_scan_checkers(entry, &actions, &ttl_dirs_by_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 {
+ ++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());
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to scan file cache disk. path=" <<
_cache_base_path
+ << ", error=" << st;
+ }
+ finalize_disk_scan_ttl_checker(&ttl_dirs_by_hash, &actions, &result);
+ drain_disk_scan_actions(&actions, &result);
+ }
+
+ *_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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ DiskScanRoundResult* result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ if (entry.expiration_time > 0) {
Review Comment:
[P2] Include `_0` when detecting duplicate hash directories
A positive-expiration directory paired with `<hash>_0` produces only one
entry here, so the directory checker sees no duplicate. The disk-memory checker
also compares only hash/offset, ignoring expiration/path; when both directories
contain the same offset, both physical files look represented by the single
in-memory block. Thus either a canonical TTL plus stale NORMAL path or the
reverse survives every round. Please group expiration zero too and compare each
disk entry with the in-memory block's expiration/type before removing the
noncanonical path; add restart tests for both canonical directions.
##########
be/src/io/cache/fs_file_cache_storage.cpp:
##########
@@ -287,6 +340,142 @@ Status
FSFileCacheStorage::change_key_meta_expiration(const FileCacheKey& key,
return Status::OK();
}
+Status FSFileCacheStorage::scan_disk_cache(DiskScanKeyDirCallback on_key_dir,
+ DiskScanBlockFileCallback
on_block_file,
+ TokenBucketRateLimiterHolder*
scan_limiter) {
+ auto consume_scan_token = [&]() {
+ if (scan_limiter != nullptr) {
+ scan_limiter->add(1);
+ }
+ };
+
+ auto scan_key_dir = [&](const std::string& prefix,
+ const std::filesystem::path& key_dir_path) ->
Status {
+ std::error_code ec;
+ if (!std::filesystem::is_directory(key_dir_path, ec) || ec) {
+ return Status::OK();
+ }
+
+ UInt128Wrapper hash;
+ uint64_t expiration_time = 0;
+ if (!parse_key_dir_name(key_dir_path.filename().native(), &hash,
&expiration_time)) {
+ return Status::OK();
+ }
+
+ DiskScanKeyDirEntry key_dir_entry;
+ key_dir_entry.prefix = prefix;
+ key_dir_entry.hash = hash;
+ key_dir_entry.expiration_time = expiration_time;
+ key_dir_entry.key_dir = key_dir_path;
+ RETURN_IF_ERROR(on_key_dir(key_dir_entry));
+
+ std::vector<std::string> block_files;
+ consume_scan_token();
+ auto st = collect_directory_entries(key_dir_path, block_files);
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to list file cache key dir " <<
key_dir_path
+ << ", error=" << st;
+ return Status::OK();
+ }
+ for (const auto& block_file : block_files) {
+ consume_scan_token();
+ std::filesystem::path block_file_path(block_file);
+ if (!std::filesystem::is_regular_file(block_file_path, ec) || ec) {
+ continue;
+ }
+ int64_t file_size = 0;
+ auto identity = get_file_identity(block_file_path, 0);
+ if (!identity) {
+ continue;
+ }
+ file_size = static_cast<int64_t>(identity->size);
+ size_t offset = 0;
+ bool is_tmp = false;
+ FileCacheType cache_type = FileCacheType::NORMAL;
+ if (!parse_filename_suffix_to_cache_type(fs,
block_file_path.filename().native(),
+ expiration_time,
file_size, &offset, &is_tmp,
+ &cache_type, false)) {
+ continue;
+ }
+
+ DiskScanBlockFileEntry block_entry;
+ block_entry.hash = hash;
+ block_entry.offset = offset;
+ block_entry.expiration_time = expiration_time;
+ block_entry.cache_type = cache_type;
+ block_entry.is_tmp = is_tmp;
+ block_entry.identity = *identity;
+ block_entry.file_path = block_file_path;
+ block_entry.key_dir = key_dir_path;
+ RETURN_IF_ERROR(on_block_file(block_entry));
+ }
+ return Status::OK();
+ };
+
+ if constexpr (USE_CACHE_VERSION2) {
+ std::vector<std::string> prefix_entries;
+ consume_scan_token();
+ RETURN_IF_ERROR(collect_directory_entries(_cache_base_path,
prefix_entries));
+ std::sort(prefix_entries.begin(), prefix_entries.end());
+ for (const auto& prefix_path_str : prefix_entries) {
+ std::filesystem::path prefix_path(prefix_path_str);
+ std::error_code ec;
+ if (!std::filesystem::is_directory(prefix_path, ec) || ec) {
+ continue;
+ }
+ auto prefix = prefix_path.filename().native();
+ if (prefix.size() != KEY_PREFIX_LENGTH) {
+ continue;
+ }
+
+ std::vector<std::string> key_dir_entries;
+ consume_scan_token();
+ auto st = collect_directory_entries(prefix_path, key_dir_entries);
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to list file cache prefix dir " <<
prefix_path
+ << ", error=" << st;
+ continue;
+ }
+ std::sort(key_dir_entries.begin(), key_dir_entries.end());
+ for (const auto& key_dir : key_dir_entries) {
+ RETURN_IF_ERROR(scan_key_dir(prefix, key_dir));
+ }
+ }
+ } else {
+ std::vector<std::string> key_dir_entries;
+ consume_scan_token();
+ RETURN_IF_ERROR(collect_directory_entries(_cache_base_path,
key_dir_entries));
+ std::sort(key_dir_entries.begin(), key_dir_entries.end());
+ for (const auto& key_dir : key_dir_entries) {
+ RETURN_IF_ERROR(scan_key_dir("", key_dir));
+ }
+ }
+ return Status::OK();
+}
+
+bool FSFileCacheStorage::has_active_writer(const UInt128Wrapper& hash, size_t
offset) {
+ std::lock_guard lock(_mtx);
+ return _key_to_writer.contains(std::make_pair(hash, offset));
+}
+
+bool FSFileCacheStorage::has_active_writer_for_hash(const UInt128Wrapper&
hash) {
+ std::lock_guard lock(_mtx);
+ for (const auto& [key, _] : _key_to_writer) {
+ if (key.first == hash) {
+ return true;
+ }
+ }
+ return false;
+}
+
+Status FSFileCacheStorage::delete_file_for_disk_scan(const
std::filesystem::path& path) {
+ return fs->delete_file(path);
Review Comment:
[P2] Remove empty key directories after repairing their last file
The normal `remove()` path deletes a key directory after its last file is
removed, but this repair path only unlinks the leaf. If a disk-only file is the
directory's sole entry, `<hash>_<expiration>` then remains indefinitely: there
is no memory cell to trigger normal cleanup, a lone TTL directory does not
qualify as a duplicate, and `_0` is excluded from that checker. At high orphan
cardinality this retains one directory inode per key and makes every later
round traverse all of them again. Please attempt a non-recursive
empty-directory removal after the unlink, and serialize it at `(hash,
expiration)` directory scope with all offsets' writer creation/finalization (or
retry append if the directory vanishes); a per-`(hash, offset)` claim is not
enough. Extend the orphan-file test to assert that the parent disappears.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
+ ttl_dirs_by_hash;
+
+ std::string last_prefix;
+ auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+ ++result.scanned_key_dirs;
+ if (last_prefix != entry.prefix) {
+ last_prefix = entry.prefix;
+ ++result.scanned_prefix_dirs;
+ }
+ run_disk_scan_checkers(entry, &actions, &ttl_dirs_by_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 {
+ ++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());
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to scan file cache disk. path=" <<
_cache_base_path
+ << ", error=" << st;
+ }
+ finalize_disk_scan_ttl_checker(&ttl_dirs_by_hash, &actions, &result);
+ drain_disk_scan_actions(&actions, &result);
+ }
+
+ *_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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ DiskScanRoundResult* result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ if (entry.expiration_time > 0) {
+ (*ttl_dirs_by_hash)[entry.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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ std::vector<DiskScanRepairAction>* actions, DiskScanRoundResult*
result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ for (auto& [hash, dirs] : *ttl_dirs_by_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());
+ if (dirs.size() <= 1) {
+ continue;
+ }
+ uint64_t canonical_expiration_time = 0;
+ if (!disk_scan_hash_has_stable_canonical_expiration(hash,
&canonical_expiration_time)) {
+ continue;
+ }
+ bool canonical_on_disk = false;
+ for (const auto& dir : dirs) {
+ canonical_on_disk |= dir.expiration_time ==
canonical_expiration_time;
+ }
+ if (!canonical_on_disk) {
+ continue;
+ }
+ 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_by_hash->clear();
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration(
+ const UInt128Wrapper& hash, uint64_t* canonical_expiration_time) {
+ SCOPED_CACHE_LOCK(_mutex, this);
+ 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) {
+ ++result->skipped_candidates;
+ continue;
+ }
+ _disk_scan_repair_limiter->add(1);
+ if (action.type == DiskScanRepairActionType::DELETE_FILE) {
+ if (!disk_scan_file_is_deletable(action)) {
+ ++result->skipped_candidates;
+ continue;
+ }
+ auto st = _storage->delete_file_for_disk_scan(action.path);
+ if (st.ok()) {
+ ++result->repaired_files;
+ if (action.observed_identity) {
+ result->deleted_bytes += action.observed_identity->size;
+ }
+ } else {
+ ++result->skipped_candidates;
+ }
+ } else {
+ if (!disk_scan_dir_is_deletable(action)) {
+ ++result->skipped_candidates;
+ continue;
+ }
+ auto st = _storage->delete_dir_for_disk_scan(action.path);
+ if (st.ok()) {
+ ++result->repaired_dirs;
+ } else {
+ ++result->skipped_candidates;
+ }
+ }
+ }
+ actions->clear();
+}
+
+bool BlockFileCache::disk_scan_file_is_deletable(const DiskScanRepairAction&
action) {
+ if (!action.offset) {
+ return false;
+ }
+ {
+ SCOPED_CACHE_LOCK(_mutex, this);
+ 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) {
+ uint64_t canonical_expiration_time = 0;
+ if (!disk_scan_hash_has_stable_canonical_expiration(action.hash,
&canonical_expiration_time)) {
+ 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));
+ return std::filesystem::exists(canonical_path);
Review Comment:
[P1] Keep filesystem errors from escaping the repair thread
This is the throwing overload of `std::filesystem::exists`. A permission or
I/O error while checking an inconsistent cache path can throw
`filesystem_error`; there is no exception boundary in
`run_disk_scan_repair_once()` or the background thread, so the exception
invokes `std::terminate` and takes down the BE. Please use the `error_code`
overload (or the storage filesystem abstraction), log/measure the error, and
treat the directory as not deletable.
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -2285,6 +2283,341 @@ 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)));
+ }
+
+ while (!_close) {
+ if (!_async_open_done) {
+ std::unique_lock close_lock(_close_mtx);
+ _close_cv.wait_for(close_lock, std::chrono::milliseconds(100));
+ 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)));
+ 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) {
+ return result;
+ }
+
+ _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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>
+ ttl_dirs_by_hash;
+
+ std::string last_prefix;
+ auto on_key_dir = [&](const DiskScanKeyDirEntry& entry) -> Status {
+ ++result.scanned_key_dirs;
+ if (last_prefix != entry.prefix) {
+ last_prefix = entry.prefix;
+ ++result.scanned_prefix_dirs;
+ }
+ run_disk_scan_checkers(entry, &actions, &ttl_dirs_by_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 {
+ ++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());
+ if (!st.ok()) {
+ LOG(WARNING) << "Failed to scan file cache disk. path=" <<
_cache_base_path
+ << ", error=" << st;
+ }
+ finalize_disk_scan_ttl_checker(&ttl_dirs_by_hash, &actions, &result);
+ drain_disk_scan_actions(&actions, &result);
+ }
+
+ *_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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ DiskScanRoundResult* result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ if (entry.expiration_time > 0) {
+ (*ttl_dirs_by_hash)[entry.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::unordered_map<UInt128Wrapper, std::vector<DiskScanKeyDirEntry>,
KeyHash>*
+ ttl_dirs_by_hash,
+ std::vector<DiskScanRepairAction>* actions, DiskScanRoundResult*
result) {
+ if (!config::file_cache_disk_scan_enable_ttl_duplicate_checker) {
+ return;
+ }
+ for (auto& [hash, dirs] : *ttl_dirs_by_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());
+ if (dirs.size() <= 1) {
+ continue;
+ }
+ uint64_t canonical_expiration_time = 0;
+ if (!disk_scan_hash_has_stable_canonical_expiration(hash,
&canonical_expiration_time)) {
+ continue;
+ }
+ bool canonical_on_disk = false;
+ for (const auto& dir : dirs) {
+ canonical_on_disk |= dir.expiration_time ==
canonical_expiration_time;
+ }
+ if (!canonical_on_disk) {
+ continue;
+ }
+ 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_by_hash->clear();
+}
+
+bool BlockFileCache::disk_scan_hash_has_stable_canonical_expiration(
+ const UInt128Wrapper& hash, uint64_t* canonical_expiration_time) {
+ SCOPED_CACHE_LOCK(_mutex, this);
+ 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) {
+ ++result->skipped_candidates;
+ continue;
+ }
+ _disk_scan_repair_limiter->add(1);
+ if (action.type == DiskScanRepairActionType::DELETE_FILE) {
+ if (!disk_scan_file_is_deletable(action)) {
+ ++result->skipped_candidates;
+ continue;
+ }
+ auto st = _storage->delete_file_for_disk_scan(action.path);
Review Comment:
[P1] Serialize validation with the final file unlink
All cache-map, active-writer, and inode checks have completed before this
pathname deletion. In that gap another request can insert the same hash/offset,
write its tmp file, and rename it over the observed final path; this call then
unlinks the new inode and leaves a `DOWNLOADED` in-memory block with no backing
file. This is distinct from the existing recursive TTL-directory race because
it is the new single-file repair path. Please hold a per-hash/offset repair
claim through an identity-aware unlink (and block publish/finalize on that
claim), with a sync-point race test.
--
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]