Add safe file deletion utility FileRetainer (#38)
Co-authored-by: Antonio Andelic <antonio.andelic@memgraph.io>
This commit is contained in:
parent
b10255a12f
commit
42f6118c00
@ -1,5 +1,6 @@
|
|||||||
set(utils_src_files
|
set(utils_src_files
|
||||||
file.cpp
|
file.cpp
|
||||||
|
file_locker.cpp
|
||||||
memory.cpp
|
memory.cpp
|
||||||
signals.cpp
|
signals.cpp
|
||||||
thread.cpp
|
thread.cpp
|
||||||
|
100
src/utils/file_locker.cpp
Normal file
100
src/utils/file_locker.cpp
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#include "utils/file_locker.hpp"
|
||||||
|
|
||||||
|
namespace utils {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void DeleteFromSystem(const std::filesystem::path &path) {
|
||||||
|
if (!utils::DeleteFile(path)) {
|
||||||
|
LOG(WARNING) << "Couldn't delete file " << path << "!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
////// FileRetainer //////
|
||||||
|
void FileRetainer::DeleteFile(const std::filesystem::path &path) {
|
||||||
|
if (active_accessors_.load()) {
|
||||||
|
files_for_deletion_.WithLock([&](auto &files) { files.emplace(path); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::unique_lock guard(main_lock_);
|
||||||
|
DeleteOrAddToQueue(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRetainer::FileLocker FileRetainer::AddLocker() {
|
||||||
|
const size_t current_locker_id = next_locker_id_.fetch_add(1);
|
||||||
|
lockers_.WithLock([&](auto &lockers) {
|
||||||
|
lockers.emplace(current_locker_id, std::set<std::filesystem::path>{});
|
||||||
|
});
|
||||||
|
return FileLocker{this, current_locker_id};
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRetainer::~FileRetainer() {
|
||||||
|
CHECK(files_for_deletion_->empty()) << "Files weren't properly deleted";
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool FileRetainer::FileLocked(const std::filesystem::path &path) {
|
||||||
|
return lockers_.WithLock([&](auto &lockers) {
|
||||||
|
for (const auto &[_, paths] : lockers) {
|
||||||
|
if (paths.count(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileRetainer::DeleteOrAddToQueue(const std::filesystem::path &path) {
|
||||||
|
if (FileLocked(path)) {
|
||||||
|
files_for_deletion_.WithLock([&](auto &files) { files.emplace(path); });
|
||||||
|
} else {
|
||||||
|
DeleteFromSystem(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileRetainer::CleanQueue() {
|
||||||
|
files_for_deletion_.WithLock([&](auto &files) {
|
||||||
|
for (auto it = files.cbegin(); it != files.cend();) {
|
||||||
|
if (!FileLocked(*it)) {
|
||||||
|
DeleteFromSystem(*it);
|
||||||
|
it = files.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
////// FileLocker //////
|
||||||
|
FileRetainer::FileLocker::~FileLocker() {
|
||||||
|
file_retainer_->lockers_.WithLock(
|
||||||
|
[this](auto &lockers) { lockers.erase(locker_id_); });
|
||||||
|
std::unique_lock guard(file_retainer_->main_lock_);
|
||||||
|
file_retainer_->CleanQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRetainer::FileLockerAccessor FileRetainer::FileLocker::Access() {
|
||||||
|
return FileLockerAccessor{file_retainer_, locker_id_};
|
||||||
|
}
|
||||||
|
|
||||||
|
////// FileLockerAccessor //////
|
||||||
|
FileRetainer::FileLockerAccessor::FileLockerAccessor(FileRetainer *retainer,
|
||||||
|
size_t locker_id)
|
||||||
|
: file_retainer_{retainer},
|
||||||
|
retainer_guard_{retainer->main_lock_},
|
||||||
|
locker_id_{locker_id} {
|
||||||
|
file_retainer_->active_accessors_.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileRetainer::FileLockerAccessor::AddFile(
|
||||||
|
const std::filesystem::path &path) {
|
||||||
|
if (!std::filesystem::exists(path)) return false;
|
||||||
|
file_retainer_->lockers_.WithLock(
|
||||||
|
[&](auto &lockers) { lockers[locker_id_].emplace(path); });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRetainer::FileLockerAccessor::~FileLockerAccessor() {
|
||||||
|
file_retainer_->active_accessors_.fetch_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace utils
|
164
src/utils/file_locker.hpp
Normal file
164
src/utils/file_locker.hpp
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <atomic>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <shared_mutex>
|
||||||
|
|
||||||
|
#include "utils/file.hpp"
|
||||||
|
#include "utils/rw_lock.hpp"
|
||||||
|
#include "utils/spin_lock.hpp"
|
||||||
|
#include "utils/synchronized.hpp"
|
||||||
|
|
||||||
|
namespace utils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class used for safer modifying and reading of files
|
||||||
|
* by preventing a deletion of a file until the file is not used in any of
|
||||||
|
* currently running threads.
|
||||||
|
* Also, while a single thread modyifies it's list of locked files, the deletion
|
||||||
|
* of ALL the files is delayed.
|
||||||
|
*
|
||||||
|
* Basic usage of FileRetainer consists of following parts:
|
||||||
|
* - Defining a global FileRetainer object which is used for locking and
|
||||||
|
* deleting of the files.
|
||||||
|
* - Each thread that wants to lock a single or multiple files first creates a
|
||||||
|
* FileLocker object.
|
||||||
|
* - Modifying a FileLocker is only possible through the FileLockerAccessor.
|
||||||
|
* - FileLockerAccessor prevents deletion of any file, so you can safely add
|
||||||
|
* multiple files to the locker with no risk of having files deleted during
|
||||||
|
* the process.
|
||||||
|
* - After a FileLocker or FileLockerAccessor is destroyed, FileRetainer scans
|
||||||
|
* the list of the files that wait to be deleted, and deletes all the files
|
||||||
|
* that are not inside any of currently present lockers.
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* FileRetainer file_retainer;
|
||||||
|
* std::filesystem::path file1;
|
||||||
|
* std::filesystem::path file2;
|
||||||
|
*
|
||||||
|
* void Foo() {
|
||||||
|
* // I want to lock a list of files
|
||||||
|
* // Create a locker
|
||||||
|
* auto locker = file_retainer.AddLocker();
|
||||||
|
* {
|
||||||
|
* // Create accessor to the locker so you can
|
||||||
|
* // add the files which need to be locked.
|
||||||
|
* // Accesor prevents deletion of any files
|
||||||
|
* // so you safely add multiple files in atomic way
|
||||||
|
* auto accessor = locker.Access();
|
||||||
|
* accessor.AddFile(file1);
|
||||||
|
* accessor.AddFile(file2);
|
||||||
|
* }
|
||||||
|
* // DO SOMETHING WITH THE FILES
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* void Bar() {
|
||||||
|
* // I want to delete file1.
|
||||||
|
* file_retiner.DeleteFile(file1);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* int main() {
|
||||||
|
* // Run Foo() and Bar() in different threads.
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class FileRetainer {
|
||||||
|
public:
|
||||||
|
struct FileLockerAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single locker inside the FileRetainer that contains a list
|
||||||
|
* of files that are guarded from deletion.
|
||||||
|
*/
|
||||||
|
struct FileLocker {
|
||||||
|
friend FileRetainer;
|
||||||
|
~FileLocker();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the FileLocker so you can modify it.
|
||||||
|
*/
|
||||||
|
FileLockerAccessor Access();
|
||||||
|
|
||||||
|
FileLocker(const FileLocker &) = delete;
|
||||||
|
FileLocker(FileLocker &&) = default;
|
||||||
|
FileLocker &operator=(const FileLocker &) = delete;
|
||||||
|
FileLocker &operator=(FileLocker &&) = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit FileLocker(FileRetainer *retainer, size_t locker_id)
|
||||||
|
: file_retainer_{retainer}, locker_id_{locker_id} {}
|
||||||
|
|
||||||
|
FileRetainer *file_retainer_;
|
||||||
|
size_t locker_id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessor to the FileLocker.
|
||||||
|
* All the modification to the FileLocker are done
|
||||||
|
* using this struct.
|
||||||
|
*/
|
||||||
|
struct FileLockerAccessor {
|
||||||
|
friend FileLocker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single file to the current locker.
|
||||||
|
*/
|
||||||
|
bool AddFile(const std::filesystem::path &path);
|
||||||
|
|
||||||
|
FileLockerAccessor(const FileLockerAccessor &) = delete;
|
||||||
|
FileLockerAccessor(FileLockerAccessor &&) = default;
|
||||||
|
FileLockerAccessor &operator=(const FileLockerAccessor &) = delete;
|
||||||
|
FileLockerAccessor &operator=(FileLockerAccessor &&) = default;
|
||||||
|
|
||||||
|
~FileLockerAccessor();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit FileLockerAccessor(FileRetainer *retainer, size_t locker_id);
|
||||||
|
|
||||||
|
FileRetainer *file_retainer_;
|
||||||
|
std::shared_lock<utils::RWLock> retainer_guard_;
|
||||||
|
size_t locker_id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file.
|
||||||
|
* If the file is inside any of the lockers or some thread is modifying
|
||||||
|
* any of the lockers, the file will be deleted after all the locks are
|
||||||
|
* lifted.
|
||||||
|
*/
|
||||||
|
void DeleteFile(const std::filesystem::path &path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a new locker.
|
||||||
|
*/
|
||||||
|
FileLocker AddLocker();
|
||||||
|
|
||||||
|
explicit FileRetainer() = default;
|
||||||
|
FileRetainer(const FileRetainer &) = delete;
|
||||||
|
FileRetainer(FileRetainer &&) = delete;
|
||||||
|
FileRetainer &operator=(const FileRetainer &) = delete;
|
||||||
|
FileRetainer &operator=(FileRetainer &&) = delete;
|
||||||
|
|
||||||
|
~FileRetainer();
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] bool FileLocked(const std::filesystem::path &path);
|
||||||
|
void DeleteOrAddToQueue(const std::filesystem::path &path);
|
||||||
|
void CleanQueue();
|
||||||
|
|
||||||
|
utils::RWLock main_lock_{RWLock::Priority::WRITE};
|
||||||
|
|
||||||
|
std::atomic<size_t> active_accessors_{0};
|
||||||
|
std::atomic<size_t> next_locker_id_{0};
|
||||||
|
utils::Synchronized<std::map<size_t, std::set<std::filesystem::path>>,
|
||||||
|
utils::SpinLock>
|
||||||
|
lockers_;
|
||||||
|
|
||||||
|
utils::Synchronized<std::set<std::filesystem::path>, utils::SpinLock>
|
||||||
|
files_for_deletion_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace utils
|
@ -195,6 +195,9 @@ target_link_libraries(${test_prefix}skip_list mg-utils)
|
|||||||
add_unit_test(small_vector.cpp)
|
add_unit_test(small_vector.cpp)
|
||||||
target_link_libraries(${test_prefix}small_vector mg-utils)
|
target_link_libraries(${test_prefix}small_vector mg-utils)
|
||||||
|
|
||||||
|
add_unit_test(utils_file_locker.cpp)
|
||||||
|
target_link_libraries(${test_prefix}utils_file_locker mg-utils fmt)
|
||||||
|
|
||||||
|
|
||||||
# Test mg-storage-v2
|
# Test mg-storage-v2
|
||||||
|
|
||||||
|
209
tests/unit/utils_file_locker.cpp
Normal file
209
tests/unit/utils_file_locker.cpp
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <utils/file_locker.hpp>
|
||||||
|
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
class FileLockerTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
std::filesystem::path testing_directory{
|
||||||
|
std::filesystem::temp_directory_path() /
|
||||||
|
"MG_test_unit_utils_file_locker"};
|
||||||
|
|
||||||
|
void SetUp() override { Clear(); }
|
||||||
|
|
||||||
|
void TearDown() override { Clear(); }
|
||||||
|
|
||||||
|
void CreateFiles(const size_t files_number) {
|
||||||
|
const auto save_path = std::filesystem::current_path();
|
||||||
|
std::filesystem::create_directory(testing_directory);
|
||||||
|
std::filesystem::current_path(testing_directory);
|
||||||
|
|
||||||
|
for (auto i = 1; i <= files_number; ++i) {
|
||||||
|
std::ofstream file(fmt::format("{}", i));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::current_path(save_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Clear() {
|
||||||
|
if (!std::filesystem::exists(testing_directory)) return;
|
||||||
|
std::filesystem::remove_all(testing_directory);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(FileLockerTest, DeleteWhileLocking) {
|
||||||
|
CreateFiles(1);
|
||||||
|
utils::FileRetainer file_retainer;
|
||||||
|
auto t1 = std::thread([&]() {
|
||||||
|
auto locker = file_retainer.AddLocker();
|
||||||
|
{
|
||||||
|
auto acc = locker.Access();
|
||||||
|
std::this_thread::sleep_for(100ms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auto file = testing_directory / "1";
|
||||||
|
auto t2 = std::thread([&]() {
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
file_retainer.DeleteFile(file);
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(file));
|
||||||
|
});
|
||||||
|
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FileLockerTest, DeleteWhileInLocker) {
|
||||||
|
CreateFiles(1);
|
||||||
|
utils::FileRetainer file_retainer;
|
||||||
|
const auto file = testing_directory / "1";
|
||||||
|
auto t1 = std::thread([&]() {
|
||||||
|
auto locker = file_retainer.AddLocker();
|
||||||
|
{
|
||||||
|
auto acc = locker.Access();
|
||||||
|
acc.AddFile(file);
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(100ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto t2 = std::thread([&]() {
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
file_retainer.DeleteFile(file);
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(file));
|
||||||
|
});
|
||||||
|
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FileLockerTest, MultipleLockers) {
|
||||||
|
CreateFiles(3);
|
||||||
|
utils::FileRetainer file_retainer;
|
||||||
|
const auto file1 = testing_directory / "1";
|
||||||
|
const auto file2 = testing_directory / "2";
|
||||||
|
const auto common_file = testing_directory / "3";
|
||||||
|
|
||||||
|
auto t1 = std::thread([&]() {
|
||||||
|
auto locker = file_retainer.AddLocker();
|
||||||
|
{
|
||||||
|
auto acc = locker.Access();
|
||||||
|
acc.AddFile(file1);
|
||||||
|
acc.AddFile(common_file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto t2 = std::thread([&]() {
|
||||||
|
auto locker = file_retainer.AddLocker();
|
||||||
|
{
|
||||||
|
auto acc = locker.Access();
|
||||||
|
acc.AddFile(file2);
|
||||||
|
acc.AddFile(common_file);
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(200ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto t3 = std::thread([&]() {
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
file_retainer.DeleteFile(file1);
|
||||||
|
file_retainer.DeleteFile(file2);
|
||||||
|
file_retainer.DeleteFile(common_file);
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file1));
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(file2));
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(common_file));
|
||||||
|
});
|
||||||
|
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
t3.join();
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file1));
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file2));
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(common_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FileLockerTest, MultipleLockersAndDeleters) {
|
||||||
|
constexpr size_t files_number = 2000;
|
||||||
|
|
||||||
|
CreateFiles(files_number);
|
||||||
|
// setup random number generator
|
||||||
|
std::random_device r;
|
||||||
|
|
||||||
|
std::default_random_engine engine(r());
|
||||||
|
std::uniform_int_distribution<int> random_short_wait(1, 10);
|
||||||
|
std::uniform_int_distribution<int> random_wait(1, 100);
|
||||||
|
std::uniform_int_distribution<int> file_distribution(0, files_number - 1);
|
||||||
|
|
||||||
|
const auto sleep_for = [&](int milliseconds) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto random_file = [&]() {
|
||||||
|
return testing_directory / fmt::format("{}", file_distribution(engine));
|
||||||
|
};
|
||||||
|
|
||||||
|
utils::FileRetainer file_retainer;
|
||||||
|
|
||||||
|
constexpr size_t thread_num = 8;
|
||||||
|
constexpr size_t file_access_num = 800;
|
||||||
|
constexpr size_t file_delete_num = 1000;
|
||||||
|
|
||||||
|
std::vector<std::thread> accessor_threads;
|
||||||
|
accessor_threads.reserve(thread_num);
|
||||||
|
for (auto i = 0; i < thread_num - 1; ++i) {
|
||||||
|
accessor_threads.emplace_back([&]() {
|
||||||
|
sleep_for(random_wait(engine));
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> locked_files;
|
||||||
|
auto locker = file_retainer.AddLocker();
|
||||||
|
{
|
||||||
|
auto acc = locker.Access();
|
||||||
|
for (auto i = 0; i < file_access_num; ++i) {
|
||||||
|
auto file = random_file();
|
||||||
|
if (acc.AddFile(file)) {
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(file));
|
||||||
|
locked_files.emplace_back(std::move(file));
|
||||||
|
} else {
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file));
|
||||||
|
}
|
||||||
|
sleep_for(random_short_wait(engine));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep_for(random_wait(engine));
|
||||||
|
for (const auto &file : locked_files) {
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(file));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> deleted_files;
|
||||||
|
auto deleter = std::thread([&]() {
|
||||||
|
sleep_for(random_short_wait(engine));
|
||||||
|
for (auto i = 0; i < file_delete_num; ++i) {
|
||||||
|
auto file = random_file();
|
||||||
|
if (std::filesystem::exists(file)) {
|
||||||
|
file_retainer.DeleteFile(file);
|
||||||
|
deleted_files.emplace_back(std::move(file));
|
||||||
|
}
|
||||||
|
sleep_for(random_short_wait(engine));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto &thread : accessor_threads) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleter.join();
|
||||||
|
|
||||||
|
for (const auto &file : deleted_files) {
|
||||||
|
ASSERT_FALSE(std::filesystem::exists(file));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user