blob: bd664e9276d97f4cba8a1e8ac1313a3f8511aaec [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "io/cache/fs_file_cache_storage.h"
#include <fmt/core.h>
#include <filesystem>
#include <mutex>
#include <system_error>
#include "common/logging.h"
#include "common/status.h"
#include "cpp/sync_point.h"
#include "io/cache/block_file_cache.h"
#include "io/cache/file_block.h"
#include "io/cache/file_cache_common.h"
#include "io/cache/file_cache_storage.h"
#include "io/fs/file_reader_writer_fwd.h"
#include "io/fs/file_writer.h"
#include "io/fs/local_file_reader.h"
#include "io/fs/local_file_writer.h"
#include "runtime/exec_env.h"
#include "runtime/memory/mem_tracker_limiter.h"
#include "runtime/thread_context.h"
#include "vec/common/hex.h"
namespace doris::io {
struct BatchLoadArgs {
UInt128Wrapper hash;
CacheContext ctx;
uint64_t offset;
size_t size;
std::string key_path;
std::string offset_path;
bool is_tmp;
};
FDCache* FDCache::instance() {
return ExecEnv::GetInstance()->file_cache_open_fd_cache();
}
std::shared_ptr<FileReader> FDCache::get_file_reader(const AccessKeyAndOffset& key) {
if (config::file_cache_max_file_reader_cache_size == 0) [[unlikely]] {
return nullptr;
}
DCHECK(ExecEnv::GetInstance());
std::shared_lock rlock(_mtx);
if (auto iter = _file_name_to_reader.find(key); iter != _file_name_to_reader.end()) {
return iter->second->second;
}
return nullptr;
}
void FDCache::insert_file_reader(const AccessKeyAndOffset& key,
std::shared_ptr<FileReader> file_reader) {
if (config::file_cache_max_file_reader_cache_size == 0) [[unlikely]] {
return;
}
std::lock_guard wlock(_mtx);
if (auto iter = _file_name_to_reader.find(key); iter == _file_name_to_reader.end()) {
if (config::file_cache_max_file_reader_cache_size == _file_reader_list.size()) {
_file_name_to_reader.erase(_file_reader_list.back().first);
_file_reader_list.pop_back();
}
_file_reader_list.emplace_front(key, std::move(file_reader));
_file_name_to_reader.insert(std::make_pair(key, _file_reader_list.begin()));
}
}
void FDCache::remove_file_reader(const AccessKeyAndOffset& key) {
if (config::file_cache_max_file_reader_cache_size == 0) [[unlikely]] {
return;
}
DCHECK(ExecEnv::GetInstance());
std::lock_guard wlock(_mtx);
if (auto iter = _file_name_to_reader.find(key); iter != _file_name_to_reader.end()) {
_file_reader_list.erase(iter->second);
_file_name_to_reader.erase(key);
}
}
bool FDCache::contains_file_reader(const AccessKeyAndOffset& key) {
std::shared_lock rlock(_mtx);
return _file_name_to_reader.contains(key);
}
size_t FDCache::file_reader_cache_size() {
std::shared_lock rlock(_mtx);
return _file_reader_list.size();
}
Status FSFileCacheStorage::init(BlockFileCache* _mgr) {
_iterator_dir_retry_cnt = std::make_shared<bvar::LatencyRecorder>(
_cache_base_path.c_str(), "file_cache_fs_storage_iterator_dir_retry_cnt");
_cache_base_path = _mgr->_cache_base_path;
_cache_background_load_thread = std::thread([this, mgr = _mgr]() {
auto mem_tracker = MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
fmt::format("FileCacheVersionReader"));
SCOPED_ATTACH_TASK(mem_tracker);
Status st = upgrade_cache_dir_if_necessary();
if (!st.ok()) {
std::string msg = fmt::format(
"file cache {} upgrade done with error. upgrade version failed. st={}",
_cache_base_path, st.to_string());
if (doris::config::ignore_file_cache_dir_upgrade_failure) {
LOG(WARNING) << msg << " be conf: `ignore_file_cache_dir_upgrade_failure = true`"
<< " so we are ignoring the error (unsuccessful cache files will be "
"removed)";
remove_old_version_directories();
} else {
LOG(WARNING) << msg << " please fix error and restart BE or"
<< " use be conf: `ignore_file_cache_dir_upgrade_failure = true`"
<< " to skip the error (unsuccessful cache files will be removed)";
throw doris::Exception(Status::InternalError(msg));
}
}
load_cache_info_into_memory(mgr);
mgr->_async_open_done = true;
LOG_INFO("file cache {} lazy load done.", _cache_base_path);
});
return Status::OK();
}
Status FSFileCacheStorage::append(const FileCacheKey& key, const Slice& value) {
FileWriter* writer = nullptr;
{
std::lock_guard lock(_mtx);
auto file_writer_map_key = std::make_pair(key.hash, key.offset);
if (auto iter = _key_to_writer.find(file_writer_map_key); iter != _key_to_writer.end()) {
writer = iter->second.get();
} else {
std::string dir = get_path_in_local_cache(key.hash, key.meta.expiration_time);
auto st = fs->create_directory(dir, false);
if (!st.ok() && !st.is<ErrorCode::ALREADY_EXIST>()) {
return st;
}
std::string tmp_file = get_path_in_local_cache(dir, key.offset, key.meta.type, true);
FileWriterPtr file_writer;
FileWriterOptions opts {.sync_file_data = false};
RETURN_IF_ERROR(fs->create_file(tmp_file, &file_writer, &opts));
writer = file_writer.get();
_key_to_writer.emplace(file_writer_map_key, std::move(file_writer));
}
}
DCHECK_NE(writer, nullptr);
return writer->append(value);
}
Status FSFileCacheStorage::finalize(const FileCacheKey& key) {
FileWriterPtr file_writer;
{
std::lock_guard lock(_mtx);
auto file_writer_map_key = std::make_pair(key.hash, key.offset);
auto iter = _key_to_writer.find(file_writer_map_key);
DCHECK(iter != _key_to_writer.end());
file_writer = std::move(iter->second);
_key_to_writer.erase(iter);
}
if (file_writer->state() != FileWriter::State::CLOSED) {
RETURN_IF_ERROR(file_writer->close());
}
std::string dir = get_path_in_local_cache(key.hash, key.meta.expiration_time);
std::string true_file = get_path_in_local_cache(dir, key.offset, key.meta.type);
return fs->rename(file_writer->path(), true_file);
}
Status FSFileCacheStorage::read(const FileCacheKey& key, size_t value_offset, Slice buffer) {
AccessKeyAndOffset fd_key = std::make_pair(key.hash, key.offset);
FileReaderSPtr file_reader = FDCache::instance()->get_file_reader(fd_key);
if (!file_reader) {
std::string file =
get_path_in_local_cache(get_path_in_local_cache(key.hash, key.meta.expiration_time),
key.offset, key.meta.type);
Status s = fs->open_file(file, &file_reader);
// handle the case that the file is not found but actually exists in other type format
// TODO(zhengyu): nasty! better eliminate the type encoding in file name in the future
if (!s.ok() && !s.is<ErrorCode::NOT_FOUND>()) {
LOG(WARNING) << "open file failed, file=" << file << ", error=" << s.to_string();
return s; // return other error directly
} else if (!s.ok() && s.is<ErrorCode::NOT_FOUND>()) { // but handle NOT_FOUND error
auto candidates = get_path_in_local_cache_all_candidates(
get_path_in_local_cache(key.hash, key.meta.expiration_time), key.offset);
for (auto& candidate : candidates) {
s = fs->open_file(candidate, &file_reader);
if (s.ok()) {
break; // success with one of there candidates
}
}
if (!s.ok()) { // still not found, return error
LOG(WARNING) << "open file failed, file=" << file << ", error=" << s.to_string();
return s;
}
} // else, s.ok() means open file success
FDCache::instance()->insert_file_reader(fd_key, file_reader);
}
size_t bytes_read = 0;
auto s = file_reader->read_at(value_offset, buffer, &bytes_read);
if (!s.ok()) {
LOG(WARNING) << "read file failed, file=" << file_reader->path()
<< ", error=" << s.to_string();
return s;
}
DCHECK(bytes_read == buffer.get_size());
return Status::OK();
}
Status FSFileCacheStorage::remove(const FileCacheKey& key) {
std::string dir = get_path_in_local_cache(key.hash, key.meta.expiration_time);
std::string file = get_path_in_local_cache(dir, key.offset, key.meta.type);
FDCache::instance()->remove_file_reader(std::make_pair(key.hash, key.offset));
RETURN_IF_ERROR(fs->delete_file(file));
// return OK not means the file is deleted, it may be not exist
// So for TTL, we make sure the old format will be removed well
if (key.meta.type == FileCacheType::TTL) {
bool exists {false};
// try to detect the file with old ttl format
file = get_path_in_local_cache_old_ttl_format(dir, key.offset, key.meta.type);
RETURN_IF_ERROR(fs->exists(file, &exists));
if (exists) {
VLOG(7) << "try to remove the file with old ttl format"
<< " file=" << file;
RETURN_IF_ERROR(fs->delete_file(file));
}
}
std::vector<FileInfo> files;
bool exists {false};
RETURN_IF_ERROR(fs->list(dir, true, &files, &exists));
if (files.empty()) {
RETURN_IF_ERROR(fs->delete_directory(dir));
}
return Status::OK();
}
Status FSFileCacheStorage::change_key_meta_type(const FileCacheKey& key, const FileCacheType type) {
// file operation
if (key.meta.type != type) {
// TTL type file dose not need to change the suffix
bool expr = (key.meta.type != FileCacheType::TTL && type != FileCacheType::TTL);
if (!expr) {
LOG(WARNING) << "TTL type file dose not need to change the suffix"
<< " key=" << key.hash.to_string() << " offset=" << key.offset
<< " old_type=" << cache_type_to_string(key.meta.type)
<< " new_type=" << cache_type_to_string(type);
}
DCHECK(expr);
std::string dir = get_path_in_local_cache(key.hash, key.meta.expiration_time);
std::string original_file = get_path_in_local_cache(dir, key.offset, key.meta.type);
std::string new_file = get_path_in_local_cache(dir, key.offset, type);
RETURN_IF_ERROR(fs->rename(original_file, new_file));
}
return Status::OK();
}
Status FSFileCacheStorage::change_key_meta_expiration(const FileCacheKey& key,
const uint64_t expiration) {
// directory operation
if (key.meta.expiration_time != expiration) {
std::string original_dir = get_path_in_local_cache(key.hash, key.meta.expiration_time);
std::string new_dir = get_path_in_local_cache(key.hash, expiration);
// It will be concurrent, but we don't care who rename
Status st = fs->rename(original_dir, new_dir);
if (!st.ok() && !st.is<ErrorCode::NOT_FOUND>()) {
return st;
}
}
return Status::OK();
}
std::string FSFileCacheStorage::get_path_in_local_cache(const std::string& dir, size_t offset,
FileCacheType type, bool is_tmp) {
if (is_tmp) {
return Path(dir) / (std::to_string(offset) + "_tmp");
} else if (type == FileCacheType::TTL) {
return Path(dir) / std::to_string(offset);
} else {
return Path(dir) / (std::to_string(offset) + cache_type_to_surfix(type));
}
}
std::string FSFileCacheStorage::get_path_in_local_cache_old_ttl_format(const std::string& dir,
size_t offset,
FileCacheType type,
bool is_tmp) {
DCHECK(type == FileCacheType::TTL);
return Path(dir) / (std::to_string(offset) + cache_type_to_surfix(type));
}
std::vector<std::string> FSFileCacheStorage::get_path_in_local_cache_all_candidates(
const std::string& dir, size_t offset) {
std::vector<std::string> candidates;
std::string base = get_path_in_local_cache(dir, offset, FileCacheType::NORMAL);
candidates.push_back(base);
candidates.push_back(base + "_idx");
candidates.push_back(base + "_ttl");
candidates.push_back(base + "_disposable");
return candidates;
}
std::string FSFileCacheStorage::get_path_in_local_cache(const UInt128Wrapper& value,
uint64_t expiration_time) const {
auto str = value.to_string();
try {
if constexpr (USE_CACHE_VERSION2) {
return Path(_cache_base_path) / str.substr(0, KEY_PREFIX_LENGTH) /
(str + "_" + std::to_string(expiration_time));
} else {
return Path(_cache_base_path) / (str + "_" + std::to_string(expiration_time));
}
} catch (std::filesystem::filesystem_error& e) {
LOG_WARNING("fail to get_path_in_local_cache")
.tag("err", e.what())
.tag("key", value.to_string())
.tag("expiration_time", expiration_time);
return "";
}
}
void FSFileCacheStorage::remove_old_version_directories() {
std::error_code ec;
std::filesystem::directory_iterator key_it {_cache_base_path, ec};
if (ec) {
LOG(WARNING) << "Failed to list directory: " << _cache_base_path
<< ", error: " << ec.message();
return;
}
std::vector<std::filesystem::path> file_list;
// the dir is concurrently accessed, so handle invalid iter with retry
bool success = false;
size_t retry_count = 0;
const size_t max_retry = 5;
while (!success && retry_count < max_retry) {
try {
++retry_count;
for (; key_it != std::filesystem::directory_iterator(); ++key_it) {
file_list.push_back(key_it->path());
}
success = true;
} catch (const std::exception& e) {
LOG(WARNING) << "Error occurred while iterating directory: " << e.what();
file_list.clear();
}
}
if (!success) {
LOG_WARNING("iteration of cache dir still failed after retry {} times.", max_retry);
}
auto path_itr = file_list.begin();
for (; path_itr != file_list.end(); ++path_itr) {
if (std::filesystem::is_directory(*path_itr)) {
std::string cache_key = path_itr->filename().native();
if (cache_key.size() > KEY_PREFIX_LENGTH) {
// try our best to delete, not care the return
(void)fs->delete_directory(*path_itr);
}
}
}
auto s = fs->delete_file(get_version_path());
if (!s.ok()) {
LOG(WARNING) << "deleted old version file failed: " << s.to_string();
return;
}
s = write_file_cache_version();
if (!s.ok()) {
LOG(WARNING) << "write new version file failed: " << s.to_string();
return;
}
}
Status FSFileCacheStorage::collect_directory_entries(const std::filesystem::path& dir_path,
std::vector<std::string>& file_list) const {
std::error_code ec;
bool success = false;
size_t retry_count = 0;
const size_t max_retry = 5;
while (!success && retry_count < max_retry) {
try {
++retry_count;
std::filesystem::directory_iterator it {dir_path, ec};
TEST_SYNC_POINT_CALLBACK("FSFileCacheStorage::collect_directory_entries");
if (ec) {
LOG(WARNING) << "Failed to list directory: " << dir_path
<< ", error: " << ec.message();
continue;
}
file_list.clear();
for (; it != std::filesystem::directory_iterator(); ++it) {
file_list.push_back(it->path().string());
}
success = true;
} catch (const std::exception& e) {
LOG(WARNING) << "Error occurred while iterating directory: " << dir_path
<< " err: " << e.what();
file_list.clear();
}
}
*_iterator_dir_retry_cnt << retry_count;
if (!success) {
LOG_WARNING("iteration of cache dir still failed after retry {} times.", max_retry);
return Status::InternalError("Failed to iterate directory after retries.");
}
return Status::OK();
}
Status FSFileCacheStorage::upgrade_cache_dir_if_necessary() const {
/*
* If use version2 but was version 1, do upgrade:
*
* Action I:
* version 1.0: cache_base_path / key / offset
* version 2.0: cache_base_path / key_prefix / key / offset
*
* Action II:
* add '_0' to hash dir
*
* Note: This is a sync operation with tons of IOs, so it may affect BE
* boot time heavily. Fortunately, Action I & II will only happen when
* upgrading (once in the cluster life time).
*/
std::string version;
std::error_code ec;
int rename_count = 0;
int failure_count = 0;
auto start_time = std::chrono::steady_clock::now();
RETURN_IF_ERROR(read_file_cache_version(&version));
LOG(INFO) << "Checking cache version upgrade. Current version: " << version
<< ", target version: 2.0, need upgrade: "
<< (USE_CACHE_VERSION2 && version != "2.0");
if (USE_CACHE_VERSION2 && version != "2.0") {
// move directories format as version 2.0
std::vector<std::string> file_list;
file_list.reserve(10000);
RETURN_IF_ERROR(collect_directory_entries(_cache_base_path, file_list));
// this directory_iterator should be a problem in concurrent access
for (const auto& file_path : file_list) {
try {
if (std::filesystem::is_directory(file_path)) {
std::string cache_key = std::filesystem::path(file_path).filename().native();
if (cache_key.size() > KEY_PREFIX_LENGTH) {
if (cache_key.find('_') == std::string::npos) {
cache_key += "_0";
}
std::string key_prefix =
Path(_cache_base_path) / cache_key.substr(0, KEY_PREFIX_LENGTH);
bool exists = false;
auto exists_status = fs->exists(key_prefix, &exists);
if (!exists_status.ok()) {
LOG(WARNING) << "Failed to check directory existence: " << key_prefix
<< ", error: " << exists_status.to_string();
++failure_count;
continue;
}
if (!exists) {
auto create_status = fs->create_directory(key_prefix);
if (!create_status.ok() &&
create_status.code() != TStatusCode::type::ALREADY_EXIST) {
LOG(WARNING) << "Failed to create directory: " << key_prefix
<< ", error: " << create_status.to_string();
++failure_count;
continue;
}
}
auto rename_status = Status::OK();
const std::string new_file_path = key_prefix + "/" + cache_key;
TEST_SYNC_POINT_CALLBACK(
"FSFileCacheStorage::upgrade_cache_dir_if_necessary_rename",
&file_path, &new_file_path);
rename_status = fs->rename(file_path, new_file_path);
if (rename_status.ok() ||
rename_status.code() == TStatusCode::type::DIRECTORY_NOT_EMPTY) {
++rename_count;
} else {
LOG(WARNING)
<< "Failed to rename directory from " << file_path << " to "
<< new_file_path << ", error: " << rename_status.to_string();
++failure_count;
continue;
}
}
}
} catch (const std::exception& e) {
LOG(WARNING) << "Error occurred while upgrading file cache directory: " << file_path
<< " err: " << e.what();
++failure_count;
}
}
std::vector<std::string> rebuilt_file_list;
rebuilt_file_list.reserve(10000);
RETURN_IF_ERROR(collect_directory_entries(_cache_base_path, rebuilt_file_list));
for (const auto& key_it : rebuilt_file_list) {
if (!std::filesystem::is_directory(key_it)) {
// maybe version hits file
continue;
}
try {
if (Path(key_it).filename().native().size() != KEY_PREFIX_LENGTH) {
LOG(WARNING) << "Unknown directory " << key_it << ", try to remove it";
auto delete_status = fs->delete_directory(key_it);
if (!delete_status.ok()) {
LOG(WARNING) << "Failed to delete unknown directory: " << key_it
<< ", error: " << delete_status.to_string();
++failure_count;
continue;
}
}
} catch (const std::exception& e) {
LOG(WARNING) << "Error occurred while upgrading file cache directory: " << key_it
<< " err: " << e.what();
++failure_count;
}
}
if (auto st = write_file_cache_version(); !st.ok()) {
return Status::InternalError("Failed to write version hints for file cache, err={}",
st.to_string());
}
}
auto end_time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
LOG(INFO) << "Cache directory upgrade completed. Total files renamed: " << rename_count
<< ", Time taken: " << duration.count() << "ms"
<< ", Failure count: " << failure_count;
return Status::OK();
}
Status FSFileCacheStorage::write_file_cache_version() const {
if constexpr (USE_CACHE_VERSION2) {
std::string version_path = get_version_path();
Slice version("2.0");
FileWriterPtr version_writer;
RETURN_IF_ERROR(fs->create_file(version_path, &version_writer));
RETURN_IF_ERROR(version_writer->append(version));
return version_writer->close();
}
return Status::OK();
}
Status FSFileCacheStorage::read_file_cache_version(std::string* buffer) const {
std::string version_path = get_version_path();
bool exists = false;
RETURN_IF_ERROR(fs->exists(version_path, &exists));
if (!exists) {
*buffer = "1.0";
return Status::OK();
}
FileReaderSPtr version_reader;
int64_t file_size = -1;
RETURN_IF_ERROR(fs->file_size(version_path, &file_size));
buffer->resize(file_size);
RETURN_IF_ERROR(fs->open_file(version_path, &version_reader));
size_t bytes_read = 0;
RETURN_IF_ERROR(version_reader->read_at(0, Slice(buffer->data(), file_size), &bytes_read));
RETURN_IF_ERROR(version_reader->close());
auto st = Status::OK();
TEST_SYNC_POINT_CALLBACK("FSFileCacheStorage::read_file_cache_version", &st);
return st;
}
std::string FSFileCacheStorage::get_version_path() const {
return Path(_cache_base_path) / "version";
}
Status FSFileCacheStorage::parse_filename_suffix_to_cache_type(
const std::shared_ptr<LocalFileSystem>& fs, const Path& file_path, long expiration_time,
size_t size, size_t* offset, bool* is_tmp, FileCacheType* cache_type) const {
std::error_code ec;
std::string offset_with_suffix = file_path.native();
auto delim_pos1 = offset_with_suffix.find('_');
bool parsed = true;
try {
if (delim_pos1 == std::string::npos) {
// same as type "normal"
*offset = stoull(offset_with_suffix);
} else {
*offset = stoull(offset_with_suffix.substr(0, delim_pos1));
std::string suffix = offset_with_suffix.substr(delim_pos1 + 1);
// not need persistent anymore
// if suffix is equals to "tmp", it should be removed too.
if (suffix == "tmp") [[unlikely]] {
*is_tmp = true;
} else {
*cache_type = surfix_to_cache_type(suffix);
}
}
} catch (...) {
parsed = false;
}
// File in dir with expiration time > 0 should all be TTL type
// while expiration time == 0 should all be NORMAL type but
// in old days, bug happens, thus break such consistency, e.g.
// BEs shut down during cache type transition.
// Nowadays, we only use expiration time to decide the type,
// i.e. whenever expiration time > 0, it IS TTL, otherwise
// it is NORMAL or INDEX depending on its suffix.
// From now on, the ttl type encoding in file name is only for
// compatibility. It won't be build into the filename, and existing
// ones will be ignored.
if (expiration_time > 0) {
*cache_type = FileCacheType::TTL;
} else if (*cache_type == FileCacheType::TTL && expiration_time == 0) {
*cache_type = FileCacheType::NORMAL;
}
if (!parsed) {
LOG(WARNING) << "parse offset err, path=" << file_path.native();
return Status::InternalError("parse offset err, path={}", file_path.native());
}
TEST_SYNC_POINT_CALLBACK("BlockFileCache::REMOVE_FILE", &offset_with_suffix);
if (ec) {
LOG(WARNING) << "failed to file_size: file_name=" << offset_with_suffix
<< "error=" << ec.message();
return Status::InternalError("failed to file_size: file_name={}, error={}",
offset_with_suffix, ec.message());
}
if (size == 0 && !(*is_tmp)) {
auto st = fs->delete_file(file_path);
if (!st.ok()) {
LOG_WARNING("delete file {} error", file_path.native()).error(st);
}
return Status::InternalError("file size is 0, file_name={}", offset_with_suffix);
}
return Status::OK();
}
void FSFileCacheStorage::load_cache_info_into_memory(BlockFileCache* _mgr) const {
int scan_length = 10000;
std::vector<BatchLoadArgs> batch_load_buffer;
batch_load_buffer.reserve(scan_length);
auto add_cell_batch_func = [&]() {
SCOPED_CACHE_LOCK(_mgr->_mutex, _mgr);
auto f = [&](const BatchLoadArgs& args) {
// in async load mode, a cell may be added twice.
if (_mgr->_files.contains(args.hash) && _mgr->_files[args.hash].contains(args.offset)) {
// TODO(zhengyu): update type&expiration if need
return;
}
// if the file is tmp, it means it is the old file and it should be removed
if (!args.is_tmp) {
_mgr->add_cell(args.hash, args.ctx, args.offset, args.size,
FileBlock::State::DOWNLOADED, cache_lock);
return;
}
std::error_code ec;
std::filesystem::remove(args.offset_path, ec);
if (ec) {
LOG(WARNING) << fmt::format("cannot remove {}: {}", args.offset_path, ec.message());
}
};
std::for_each(batch_load_buffer.begin(), batch_load_buffer.end(), f);
batch_load_buffer.clear();
};
auto scan_file_cache = [&](std::filesystem::directory_iterator& key_it) {
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()));
std::error_code ec;
std::filesystem::directory_iterator offset_it(key_it->path(), ec);
if (ec) [[unlikely]] {
LOG(WARNING) << "filesystem error, failed to list dir, dir=" << key_it->path()
<< " error=" << ec.message();
continue;
}
CacheContext context;
context.query_id = TUniqueId();
long expiration_time = std::stoul(expiration_time_str);
context.expiration_time = expiration_time;
for (; offset_it != std::filesystem::directory_iterator(); ++offset_it) {
size_t size = offset_it->file_size(ec);
size_t offset = 0;
bool is_tmp = false;
FileCacheType cache_type = FileCacheType::NORMAL;
if (!parse_filename_suffix_to_cache_type(fs, offset_it->path().filename().native(),
expiration_time, size, &offset, &is_tmp,
&cache_type)) {
continue;
}
context.cache_type = cache_type;
BatchLoadArgs args;
args.ctx = context;
args.hash = hash;
args.key_path = key_it->path();
args.offset_path = offset_it->path();
args.size = size;
args.offset = offset;
args.is_tmp = is_tmp;
batch_load_buffer.push_back(std::move(args));
// add lock
if (batch_load_buffer.size() >= scan_length) {
add_cell_batch_func();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
}
};
std::error_code ec;
if constexpr (USE_CACHE_VERSION2) {
TEST_SYNC_POINT_CALLBACK("BlockFileCache::BeforeScan");
std::filesystem::directory_iterator key_prefix_it {_cache_base_path, ec};
if (ec) {
LOG(WARNING) << ec.message();
return;
}
for (; key_prefix_it != std::filesystem::directory_iterator(); ++key_prefix_it) {
if (!key_prefix_it->is_directory()) {
// skip version file
continue;
}
if (key_prefix_it->path().filename().native().size() != KEY_PREFIX_LENGTH) {
LOG(WARNING) << "Unknown directory " << key_prefix_it->path().native()
<< ", try to remove it";
std::error_code ec;
std::filesystem::remove(key_prefix_it->path(), ec);
if (ec) {
LOG(WARNING) << "failed to remove=" << key_prefix_it->path()
<< " msg=" << ec.message();
}
continue;
}
std::filesystem::directory_iterator key_it {key_prefix_it->path(), ec};
if (ec) {
LOG(WARNING) << ec.message();
continue;
}
scan_file_cache(key_it);
}
} else {
std::filesystem::directory_iterator key_it {_cache_base_path, ec};
if (ec) {
LOG(WARNING) << ec.message();
return;
}
scan_file_cache(key_it);
}
if (!batch_load_buffer.empty()) {
add_cell_batch_func();
}
TEST_SYNC_POINT_CALLBACK("BlockFileCache::TmpFile2");
}
Status FSFileCacheStorage::get_file_cache_infos(std::vector<FileCacheInfo>& infos,
std::lock_guard<std::mutex>& cache_lock) const {
std::error_code ec;
std::filesystem::directory_iterator key_prefix_it {_cache_base_path, ec};
if (ec) [[unlikely]] {
LOG(ERROR) << fmt::format("Failed to list dir {}, err={}", _cache_base_path, ec.message());
return Status::InternalError("Failed to list dir {}, err={}", _cache_base_path,
ec.message());
}
// Only supports version 2. For more details, refer to 'USE_CACHE_VERSION2'.
for (; key_prefix_it != std::filesystem::directory_iterator(); ++key_prefix_it) {
if (!key_prefix_it->is_directory()) {
// skip version file
continue;
}
if (key_prefix_it->path().filename().native().size() != KEY_PREFIX_LENGTH) {
LOG(WARNING) << "Unknown directory " << key_prefix_it->path().native();
continue;
}
std::filesystem::directory_iterator key_it {key_prefix_it->path(), ec};
if (ec) [[unlikely]] {
LOG(ERROR) << fmt::format("Failed to list dir {}, err={}",
key_prefix_it->path().filename().native(), ec.message());
return Status::InternalError("Failed to list dir {}, err={}",
key_prefix_it->path().filename().native(), ec.message());
}
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);
long expiration_time = std::stoul(expiration_time_str);
auto hash = UInt128Wrapper(vectorized::unhex_uint<uint128_t>(key_str.c_str()));
std::error_code ec;
std::filesystem::directory_iterator offset_it(key_it->path(), ec);
if (ec) [[unlikely]] {
LOG(ERROR) << fmt::format("Failed to list dir {}, err={}",
key_it->path().filename().native(), ec.message());
return Status::InternalError("Failed to list dir {}, err={}",
key_it->path().filename().native(), ec.message());
}
for (; offset_it != std::filesystem::directory_iterator(); ++offset_it) {
size_t size = offset_it->file_size(ec);
if (ec) [[unlikely]] {
LOG(ERROR) << fmt::format("Failed to get file size, file name {}, err={}",
key_it->path().filename().native(), ec.message());
return Status::InternalError("Failed to get file size, file name {}, err={}",
key_it->path().filename().native(), ec.message());
}
size_t offset = 0;
bool is_tmp = false;
FileCacheType cache_type = FileCacheType::NORMAL;
RETURN_IF_ERROR(this->parse_filename_suffix_to_cache_type(
fs, offset_it->path().filename().native(), expiration_time, size, &offset,
&is_tmp, &cache_type));
infos.emplace_back(hash, expiration_time, size, offset, is_tmp, cache_type);
}
}
}
return Status::OK();
}
void FSFileCacheStorage::load_blocks_directly_unlocked(BlockFileCache* mgr, const FileCacheKey& key,
std::lock_guard<std::mutex>& cache_lock) {
// async load, can't find key, need to check exist.
auto key_path = get_path_in_local_cache(key.hash, key.meta.expiration_time);
bool exists = false;
auto st = fs->exists(key_path, &exists);
if (auto st = fs->exists(key_path, &exists); !exists && st.ok()) {
// cache miss
return;
} else if (!st.ok()) [[unlikely]] {
LOG_WARNING("failed to exists file {}", key_path).error(st);
return;
}
CacheContext context_original;
context_original.query_id = TUniqueId();
context_original.expiration_time = key.meta.expiration_time;
std::error_code ec;
std::filesystem::directory_iterator check_it(key_path, ec);
if (ec) [[unlikely]] {
LOG(WARNING) << "fail to directory_iterator " << ec.message();
return;
}
for (; check_it != std::filesystem::directory_iterator(); ++check_it) {
size_t size = check_it->file_size(ec);
size_t offset = 0;
bool is_tmp = false;
FileCacheType cache_type = FileCacheType::NORMAL;
if (!parse_filename_suffix_to_cache_type(fs, check_it->path().filename().native(),
context_original.expiration_time, size, &offset,
&is_tmp, &cache_type)) {
continue;
}
if (!mgr->_files.contains(key.hash) || !mgr->_files[key.hash].contains(offset)) {
// if the file is tmp, it means it is the old file and it should be removed
if (is_tmp) {
std::error_code ec;
std::filesystem::remove(check_it->path(), ec);
if (ec) {
LOG(WARNING) << fmt::format("cannot remove {}: {}", check_it->path().native(),
ec.message());
}
} else {
context_original.cache_type = cache_type;
mgr->add_cell(key.hash, context_original, offset, size,
FileBlock::State::DOWNLOADED, cache_lock);
}
}
}
}
Status FSFileCacheStorage::clear(std::string& msg) {
LOG(INFO) << "clear file storage, path=" << _cache_base_path;
std::error_code ec;
std::filesystem::directory_iterator key_it {_cache_base_path, ec};
if (ec) {
LOG(WARNING) << "Failed to list directory: " << _cache_base_path
<< ", error: " << ec.message();
return Status::InternalError("Failed to list dir {}: {}", _cache_base_path, ec.message());
}
int failed = 0;
int total = 0;
auto t0 = std::chrono::steady_clock::now();
for (; key_it != std::filesystem::directory_iterator(); ++key_it) {
if (!key_it->is_directory()) continue; // all file cache data is in sub-directories
++total;
std::string cache_key = key_it->path().string();
auto st = global_local_filesystem()->delete_directory(cache_key);
if (st.ok()) continue;
failed++;
LOG(WARNING) << "failed to clear base_path=" << _cache_base_path
<< " path_to_delete=" << cache_key << " error=" << st;
}
auto t1 = std::chrono::steady_clock::now();
std::stringstream ss;
ss << "finished clear file storage, path=" << _cache_base_path
<< " deleted=" << (total - failed) << " failed=" << failed
<< " elapsed_ms=" << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
LOG(INFO) << ss.str();
if (failed > 0) {
msg = ss.str();
return Status::InternalError(msg);
}
return Status::OK();
}
std::string FSFileCacheStorage::get_local_file(const FileCacheKey& key) {
return get_path_in_local_cache(get_path_in_local_cache(key.hash, key.meta.expiration_time),
key.offset, key.meta.type, false);
}
FSFileCacheStorage::~FSFileCacheStorage() {
if (_cache_background_load_thread.joinable()) {
_cache_background_load_thread.join();
}
}
} // namespace doris::io