diff --git a/doc/sfschunkserver.cfg.5.adoc b/doc/sfschunkserver.cfg.5.adoc index f5b60878a..96511eb8e 100644 --- a/doc/sfschunkserver.cfg.5.adoc +++ b/doc/sfschunkserver.cfg.5.adoc @@ -153,6 +153,22 @@ the operation is considered failed and is immediately aborted (default: 1000) replication. After this timeout, next wave of read requests is sent to other chunkservers (default: 500) +*CHUNK_TRASH_ENABLED*:: enables or disables the chunk trash feature. When +enabled, deleted chunks are moved to a trash directory instead of being +immediately removed. (Default: 1) + +*CHUNK_TRASH_EXPIRATION_SECONDS*:: specifies the timeout in seconds for chunks to remain +in the trash before being permanently deleted. (Default: 259200) + +*CHUNK_TRASH_FREE_SPACE_THRESHOLD_GB*:: sets the available space threshold in +gigabytes. If the available space on the disk falls below this threshold, the +system will start deleting older chunks from the trash to free up space. +(Default: 1024) + +*CHUNK_TRASH_GC_BATCH_SIZE*:: defines the bulk size for the garbage collector +when processing chunks in the trash. This determines how many files are +processed in each garbage collection cycle. (Default: 1000) + *LOG_LEVEL*:: Setup logging. Uses the environment variable SAUNAFS_LOG_LEVEL or config value LOG_LEVEL to determine logging level. Valid log levels are - 'trace' diff --git a/src/admin/dump_config_command.cc b/src/admin/dump_config_command.cc index def7befff..54df51da1 100644 --- a/src/admin/dump_config_command.cc +++ b/src/admin/dump_config_command.cc @@ -171,6 +171,11 @@ const static std::unordered_map defaultOptionsCS = { {"REPLICATION_TOTAL_TIMEOUT_MS", "60000"}, {"REPLICATION_CONNECTION_TIMEOUT_MS", "1000"}, {"REPLICATION_WAVE_TIMEOUT_MS", "500"}, + {"CHUNK_TRASH_ENABLED", "1"}, + {"CHUNK_TRASH_EXPIRATION_SECONDS", "259200"}, + {"CHUNK_TRASH_FREE_SPACE_THRESHOLD_GB", "1024"}, + {"CHUNK_TRASH_GC_BATCH_SIZE", "1000"}, + {"CHUNK_TRASH_GC_SPACE_RECOVERY_BATCH_SIZE", "100"}, }; const static std::unordered_map defaultOptionsMeta = { diff --git a/src/chunkserver/chunkserver-common/CMakeLists.txt b/src/chunkserver/chunkserver-common/CMakeLists.txt index f34f52150..99b9865c8 100644 --- a/src/chunkserver/chunkserver-common/CMakeLists.txt +++ b/src/chunkserver/chunkserver-common/CMakeLists.txt @@ -3,6 +3,16 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}) collect_sources(CHUNKSERVER_PLUGINS) shared_add_library(chunkserver-common ${CHUNKSERVER_PLUGINS_SOURCES}) target_link_libraries(chunkserver-common safsprotocol sfscommon - ${Boost_LIBRARIES} ${ADDITIONAL_LIBS}) + ${Boost_LIBRARIES} ${ADDITIONAL_LIBS}) +list(REMOVE_ITEM CHUNKSERVER_PLUGINS_TESTS + ${CMAKE_CURRENT_SOURCE_DIR}/cmr_disk_unittest.cc) create_unittest(chunkserver-common ${CHUNKSERVER_PLUGINS_TESTS}) -link_unittest(chunkserver-common chunkserver-common sfscommon) +link_unittest(chunkserver-common gmock gtest chunkserver-common sfscommon) + +add_executable(chunkserver-common-mocked-time-unittest + ${CMAKE_CURRENT_SOURCE_DIR}/cmr_disk_unittest.cc) +target_link_options(chunkserver-common-mocked-time-unittest PRIVATE "-Wl,--wrap=time") +target_link_libraries(chunkserver-common-mocked-time-unittest gmock gtest + chunkserver-common gtest_main sfscommon ${Boost_LIBRARIES} + ${ADDITIONAL_LIBS}) +add_test(NAME CmrDiskTest COMMAND chunkserver-common-mocked-time-unittest) diff --git a/src/chunkserver/chunkserver-common/chunk_trash_index.cc b/src/chunkserver/chunkserver-common/chunk_trash_index.cc new file mode 100644 index 000000000..c7fc47b30 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_index.cc @@ -0,0 +1,135 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#include "chunk_trash_index.h" +#include "chunk_trash_manager.h" + +ChunkTrashIndex &ChunkTrashIndex::instance() { + static ChunkTrashIndex instance; + return instance; +} + +void ChunkTrashIndex::reset(const std::filesystem::path &diskPath) { + std::scoped_lock const lock(trashIndexMutex); + trashIndex.erase(diskPath); + trashIndex[diskPath] = {}; +} + +void +ChunkTrashIndex::add(const time_t &deletionTime, const std::string &filePath, + const std::string &diskPath) { + std::scoped_lock const lock(trashIndexMutex); + trashIndex[diskPath].emplace(deletionTime, filePath); +} + +void ChunkTrashIndex::removeInternal(const time_t &deletionTime, + const std::string &filePath, + const std::string &diskPath) { + auto range = trashIndex[diskPath].equal_range(deletionTime); + for (auto it = range.first; it != range.second; ++it) { + if (it->second == filePath) { + trashIndex[diskPath].erase(it); + return; // Avoid further iteration after removal + } + } +} + +void ChunkTrashIndex::remove(const time_t &deletionTime, + const std::string &filePath, + const std::string &diskPath) { + std::scoped_lock const lock(trashIndexMutex); + removeInternal(deletionTime, filePath, diskPath); +} + +void ChunkTrashIndex::remove(const time_t &deletionTime, + const std::string &filePath) { + std::scoped_lock const lock(trashIndexMutex); + for (const auto &diskEntry: trashIndex) { + removeInternal(deletionTime, filePath, diskEntry.first); + return; // Avoid further iteration after removal + } +} + +ChunkTrashIndex::TrashIndexDiskEntries +ChunkTrashIndex::getExpiredFiles(const time_t &timeLimit, size_t bulkSize) { + std::scoped_lock const lock(trashIndexMutex); + return getExpiredFilesInternal(timeLimit, bulkSize); +} + + +ChunkTrashIndex::TrashIndexDiskEntries +ChunkTrashIndex::getExpiredFilesInternal(const time_t &timeLimit, size_t bulkSize) { + TrashIndexDiskEntries expiredFiles; + size_t count = 0; + for (const auto &diskEntry: trashIndex) { + count += getExpiredFilesInternal(diskEntry.first, timeLimit, + expiredFiles, + bulkSize); + if (bulkSize != 0 && count >= bulkSize) { + break; + } + } + + return expiredFiles; +} + +size_t ChunkTrashIndex::getExpiredFilesInternal(const std::filesystem::path &diskPath, + const std::time_t &timeLimit, + std::unordered_map> &expiredFiles, + size_t bulkSize) { + auto &diskTrashIndex = trashIndex[diskPath]; + auto limit = diskTrashIndex.upper_bound(timeLimit); + + expiredFiles[diskPath] = {}; + size_t count = 0; + for (auto it = diskTrashIndex.begin(); it != limit; ++it) { + expiredFiles[diskPath].emplace(it->first, it->second); + if (bulkSize != 0 && ++count >= bulkSize) { + break; + } + } + + return count; +} + +ChunkTrashIndex::TrashIndexFileEntries +ChunkTrashIndex::getOlderFiles(const std::string &diskPath, + const size_t removalStepSize) { + std::scoped_lock const lock(trashIndexMutex); + auto &diskTrashIndex = trashIndex[diskPath]; + TrashIndexFileEntries olderFiles; + size_t count = 0; + for (auto it = diskTrashIndex.begin(); it != diskTrashIndex.end(); ++it) { + olderFiles.emplace(it->first, it->second); + if (removalStepSize != 0 && ++count >= removalStepSize) { + break; + } + } + + return olderFiles; +} + +std::vector ChunkTrashIndex::getDiskPaths() { + std::scoped_lock const lock(trashIndexMutex); + std::vector diskPaths; + for (const auto &diskEntry: trashIndex) { + diskPaths.push_back(diskEntry.first); + } + + return diskPaths; +} diff --git a/src/chunkserver/chunkserver-common/chunk_trash_index.h b/src/chunkserver/chunkserver-common/chunk_trash_index.h new file mode 100644 index 000000000..e1cab1fac --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_index.h @@ -0,0 +1,174 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ +#pragma once + +#include "common/platform.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +#include + +#pragma GCC diagnostic pop + +#include +#include +#include +#include +#include +#include + +/** + * @brief Manages the index of files in the chunk trash. + * + * This class provides functionality to add, remove, and retrieve files + * based on their deletion time, ensuring thread safety with mutex protection. + */ +class ChunkTrashIndex { +public: + using TrashIndexFileEntries = std::multimap; ///< Type for storing file entries with their deletion time. + using TrashIndexDiskEntries = std::unordered_map; ///< Type for storing disk path entries and their associated file entries. + using TrashIndexType = TrashIndexDiskEntries; ///< Alias for the trash index type. + + /** + * @brief Gets the singleton instance of the ChunkTrashIndex. + * + * @return Reference to the singleton instance of ChunkTrashIndex. + */ + static ChunkTrashIndex &instance(); + + /** + * @brief Resets the trash index for a specific disk path. + * + * This method clears all entries associated with the specified disk path. + * + * @param diskPath The path of the disk whose index will be reset. + */ + void reset(const std::filesystem::path &diskPath); + + /** + * @brief Retrieves expired files from the trash index. + * + * This method returns a map of expired files across all disks with the + * specified time limit and bulk size. + * + * @param timeLimit The time limit to determine expired files. + * @param bulkSize The maximum number of files to retrieve (default is 0, which means no limit). + * @return A map containing expired files. + */ + TrashIndexDiskEntries getExpiredFiles(const std::time_t &timeLimit, + size_t bulkSize = 0); + + /** + * @brief Adds a file entry to the trash index with its deletion time. + * + * @param deletionTime The time when the file was deleted. + * @param filePath The path of the file being added. + * @param diskPath The path of the disk associated with the file. + */ + void add(const std::time_t &deletionTime, const std::string &filePath, + const std::string &diskPath); + + /** + * @brief Removes a file entry from the trash index by its deletion time and path. + * + * @param deletionTime The time when the file was deleted. + * @param filePath The path of the file being removed. + */ + void remove(const time_t &deletionTime, const std::string &filePath); + + /** + * @brief Removes a file entry from the trash index for a specific disk path. + * + * @param deletionTime The time when the file was deleted. + * @param filePath The path of the file being removed. + * @param diskPath The path of the disk associated with the file. + */ + void remove(const time_t &deletionTime, const std::string &filePath, + const std::string &diskPath); + + // Deleted to enforce singleton behavior + ChunkTrashIndex( + const ChunkTrashIndex &) = delete; ///< Copy constructor is deleted. + + ChunkTrashIndex &operator=( + const ChunkTrashIndex &) = delete; ///< Copy assignment operator is deleted. + + ChunkTrashIndex( + ChunkTrashIndex &&) = delete; ///< Move constructor is deleted. + + ChunkTrashIndex &operator=( + ChunkTrashIndex &&) = delete; ///< Move assignment operator is deleted. + + TrashIndexFileEntries + getOlderFiles(const std::string &diskPath, const size_t removalStepSize); + + std::vector getDiskPaths(); + +private: + // Constructor is private to enforce singleton behavior + ChunkTrashIndex() = default; ///< Default constructor. + + ~ChunkTrashIndex() = default; ///< Destructor. + + + TrashIndexType trashIndex; ///< The main data structure holding the trash index. + std::mutex trashIndexMutex; ///< Mutex for thread-safe access to the trash index. + + + + /** + * @brief Retrieves expired files from the trash index for a specific disk path. + * + * This method populates the provided expiredFiles map with entries that + * have a deletion time earlier than the specified time limit. + * + * @param diskPath The path of the disk to retrieve expired files from. + * @param timeLimit The time limit to determine expired files. + * @param expiredFiles Reference to a map that will be populated with expired files. + * @param bulkSize The maximum number of files to retrieve (default is 0, which means no limit). + * @return The number of expired files retrieved. + */ + size_t getExpiredFilesInternal(const std::filesystem::path &diskPath, + const std::time_t &timeLimit, + std::unordered_map> &expiredFiles, + size_t bulkSize = 0); + + /** + * @brief Retrieves expired files from the trash index. + * + * This method returns a map of expired files across all disks with the + * specified time limit and bulk size. + * + * @param timeLimit The time limit to determine expired files. + * @param bulkSize The maximum number of files to retrieve (default is 0, which means no limit). + * @return A map containing expired files. + */ + TrashIndexDiskEntries getExpiredFilesInternal(const std::time_t &timeLimit, + size_t bulkSize = 0); + + /** + * @brief Removes a file entry from the trash index for a specific disk path. + * @param deletionTime + * @param filePath + * @param diskPath + */ + void removeInternal(const time_t &deletionTime, const std::string &filePath, + const std::string &diskPath); +}; diff --git a/src/chunkserver/chunkserver-common/chunk_trash_index_unittest.cc b/src/chunkserver/chunkserver-common/chunk_trash_index_unittest.cc new file mode 100644 index 000000000..115115560 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_index_unittest.cc @@ -0,0 +1,94 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#include + +#include "chunk_trash_index.h" + +class ChunkTrashIndexTest : public ::testing::Test { +protected: + void SetUp() override { + // Resetting state before each test to ensure no interference + ChunkTrashIndex::instance().reset("/testDisk"); + } + + void TearDown() override { + // Resetting state after each test to clean up + ChunkTrashIndex::instance().reset("/testDisk"); + } +}; + +TEST_F(ChunkTrashIndexTest, SingletonInstance) { + // Ensure that multiple calls to instance() return the same object + ChunkTrashIndex &index1 = ChunkTrashIndex::instance(); + ChunkTrashIndex &index2 = ChunkTrashIndex::instance(); + EXPECT_EQ(&index1, &index2); +} + +TEST_F(ChunkTrashIndexTest, AddFileEntry) { + auto &index = ChunkTrashIndex::instance(); + index.add(1234567890, "/.trash.bin/path/to/file", "/testDisk"); + + auto expiredFiles = index.getExpiredFiles(1234567891); + ASSERT_EQ(expiredFiles.size(), 1); + EXPECT_EQ(expiredFiles["/testDisk"].begin()->second, "/.trash.bin/path/to/file"); +} + +TEST_F(ChunkTrashIndexTest, RemoveFileEntry) { + auto &index = ChunkTrashIndex::instance(); + index.add(1234567890, "/path/to/file", "/testDisk"); + index.remove(1234567890, "/path/to/file", "/testDisk"); + + auto expiredFiles = index.getExpiredFiles(1234567891); + EXPECT_EQ(expiredFiles["/testDisk"].size(), 0); +} + +TEST_F(ChunkTrashIndexTest, ThreadSafety) { + auto &index = ChunkTrashIndex::instance(); + std::thread t1([&index]() { + index.add(1234567890, "/path/to/file1", "/testDisk"); + }); + std::thread t2([&index]() { + index.add(1234567891, "/path/to/file2", "/testDisk"); + }); + + t1.join(); + t2.join(); + + auto expiredFiles = index.getExpiredFiles(1234567892); + ASSERT_EQ(expiredFiles.size(), 1); + EXPECT_EQ(expiredFiles["/testDisk"].size(), 2); +} + +TEST_F(ChunkTrashIndexTest, RemoveFileEntry2) { + auto &index = ChunkTrashIndex::instance(); + + // Arrange: Add a file to the trash index + std::string diskPath = "/testDisk"; + std::string filePath1 = diskPath + "/.trash.bin/path/to/file1"; + time_t deletionTime = 1234567890; + index.add(deletionTime, filePath1, diskPath); + + // Act: Remove the file entries + index.remove(deletionTime, filePath1); + + // Assert: Verify the file entry is removed + auto expiredFiles = index.getExpiredFiles(deletionTime + 1); + EXPECT_EQ(expiredFiles.size(), 1); + EXPECT_EQ(expiredFiles[diskPath].size(), 0); +} diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager.cc b/src/chunkserver/chunkserver-common/chunk_trash_manager.cc new file mode 100644 index 000000000..5dc9335c2 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager.cc @@ -0,0 +1,66 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#include + +#include "chunk_trash_manager.h" +#include "chunk_trash_manager_impl.h" +#include "config/cfg.h" + +u_short ChunkTrashManager::isEnabled = 1; + +ChunkTrashManager::ImplentationPtr ChunkTrashManager::pImpl = + std::make_shared(); + +ChunkTrashManager &ChunkTrashManager::instance(ImplentationPtr newImpl) { + static ChunkTrashManager instance; + if (newImpl) { + pImpl = newImpl; + } + return instance; +} + +int ChunkTrashManager::moveToTrash(const std::filesystem::path &filePath, + const std::filesystem::path &diskPath, + const std::time_t &deletionTime) { + if(!isEnabled) { + return 0; + } + assert(pImpl && "Implementation should be set"); + return pImpl->moveToTrash(filePath, diskPath, deletionTime); +} + +int ChunkTrashManager::init(const std::string &diskPath) { + reloadConfig(); + assert(pImpl && "Implementation should be set"); + return pImpl->init(diskPath); +} + +void ChunkTrashManager::collectGarbage() { + if(!isEnabled) { + return; + } + assert(pImpl && "Implementation should be set"); + pImpl->collectGarbage(); +} + +void ChunkTrashManager::reloadConfig() { + assert(pImpl && "Implementation should be set"); + isEnabled = cfg_get("CHUNK_TRASH_ENABLED", isEnabled); + pImpl->reloadConfig(); +} diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager.h b/src/chunkserver/chunkserver-common/chunk_trash_manager.h new file mode 100644 index 000000000..b1dc257f7 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager.h @@ -0,0 +1,118 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#pragma once + +#include "common/platform.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +#include + +#pragma GCC diagnostic pop + +#include + +class IChunkTrashManagerImpl { +public: + virtual ~IChunkTrashManagerImpl() = default; + + virtual int + moveToTrash(const std::filesystem::path &, const std::filesystem::path &, + const std::time_t &) = 0; + + virtual int init(const std::string &) = 0; + + virtual void collectGarbage() = 0; + + virtual void reloadConfig() = 0; +}; + +/** + * @brief Manages the trash files in the system. + * + * This class provides functionality to initialize the trash, move files to the trash, + * and manage expired files. It enforces singleton behavior to ensure only one instance + * of the manager is active. + */ +class ChunkTrashManager { +public: + using Implentation = IChunkTrashManagerImpl; + using ImplentationPtr = std::shared_ptr; + + /** + * @brief Gets the singleton instance of the ChunkTrashManager. + * + * @return Reference to the singleton instance of ChunkTrashManager. + */ + static ChunkTrashManager &instance(ImplentationPtr newImpl = nullptr); + + /** + * @brief The name of the trash directory. + */ + static constexpr const std::string kTrashDirname = ".trash.bin"; + + static u_short isEnabled; ///< Flag to enable or disable the trash manager. + + /** + * @brief Initializes the trash directory for the specified disk. + * + * @param diskPath The path of the disk where the trash directory will be initialized. + */ + int init(const std::string &diskPath); + + /** + * @brief Moves a file to the trash directory. + * + * @param filePath The path of the file to be moved. + * @param diskPath The path to the disk containing the file. + * @param deletionTime The time when the file was deleted. + * @return 0 on success, error code otherwise. + */ + int moveToTrash(const std::filesystem::path &filePath, + const std::filesystem::path &diskPath, + const std::time_t &deletionTime); + + /** + * @brief Runs the garbage collection process, which includes + * removing expired files, freeing up disk space, and cleaning + * empty directories. + */ + void collectGarbage(); + + /// Reloads the configuration for the trash manager. + void reloadConfig(); + + // Deleted to enforce singleton behavior + ChunkTrashManager(const ChunkTrashManager &) = delete; + ChunkTrashManager &operator=(const ChunkTrashManager &) = delete; + ChunkTrashManager(ChunkTrashManager &&) = delete; + ChunkTrashManager &operator=(ChunkTrashManager &&) = delete; + + ~ChunkTrashManager() = default; ///< Destructor + +private: + /// Constructor is private to enforce singleton behavior + ChunkTrashManager() = default; + + /// Pointer to the singleton instance of the trash manager implementation. + static ChunkTrashManager::ImplentationPtr pImpl; + +}; diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.cc b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.cc new file mode 100644 index 000000000..a9cd6384d --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.cc @@ -0,0 +1,299 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +#include + +#pragma GCC diagnostic pop + +#include +#include +#include + +#include "errors/saunafs_error_codes.h" +#include "slogger/slogger.h" + +#include "chunk_trash_manager_impl.h" +#include "config/cfg.h" + +namespace fs = std::filesystem; + +size_t ChunkTrashManagerImpl::kAvailableThresholdGB = 10; +size_t ChunkTrashManagerImpl::kTrashTimeLimitSeconds = 259200; +size_t ChunkTrashManagerImpl::kTrashGarbageCollectorBulkSize = 1000; +size_t ChunkTrashManagerImpl::kGarbageCollectorSpaceRecoveryStep = 10; + +void ChunkTrashManagerImpl::reloadConfig() { + kAvailableThresholdGB = cfg_get("CHUNK_TRASH_FREE_SPACE_THRESHOLD_GB", + kAvailableThresholdGB); + kTrashTimeLimitSeconds = cfg_get("CHUNK_TRASH_EXPIRATION_SECONDS", + kTrashTimeLimitSeconds); + kTrashGarbageCollectorBulkSize = cfg_get("CHUNK_TRASH_GC_BATCH_SIZE", + kTrashGarbageCollectorBulkSize); + kGarbageCollectorSpaceRecoveryStep = cfg_get( + "CHUNK_TRASH_GC_SPACE_RECOVERY_BATCH_SIZE", + kGarbageCollectorSpaceRecoveryStep); +} + +std::string +ChunkTrashManagerImpl::getTimeString(std::time_t time1) { +// + std::tm *utcTime = std::gmtime(&time1); // Convert to UTC + + std::ostringstream oss; + oss << std::put_time(utcTime, kTimeStampFormat.c_str()); + return oss.str(); +} + +std::time_t ChunkTrashManagerImpl::getTimeFromString( + const std::string &timeString, int &errorCode) { + errorCode = SAUNAFS_STATUS_OK; + std::tm time = {}; + std::istringstream stringReader(timeString); + stringReader >> std::get_time(&time, kTimeStampFormat.c_str()); + if (stringReader.fail()) { + safs_pretty_syslog(LOG_ERR, "Failed to parse time string: %s", + timeString.c_str()); + errorCode = SAUNAFS_ERROR_EINVAL; + } + return std::mktime(&time); +} + +int ChunkTrashManagerImpl::getMoveDestinationPath(const std::string &filePath, + const std::string &sourceRoot, + const std::string &destinationRoot, + std::string &destinationPath) { + if (filePath.find(sourceRoot) != 0) { + safs_pretty_syslog(LOG_ERR, + "File path does not contain source root: %s", + filePath.c_str()); + return SAUNAFS_ERROR_EINVAL; + } + + destinationPath = + destinationRoot + "/" + filePath.substr(sourceRoot.size()); + return SAUNAFS_STATUS_OK; +} + + +int ChunkTrashManagerImpl::moveToTrash( + const fs::path &filePath, + const fs::path &diskPath, + const std::time_t &deletionTime) { + + if (!fs::exists(filePath)) { + safs_pretty_syslog(LOG_ERR, "File does not exist: %s", + filePath.string().c_str()); + return SAUNAFS_ERROR_ENOENT; + } + + const fs::path trashDir = getTrashDir(diskPath); + fs::create_directories(trashDir); + + const std::string deletionTimestamp = getTimeString(deletionTime); + std::string trashFilename; + + auto errorCode = getMoveDestinationPath(filePath.string(), + diskPath.string(), + trashDir.string(), trashFilename); + if (errorCode != SAUNAFS_STATUS_OK) { + return errorCode; + } + + trashFilename += "." + deletionTimestamp; + + try { + fs::create_directories(fs::path(trashFilename).parent_path()); + fs::rename(filePath, trashFilename); + trashIndex->add(deletionTime, trashFilename, + diskPath.string()); + } catch (const fs::filesystem_error &e) { + safs_pretty_syslog(LOG_ERR, + "Failed to move file to trash: %s, error: %s", + filePath.string().c_str(), e.what()); + return SAUNAFS_ERROR_NOTDONE; + } + + return SAUNAFS_STATUS_OK; +} + +void ChunkTrashManagerImpl::removeTrashFiles( + const ChunkTrashIndex::TrashIndexDiskEntries &filesToRemove) { + for (const auto &[diskPath, fileEntries]: filesToRemove) { + for (const auto &fileEntry: fileEntries) { + if (removeFileFromTrash(fileEntry.second) != SAUNAFS_STATUS_OK) { + continue; + } + trashIndex->remove(fileEntry.first, fileEntry.second, diskPath); + } + } +} + +fs::path ChunkTrashManagerImpl::getTrashDir(const fs::path &diskPath) { + return diskPath / ChunkTrashManager::kTrashDirname; +} + +int ChunkTrashManagerImpl::init(const std::string &diskPath) { + reloadConfig(); + const fs::path trashDir = getTrashDir(diskPath); + if (!fs::exists(trashDir)) { + fs::create_directories(trashDir); + } + + trashIndex->reset(diskPath); + + for (const auto &file: fs::recursive_directory_iterator(trashDir)) { + if (fs::is_regular_file(file) && isTrashPath(file.path().string())) { + const std::string filename = file.path().filename().string(); + const std::string deletionTimeStr = filename.substr( + filename.find_last_of('.') + 1); + if (!isValidTimestampFormat(deletionTimeStr)) { + safs_pretty_syslog(LOG_ERR, + "Invalid timestamp format in file: %s, skipping.", + file.path().string().c_str()); + continue; + } + int errorCode; + const std::time_t deletionTime = getTimeFromString( + deletionTimeStr, errorCode); + if (errorCode != SAUNAFS_STATUS_OK) { + safs_pretty_syslog(LOG_ERR, "Failed to parse deletion time " + "from file: %s, skipping.", + file.path().string().c_str()); + continue; + } + trashIndex->add(deletionTime, file.path().string(), + diskPath); + } + } + + return SAUNAFS_STATUS_OK; +} + +bool ChunkTrashManagerImpl::isValidTimestampFormat( + const std::string ×tamp) { + return timestamp.size() == kTimeStampLength && + std::all_of(timestamp.begin(), timestamp.end(), ::isdigit); +} + +void ChunkTrashManagerImpl::removeExpiredFiles( + const time_t &timeLimit, size_t bulkSize) { + const auto expiredFilesCollection = trashIndex->getExpiredFiles(timeLimit, + bulkSize); + removeTrashFiles(expiredFilesCollection); +} + +void ChunkTrashManagerImpl::cleanEmptyFolders(const std::string &directory) { + if (!fs::exists(directory) || !fs::is_directory(directory)) { + return; // Invalid path, so we do nothing + } + + // Iterate through the directory's contents + for (const auto &entry: fs::directory_iterator(directory)) { + if (fs::is_directory(entry.path())) { + // Recursively clean subdirectories + cleanEmptyFolders(entry.path()); + } + } + + if (!isTrashPath(directory)) { + return; // We only clean up trash directories + } + + // After processing subdirectories, check if the directory is now empty + if (fs::is_empty(directory)) { + removeFileFromTrash(directory); + } +} + +size_t ChunkTrashManagerImpl::checkAvailableSpace( + const std::string &diskPath) { + struct statvfs stat{}; + if (statvfs(diskPath.c_str(), &stat) != 0) { + safs_pretty_syslog(LOG_ERR, "Failed to get file system statistics"); + return 0; + } + size_t const kGiBMultiplier = 1 << 30; + size_t const availableGb = stat.f_bavail * stat.f_frsize / kGiBMultiplier; + return availableGb; +} + +void ChunkTrashManagerImpl::makeSpace( + const std::string &diskPath, + const size_t spaceAvailabilityThreshold, + const size_t recoveryStep) { + size_t availableSpace = checkAvailableSpace(diskPath); + while (availableSpace < spaceAvailabilityThreshold) { + const auto olderFilesCollection = trashIndex->getOlderFiles(diskPath, + recoveryStep); + if (olderFilesCollection.empty()) { + break; + } + removeTrashFiles({{diskPath, olderFilesCollection}}); + availableSpace = checkAvailableSpace(diskPath); + } +} + +void ChunkTrashManagerImpl::makeSpace( + const size_t spaceAvailabilityThreshold, const size_t recoveryStep) { + for (const auto &diskPath: trashIndex->getDiskPaths()) { + makeSpace(diskPath, spaceAvailabilityThreshold, recoveryStep); + } +} + +void ChunkTrashManagerImpl::cleanEmptyFolders() { + for (const auto &diskPath: trashIndex->getDiskPaths()) { + fs::path trashDir = getTrashDir(diskPath); + cleanEmptyFolders(trashDir.string()); + } +} + +void ChunkTrashManagerImpl::collectGarbage() { + if (!ChunkTrashManager::isEnabled) { + return; + } + std::time_t const currentTime = std::time(nullptr); + std::time_t const expirationTime = currentTime - kTrashTimeLimitSeconds; + removeExpiredFiles(expirationTime, kTrashGarbageCollectorBulkSize); + makeSpace(kAvailableThresholdGB, + kGarbageCollectorSpaceRecoveryStep); +// cleanEmptyFolders(); +} + +bool ChunkTrashManagerImpl::isTrashPath(const std::string &filePath) { + return filePath.find("/" + ChunkTrashManager::kTrashDirname + "/") != + std::string::npos; +} + +int ChunkTrashManagerImpl::removeFileFromTrash(const std::string &filePath) { + if (!isTrashPath(filePath)) { + safs_pretty_syslog(LOG_ERR, "Invalid trash path: %s", filePath.c_str()); + return SAUNAFS_ERROR_EINVAL; + } + std::error_code errorCode; + fs::remove(filePath, errorCode); // Remove the file or directory + if (errorCode) { + safs_pretty_syslog(LOG_ERR, "Failed to remove file or directory: %s", + filePath.c_str()); + return SAUNAFS_ERROR_NOTDONE; + } + + return SAUNAFS_STATUS_OK; +} diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.h b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.h new file mode 100644 index 000000000..b91b88725 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl.h @@ -0,0 +1,198 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#pragma once + +#include "common/platform.h" + +#include +#include +#include +#include +#include + +#include "chunk_trash_manager.h" +#include "chunk_trash_index.h" + +class ChunkTrashManagerImpl : public IChunkTrashManagerImpl { + using TrashIndex = ChunkTrashIndex; +public: + /// A string representing the guard that distinguishes trash directories. + static constexpr const std::string kTrashGuardString = + "/" + (ChunkTrashManager::kTrashDirname) + "/"; + + /// The format of the timestamp used in file names, representing the deletion time. + static constexpr const std::string kTimeStampFormat = "%Y%m%d%H%M%S"; + + /// Length of the timestamp string expected in file names. + static constexpr const u_short kTimeStampLength = 14; + + /** + * @brief Initializes the trash manager for the specified disk path. + * @param diskPath The path of the disk to be initialized. + * @return Status code indicating success or failure. + */ + int init(const std::string &diskPath) override; + + /** + * @brief Moves a specified file to the trash directory. + * @param filePath The path of the file to be moved. + * @param diskPath The target disk path for the trash location. + * @param deletionTime The time at which the file was marked for deletion. + * @return Status code indicating success or failure. + */ + int moveToTrash(const std::filesystem::path &filePath, + const std::filesystem::path &diskPath, + const std::time_t &deletionTime) override; + + /** + * @brief Converts a given time value to a formatted string representation. + * @param time1 The time to be converted. + * @return A string representation of the time. + */ + static std::string getTimeString(time_t time1); + + /** + * @brief Removes files from the trash that are older than a specified time limit. + * @param timeLimit The cutoff time; files older than this will be removed. + * @param bulkSize The number of files to process in a batch operation. + */ + void removeExpiredFiles(const std::time_t &timeLimit, size_t bulkSize = 0); + + /** + * @brief Removes a set of specified files from the trash. + * @param filesToRemove The list of files to be permanently deleted. + */ + void removeTrashFiles(const ChunkTrashIndex::TrashIndexDiskEntries + &filesToRemove); + + /** + * @brief Checks if a given timestamp string matches the expected format. + * @param timestamp The timestamp string to validate. + * @return True if the timestamp format is valid; otherwise, false. + */ + static bool isValidTimestampFormat(const std::string ×tamp); + + /** + * @brief Recursively cleans empty folders from the specified directory. + * @param directory The directory in which to clean up empty folders. + */ + static void cleanEmptyFolders(const std::string &directory); + + /** + * @brief Cleans empty folders from the trash directories. + */ + void cleanEmptyFolders(); + + /** + * @brief Frees up disk space by removing files from the trash if the available space falls below a threshold. + * @param spaceAvailabilityThreshold The minimum required space in GB before taking action. + * @param recoveryStep The number of files to delete in each step until sufficient space is recovered. + */ + void makeSpace(size_t spaceAvailabilityThreshold, size_t recoveryStep); + + /** + * @brief Runs the garbage collection process, which includes + * removing expired files, freeing up disk space, and cleaning + * empty directories. + */ + void collectGarbage() override; + + /** + * @brief Converts a formatted timestamp string to a time value. + * @param timeString The string representation of the timestamp. + * @param errorCode (output) The value to return in case of an error. + * @return The corresponding time value. + */ + time_t getTimeFromString(const std::string &timeString, int &errorCode); + + /** + * @brief Checks the available disk space on the specified disk path. + * @param diskPath The path of the disk to check. + * @return The amount of available space in GB. + */ + static size_t checkAvailableSpace(const std::string &diskPath); + + /** + * @brief Frees up disk space on the specified disk by removing trash files. + * @param diskPath The path of the disk where space should be freed. + * @param spaceAvailabilityThreshold The minimum required space in GB before taking action. + * @param recoveryStep The number of files to delete in each step until sufficient space is recovered. + */ + void makeSpace(const std::string &diskPath, + size_t spaceAvailabilityThreshold, + size_t recoveryStep); + + /** + * @brief Reloads the configuration settings for the trash manager. + * Assuming that the configuration is already loaded by the ChunkServer. + */ + void reloadConfig() override; + +private: + /// Pointer to the singleton instance of the trash index used for managing trash files. + TrashIndex *trashIndex = &ChunkTrashIndex::instance(); + + /// Minimum available space threshold (in GB) before triggering garbage collection. + static size_t kAvailableThresholdGB; + + /// Time limit (in seconds) for files to be considered eligible for deletion. + static size_t kTrashTimeLimitSeconds; + + /// Number of files processed in each bulk operation during garbage collection. + static size_t kTrashGarbageCollectorBulkSize; + + /// Number of files to remove in each step to free up space when required. + static size_t kGarbageCollectorSpaceRecoveryStep; + + /** + * @brief Gets the trash directory for the specified disk path. + * @param diskPath The path of the disk to get the trash directory for. + * @return The path to the trash directory. + */ + static std::filesystem::path getTrashDir(const std::filesystem::path + &diskPath); + + /** + * @brief Gets the destination path for a file to be moved to the trash. + * @param filePath The path of the file to be moved. + * @param sourceRoot The root directory of the source file. + * @param destinationRoot The root directory of the destination file. + * @param destinationPath The path to the destination file (output). + * @return Status code indicating success or failure. + */ + int getMoveDestinationPath(const std::string &filePath, + const std::string &sourceRoot, + const std::string &destinationRoot, + std::string &destinationPath); + + /** + * @brief Checks the belonging of a file to the trash directory. + * @param filePath The path of the file to check. + * @return True if the file is in the trash directory; otherwise, false. + */ + static bool isTrashPath(const std::string &filePath); + + /** + * @brief Does the actual work of removing a file from the trash. + * @param filePath The path of the file to be removed. + * @return Status code indicating success or failure. + */ + static int removeFileFromTrash(const std::string &filePath); + +}; diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager_impl_unittest.cc b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl_unittest.cc new file mode 100644 index 000000000..3d61c9519 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager_impl_unittest.cc @@ -0,0 +1,317 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +#include +#include + +#pragma GCC diagnostic pop + +#include +#include +#include "chunk_trash_manager_impl.h" +#include "errors/saunafs_error_codes.h" // Include the error codes header + +namespace fs = std::filesystem; + +class ChunkTrashManagerImplTest : public ::testing::Test { +protected: + fs::path testDir; + ChunkTrashManagerImpl chunkTrashManagerImpl; + + void SetUp() override { + testDir = fs::temp_directory_path() / "chunk_trash_manager_test"; + fs::create_directories(testDir); + chunkTrashManagerImpl.init(testDir.string()); + } + + void TearDown() override { + fs::remove_all(testDir); + } +}; + +TEST_F(ChunkTrashManagerImplTest, MoveToTrashValidFile) { + fs::path filePath = testDir / "valid_file.txt"; + std::ofstream(filePath.string()); // Create a valid file + std::time_t deletionTime = 1729259531; + std::string const deletionTimeString = "20241018135211"; + + ASSERT_TRUE(fs::exists(filePath)); + int const result = chunkTrashManagerImpl.moveToTrash(filePath, testDir, + deletionTime); + ASSERT_EQ(result, SAUNAFS_STATUS_OK); + ASSERT_FALSE(fs::exists(filePath)); + ASSERT_TRUE(fs::exists((testDir / ".trash.bin/valid_file.txt.").string() + + deletionTimeString)); +} + +TEST_F(ChunkTrashManagerImplTest, MoveToTrashNonExistentFile) { + fs::path filePath = testDir / "non_existent_file.txt"; + std::time_t deletionTime = std::time(nullptr); + + int const result = chunkTrashManagerImpl.moveToTrash(filePath, testDir, + deletionTime); + ASSERT_NE(result, SAUNAFS_STATUS_OK); +} + +TEST_F(ChunkTrashManagerImplTest, MoveToTrashFileInNestedDirectory) { + fs::path nestedDir = testDir / "nested"; + fs::create_directories(nestedDir); + fs::path filePath = nestedDir / "nested_file.txt"; + std::ofstream(filePath.string()); + + std::time_t deletionTime = 1729259531; + std::string const deletionTimeString = "20241018135211"; + + int const result = chunkTrashManagerImpl.moveToTrash(filePath, testDir, + deletionTime); + ASSERT_EQ(result, SAUNAFS_STATUS_OK); + ASSERT_FALSE(fs::exists(filePath)); + ASSERT_TRUE(fs::exists( + (testDir / ".trash.bin/nested/nested_file.txt.").string() + + deletionTimeString)); +} + +TEST_F(ChunkTrashManagerImplTest, MoveToTrashReadOnlyTrashDirectory) { + fs::path readOnlyTrash = testDir / ".trash.bin"; + fs::create_directories(readOnlyTrash); + fs::permissions(readOnlyTrash, fs::perms::owner_read); // Make it read-only + + fs::path filePath = testDir / "valid_file.txt"; + std::ofstream(filePath.string()); + std::time_t deletionTime = 1729259531; + + int const result = chunkTrashManagerImpl.moveToTrash(filePath, testDir, + deletionTime); + ASSERT_NE(result, SAUNAFS_STATUS_OK); // Expect failure +} + +TEST_F(ChunkTrashManagerImplTest, MoveToTrashAlreadyTrashedFile) { + fs::path filePath = testDir / "valid_file.txt"; + std::ofstream(filePath.string()); + std::time_t deletionTime = 1729259531; + chunkTrashManagerImpl.moveToTrash(filePath, testDir, deletionTime); + + int const result = chunkTrashManagerImpl.moveToTrash(filePath, testDir, + deletionTime); + ASSERT_NE(result, SAUNAFS_STATUS_OK); // Expect failure +} + +TEST_F(ChunkTrashManagerImplTest, ConcurrentMoveToTrash) { + const int numFiles = 10; + std::vector threads; + for (int i = 0; i < numFiles; ++i) { + threads.emplace_back([this, i]() { + fs::path filePath = + testDir / ("concurrent_file_" + std::to_string(i) + ".txt"); + std::ofstream(filePath.string()); + std::time_t deletionTime = 1729259531; + chunkTrashManagerImpl.moveToTrash(filePath, testDir, deletionTime); + }); + } + + for (auto &thread: threads) { + thread.join(); + } + + // Check if all files are moved to trash + for (int i = 0; i < numFiles; ++i) { + ASSERT_FALSE(fs::exists( + testDir / ("concurrent_file_" + std::to_string(i) + ".txt"))); + ASSERT_TRUE(fs::exists((testDir / ".trash.bin/concurrent_file_") + .string() + std::to_string(i) + ".txt." + + "20241018135211")); + } +} + +TEST_F(ChunkTrashManagerImplTest, PerformanceTest) { + const int numFiles = 1000; + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < numFiles; ++i) { + fs::path filePath = + testDir / ("performance_file_" + std::to_string(i) + ".txt"); + std::ofstream(filePath.string()); + chunkTrashManagerImpl.moveToTrash(filePath, testDir, + std::time(nullptr)); + } + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end - start; + ASSERT_LE(elapsed.count(), 2.0); // Expect it to complete within 2 seconds +} + +// Testing initialization method +TEST_F(ChunkTrashManagerImplTest, InitCreatesTrashDirectory) { + fs::path trashPath = testDir / ".trash.bin"; + ASSERT_TRUE(fs::exists(trashPath)); // Ensure trash directory exists +} + +// Testing moving a file to trash +TEST_F(ChunkTrashManagerImplTest, MoveToTrash) { + fs::path filePath = testDir / "file_to_trash.txt"; + std::ofstream(filePath.string()); + std::time_t deletionTime = std::time(nullptr); + + ASSERT_EQ( + chunkTrashManagerImpl.moveToTrash(filePath, testDir, deletionTime), + SAUNAFS_STATUS_OK); + ASSERT_FALSE(fs::exists(filePath)); // Ensure file is moved to trash +} + +// Testing getTimeString method +TEST_F(ChunkTrashManagerImplTest, GetTimeString) { + + std::time_t testDeletionTime = 1729259531; + std::string testTimeString = "20241018135211"; + std::string timeString = chunkTrashManagerImpl.getTimeString( + testDeletionTime); + ASSERT_EQ(timeString, testTimeString); // Compare formatted strings +} + +// Testing removing expired files +TEST_F(ChunkTrashManagerImplTest, RemoveExpiredFiles) { + fs::path expiredFilePath = testDir / "expired_file.txt"; + std::ofstream(expiredFilePath.string()); + std::time_t oldDeletionTime = std::time(nullptr) - 86400; // 1 day ago + + chunkTrashManagerImpl.moveToTrash(expiredFilePath, testDir, + oldDeletionTime); + chunkTrashManagerImpl.removeExpiredFiles( + std::time(nullptr) - 3600); // 1 hour ago + + ASSERT_FALSE(fs::exists(expiredFilePath)); // Ensure expired file is removed +} + +// Testing cleanup of empty folders +TEST_F(ChunkTrashManagerImplTest, CleanEmptyFolders) { + // Create empty directories + fs::path emptyValidDirPath = testDir / ".trash.bin" / "empty_dir"; + // Create non-empty directory + fs::path nonEmptyDirPath = testDir / ".trash.bin" / "non_empty_dir"; + // Create an empty directory outside of trash + fs::path emptyInvalidDirPath = testDir / "empty_dir"; + fs::create_directory(emptyValidDirPath); + fs::create_directory(emptyInvalidDirPath); + fs::create_directory(nonEmptyDirPath); + std::ofstream(nonEmptyDirPath / "file.txt"); + + chunkTrashManagerImpl.cleanEmptyFolders(testDir.string()); + + // Ensure empty directories are removed + ASSERT_FALSE(fs::exists(emptyValidDirPath)); + // Ensure does not remove non-trash empty directory + ASSERT_TRUE(fs::exists(emptyInvalidDirPath)); + // Ensure does not remove non-empty directory + ASSERT_TRUE(fs::exists(nonEmptyDirPath)); + // Ensure the file in the non-empty directory is not removed + ASSERT_TRUE(fs::exists(nonEmptyDirPath / "file.txt")); +} + +// Testing checking valid timestamp format +TEST_F(ChunkTrashManagerImplTest, ValidTimestampFormat) { + ASSERT_TRUE(chunkTrashManagerImpl.isValidTimestampFormat( + "20231018120350")); // Valid format + ASSERT_FALSE(chunkTrashManagerImpl.isValidTimestampFormat( + "invalid_timestamp")); // Invalid format +} + +// Testing makeSpace functionality +TEST_F(ChunkTrashManagerImplTest, MakeSpaceRemovesOldFiles) { + fs::path lowSpaceFilePath = testDir / "file1.txt"; + std::ofstream(lowSpaceFilePath.string()); + + chunkTrashManagerImpl.moveToTrash(lowSpaceFilePath, testDir, + std::time(nullptr)); + + // Simulate low space condition and check removal + chunkTrashManagerImpl.makeSpace(1, 1); // 1 GB threshold + ASSERT_FALSE(fs::exists(lowSpaceFilePath)); // Ensure file is removed +} + +// Testing garbage collection process +TEST_F(ChunkTrashManagerImplTest, CollectGarbage) { + fs::path garbageFilePath = testDir / "garbage_file.txt"; + std::ofstream(garbageFilePath.string()); + + chunkTrashManagerImpl.moveToTrash(garbageFilePath, testDir, + std::time(nullptr)); + chunkTrashManagerImpl.collectGarbage(); + + ASSERT_FALSE( + fs::exists(garbageFilePath)); // Ensure garbage file is collected +} + +// Testing check available space functionality +TEST_F(ChunkTrashManagerImplTest, CheckAvailableSpace) { + size_t availableSpace = chunkTrashManagerImpl.checkAvailableSpace( + testDir.string()); + ASSERT_GT(availableSpace, 0); // Ensure there is available space +} + +// Testing makeSpace with specific disk path +TEST_F(ChunkTrashManagerImplTest, MakeSpaceOnSpecificDisk) { + fs::path filePath1 = testDir / "file1.txt"; + fs::path filePath2 = testDir / "file2.txt"; + std::ofstream(filePath1.string()); + std::ofstream(filePath2.string()); + + chunkTrashManagerImpl.moveToTrash(filePath1, testDir, std::time(nullptr)); + chunkTrashManagerImpl.moveToTrash(filePath2, testDir, std::time(nullptr)); + + chunkTrashManagerImpl.makeSpace(testDir.string(), 1, 1); // 1 GB threshold + ASSERT_FALSE(fs::exists(filePath1)); // Ensure the first file is removed + ASSERT_FALSE(fs::exists(filePath2)); // Ensure the second file is removed +} + +// Testing converting time string to time value +TEST_F(ChunkTrashManagerImplTest, GetTimeFromString) { + std::string timeString = "20231018120350"; + int errorCode = 0; + time_t timeValue = chunkTrashManagerImpl.getTimeFromString(timeString, + errorCode); + + std::tm tm = {}; + strptime(timeString.c_str(), "%Y%m%d%H%M%S", &tm); + time_t expectedTimeValue = std::mktime(&tm); + + ASSERT_EQ(timeValue, expectedTimeValue); // Compare time values +} + +// Mocking helper functions for filesystem operations +TEST_F(ChunkTrashManagerImplTest, InitHandlesInvalidAndValidFiles) { + // Arrange: Create mocks for filesystem entries + std::string validFile = "file_with_timestamp.20231015"; + std::string invalidFile = "file_without_timestamp"; + + fs::path trashDir = testDir / "trashDir"; + + fs::create_directories(trashDir); + std::ofstream(trashDir / validFile); + std::ofstream(trashDir / invalidFile); + + // Act: Call the init function to cover the lines + int status = chunkTrashManagerImpl.init(trashDir.string()); + + // Assert: Check function behavior and coverage + EXPECT_EQ(status, SAUNAFS_STATUS_OK); +} diff --git a/src/chunkserver/chunkserver-common/chunk_trash_manager_unittest.cc b/src/chunkserver/chunkserver-common/chunk_trash_manager_unittest.cc new file mode 100644 index 000000000..3f180e498 --- /dev/null +++ b/src/chunkserver/chunkserver-common/chunk_trash_manager_unittest.cc @@ -0,0 +1,93 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#include +#include +#include "chunk_trash_manager.h" + +class MockChunkTrashManagerImpl : public IChunkTrashManagerImpl { +public: + MOCK_METHOD(int, moveToTrash, + (const std::filesystem::path&, const std::filesystem::path&, const std::time_t&), + (override)); + MOCK_METHOD(int, init, (const std::string&), (override)); + MOCK_METHOD(void, collectGarbage, (), (override)); + MOCK_METHOD(void, reloadConfig, (), (override)); +}; + +class ChunkTrashManagerTest : public ::testing::Test { +public: + std::shared_ptr mockImpl = nullptr; + +protected: + + void SetUp() override { + mockImpl = std::make_shared(); + ChunkTrashManager::instance(mockImpl); + } + + void TearDown() override { + mockImpl.reset(); + } +}; + +TEST_F(ChunkTrashManagerTest, MoveToTrashForwardsCall) { + std::filesystem::path filePath = "example.txt"; + std::filesystem::path diskPath = "/disk/"; + std::time_t deletionTime = 1234567890; + + EXPECT_CALL(*mockImpl, moveToTrash(filePath, diskPath, deletionTime)) + .Times(1) + .WillOnce(testing::Return(0)); // Assuming 0 is a success return + // value. + + int result = ChunkTrashManager::instance().moveToTrash(filePath, diskPath, + deletionTime); + EXPECT_EQ(result, 0); // Validate that the return value is as expected. +} + +TEST_F(ChunkTrashManagerTest, InitForwardsCall) { + std::string const diskPath = "/disk/"; + + EXPECT_CALL(*mockImpl, init(diskPath)).Times(1); + + ChunkTrashManager::instance().init(diskPath); // Call the method. +} + +TEST_F(ChunkTrashManagerTest, CollectGarbageForwardsCall) { + EXPECT_CALL(*mockImpl, collectGarbage()).Times(1); + + ChunkTrashManager::instance().collectGarbage(); // Call the method. +} + +TEST_F(ChunkTrashManagerTest, ReloadConfigForwardsCall) { + EXPECT_CALL(*mockImpl, reloadConfig()).Times(1); + + ChunkTrashManager::instance().reloadConfig(); // Call the method. +} + +TEST_F(ChunkTrashManagerTest, SingletonBehavior) { + // Get the first instance + ChunkTrashManager &firstInstance = ChunkTrashManager::instance(); + + // Get a second instance + ChunkTrashManager &secondInstance = ChunkTrashManager::instance(); + + // Verify that both instances point to the same address + EXPECT_EQ(&firstInstance, &secondInstance); +} diff --git a/src/chunkserver/chunkserver-common/cmr_disk.cc b/src/chunkserver/chunkserver-common/cmr_disk.cc index f81554413..82ee90f7c 100644 --- a/src/chunkserver/chunkserver-common/cmr_disk.cc +++ b/src/chunkserver/chunkserver-common/cmr_disk.cc @@ -9,9 +9,11 @@ #include "chunkserver-common/hdd_stats.h" #include "chunkserver-common/subfolder.h" #include "common/crc.h" -#include "errors/saunafs_error_codes.h" #include "devtools/TracePrinter.h" #include "devtools/request_log.h" +#include "errors/saunafs_error_codes.h" + +#include "chunk_trash_manager.h" CmrDisk::CmrDisk(const std::string &_metaPath, const std::string &_dataPath, bool _isMarkedForRemoval, bool _isZonedDevice) @@ -27,9 +29,14 @@ void CmrDisk::createPathsAndSubfolders() { if (!isMarkedForDeletion()) { ret &= (::mkdir(metaPath().c_str(), mode) == 0); + ret &= (::mkdir((std::filesystem::path(metaPath()) / + ChunkTrashManager::kTrashDirname).c_str(), mode) == 0); if (dataPath() != metaPath()) { ret &= (::mkdir(dataPath().c_str(), mode) == 0); + ret &= (::mkdir((std::filesystem::path(dataPath()) / + ChunkTrashManager::kTrashDirname).c_str(), mode) + == 0); } for (uint32_t i = 0; i < Subfolder::kNumberOfSubfolders; ++i) { @@ -186,17 +193,61 @@ void CmrDisk::open(IChunk *chunk) { } int CmrDisk::unlinkChunk(IChunk *chunk) { - int result = 0; - - if (::unlink(chunk->fullMetaFilename().c_str()) != 0) { - result = -1; + // Get absolute paths for meta and data files + const std::filesystem::path metaFile = chunk->fullMetaFilename(); + const std::filesystem::path dataFile = chunk->fullDataFilename(); + + // Use the metaPath() and dataPath() to get the disk paths + const std::string metaDiskPath = metaPath(); + const std::string dataDiskPath = dataPath(); + + // Ensure we found a valid disk path + if (metaDiskPath.empty() || dataDiskPath.empty()) { + safs_pretty_errlog(LOG_ERR, "Error finding disk path for chunk: %s", + chunk->metaFilename().c_str()); + return SAUNAFS_ERROR_ENOENT; } - if (::unlink(chunk->fullDataFilename().c_str()) != 0) { - result = -1; + if (ChunkTrashManager::isEnabled) { + // Create a deletion timestamp + const std::time_t deletionTime = std::time(nullptr); + + // Move meta file to trash + int result = ChunkTrashManager::instance().moveToTrash(metaFile, + metaDiskPath, + deletionTime); + if (result != SAUNAFS_STATUS_OK) { + safs_pretty_errlog(LOG_ERR, "Error moving meta file to trash: %s, error: %d", + metaFile.c_str(), result); + return result; + } + + // Move data file to trash + result = ChunkTrashManager::instance().moveToTrash(dataFile, + dataDiskPath, + deletionTime); + if (result != SAUNAFS_STATUS_OK) { + safs_pretty_errlog(LOG_ERR, "Error moving data file to trash: %s, error: %d", + dataFile.c_str(), result); + return result; + } + } else { + // Unlink the meta file + if (::unlink(metaFile.c_str()) != 0) { + safs_pretty_errlog(LOG_ERR, "Error unlinking meta file: %s", + metaFile.c_str()); + return SAUNAFS_ERROR_UNKNOWN; + } + + // Unlink the data file + if (::unlink(dataFile.c_str()) != 0) { + safs_pretty_errlog(LOG_ERR, "Error unlinking data file: %s", + dataFile.c_str()); + return SAUNAFS_ERROR_UNKNOWN; + } } - return result; + return SAUNAFS_STATUS_OK; } int CmrDisk::ftruncateData(IChunk *chunk, uint64_t size) { diff --git a/src/chunkserver/chunkserver-common/cmr_disk.h b/src/chunkserver/chunkserver-common/cmr_disk.h index fc09cdc9b..990b457ae 100644 --- a/src/chunkserver/chunkserver-common/cmr_disk.h +++ b/src/chunkserver/chunkserver-common/cmr_disk.h @@ -23,7 +23,7 @@ class CmrDisk : public FDDisk { CmrDisk &operator=(CmrDisk &&) = delete; /// Virtual destructor needed for correct polymorphism - ~CmrDisk() = default; + ~CmrDisk() override = default; /// Tries to create the paths and subfolders for metaPath and dataPath /// @@ -33,8 +33,8 @@ class CmrDisk : public FDDisk { /// Creates the lock files for metadata and data directories void createLockFiles( - bool isLockNeeded, - std::vector> &allDisks) override; + bool isLockNeeded, + std::vector> &allDisks) override; /// Updates the disk usage information preserving the reserved space /// @@ -49,7 +49,7 @@ class CmrDisk : public FDDisk { /// Creates a new ChunkSignature for the given Chunk. /// Used mostly to write the metadata file of an existing in-memory Chunk. std::unique_ptr createChunkSignature( - IChunk *chunk) override; + IChunk *chunk) override; /// Creates a new empty ChunkSignature that later will be filled with the /// information of the Chunk using readFromDescriptor. diff --git a/src/chunkserver/chunkserver-common/cmr_disk_unittest.cc b/src/chunkserver/chunkserver-common/cmr_disk_unittest.cc new file mode 100644 index 000000000..f939fe4a4 --- /dev/null +++ b/src/chunkserver/chunkserver-common/cmr_disk_unittest.cc @@ -0,0 +1,206 @@ +/* + Copyright 2023-2024 Leil Storage OÜ + + This file is part of SaunaFS. + + SaunaFS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + SaunaFS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SaunaFS. If not, see . + */ + +#include "common/platform.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +#include + +#pragma GCC diagnostic pop + +#include + +#include "chunk_trash_manager.h" +#include "cmr_disk.h" +#include "errors/saunafs_error_codes.h" +#include "chunk_with_fd.h" + +extern "C" std::time_t __wrap_time(std::time_t *time1); + +std::time_t mockTimeValue = 0; + +std::time_t __wrap_time(std::time_t *time1) { + if (time1 != nullptr) { + *time1 = mockTimeValue; + } + return mockTimeValue; +} + +class MockChunk : public FDChunk { +public: + MockChunk() : FDChunk(0, ChunkPartType(detail::SliceType::kECFirst), + ChunkState::Available) {} + + MockChunk(const std::filesystem::path &metaFile, const + std::filesystem::path &dataFile) : FDChunk(0, + ChunkPartType( + detail::SliceType::kECFirst), + ChunkState::Available), + metaFile_(metaFile.string()), + dataFile_(dataFile.string()) {} + + std::string fullMetaFilename() const override { return metaFile_; } + + std::string fullDataFilename() const override { return dataFile_; } + + // Implement all the pure virtual methods with simple stubs + std::string generateDataFilenameForVersion( + uint32_t /*_version*/) const override { return ""; } + + int renameChunkFile(uint32_t /*new_version*/) override { return 0; } + + uint8_t *getChunkHeaderBuffer() const override { return nullptr; } + + size_t getHeaderSize() const override { return 0; } + + off_t getCrcOffset() const override { return 0; } + + void shrinkToBlocks(uint16_t /*newBlocks*/) override {} + + bool isDirty() override { return false; } + + std::string toString() const override { return "MockChunk"; } + +private: + std::string metaFile_; + std::string dataFile_; +}; + +class CmrDiskTest : public ::testing::Test { +protected: + CmrDiskTest() + : cmrDiskInstance("", "", false, false) {} + + static std::filesystem::path testDir; + CmrDisk cmrDiskInstance; + + void SetUp() override { + testDir = std::filesystem::temp_directory_path() / "cmr_disk_test"; + std::filesystem::create_directories(testDir); + + // Create dummy disk paths + cmrDiskInstance.setMetaPath(testDir / "meta"); + cmrDiskInstance.setDataPath(testDir / "data"); + std::filesystem::create_directories(cmrDiskInstance.metaPath()); + std::filesystem::create_directories(cmrDiskInstance.dataPath()); + } + + void TearDown() override { + if (std::filesystem::remove_all(testDir) == 0) { + std::cerr << "Failed to remove test directory: " << testDir << '\n'; + } + } +}; + +std::filesystem::path CmrDiskTest::testDir; +TEST_F(CmrDiskTest, UnlinkChunkSuccessful +) { +// Create a dummy meta and data file in the test directory + std::filesystem::path const metaFile = + std::filesystem::path(cmrDiskInstance.metaPath()) / + "chunk_meta_file.txt"; + std::filesystem::path const dataFile = + std::filesystem::path(cmrDiskInstance.dataPath()) / + "chunk_data_file.txt"; + std::ofstream(metaFile) + << "meta content"; + std::ofstream(dataFile) + << "data content"; + + MockChunk chunk(metaFile, dataFile); + + mockTimeValue = 1729259531; + std::string const mockDeletionTimeString = "20241018135211"; + +// Call the unlinkChunk method + int const result = cmrDiskInstance.unlinkChunk(&chunk); + + std::filesystem::path const expectedMetaTrashPath = + std::filesystem::path(cmrDiskInstance.metaPath()) / + ChunkTrashManager::kTrashDirname / + (metaFile.filename().string() + "." + mockDeletionTimeString); + std::filesystem::path const expectedDataTrashPath = + std::filesystem::path(cmrDiskInstance.dataPath()) / + ChunkTrashManager::kTrashDirname / + (dataFile.filename().string() + "." + mockDeletionTimeString); + +// Verify that the meta and data files were moved to the trash + EXPECT_TRUE(std::filesystem::exists(expectedMetaTrashPath) + ); + EXPECT_TRUE(std::filesystem::exists(expectedDataTrashPath) + ); + EXPECT_EQ(result, SAUNAFS_STATUS_OK + ); +} + +TEST_F(CmrDiskTest, UnlinkChunkMetaFileMissing +) { +// Create only the data file + std::filesystem::path const dataFile = + std::filesystem::path(cmrDiskInstance.dataPath()) / + "chunk_data_file.txt"; + std::ofstream(dataFile) + << "data content"; + + MockChunk chunk("non_existent_meta_file.txt", dataFile); + +// Call the unlinkChunk method + int const result = cmrDiskInstance.unlinkChunk(&chunk); + +// Check that the correct error code is returned + EXPECT_EQ(result, SAUNAFS_ERROR_ENOENT + ); +} + +TEST_F(CmrDiskTest, UnlinkChunkDataFileMissing +) { +// Create only the meta file + std::filesystem::path const metaFile = + std::filesystem::path(cmrDiskInstance.metaPath()) / + "chunk_meta_file.txt"; + std::ofstream(metaFile) + << "meta content"; + + MockChunk chunk(metaFile, "non_existent_data_file.txt"); + +// Call the unlinkChunk method + int const result = cmrDiskInstance.unlinkChunk(&chunk); + +// Check that the correct error code is returned + EXPECT_EQ(result, SAUNAFS_ERROR_ENOENT + ); +} + +TEST_F(CmrDiskTest, UnlinkChunkDiskPathError +) { +// Set disk paths to empty strings to simulate a disk path error + cmrDiskInstance.setMetaPath(""); + cmrDiskInstance.setDataPath(""); + + MockChunk chunk("chunk_meta_file.txt", "chunk_data_file.txt"); + +// Call the unlinkChunk method + int const result = cmrDiskInstance.unlinkChunk(&chunk); + +// Check that the correct error code is returned + EXPECT_EQ(result, SAUNAFS_ERROR_ENOENT + ); +} diff --git a/src/chunkserver/hddspacemgr.cc b/src/chunkserver/hddspacemgr.cc index 36d11ed99..a2a8b9d4f 100644 --- a/src/chunkserver/hddspacemgr.cc +++ b/src/chunkserver/hddspacemgr.cc @@ -66,6 +66,7 @@ #include #include "chunkserver-common/chunk_interface.h" +#include "chunkserver-common/chunk_trash_manager.h" #include "chunkserver-common/chunk_with_fd.h" #include "chunkserver-common/cmr_disk.h" #include "chunkserver-common/default_disk_manager.h" @@ -84,17 +85,17 @@ #include "common/disk_info.h" #include "common/event_loop.h" #include "common/exceptions.h" -#include "common/massert.h" #include "common/legacy_vector.h" -#include "errors/saunafs_error_codes.h" +#include "common/massert.h" #include "common/serialization.h" #include "common/slice_traits.h" -#include "slogger/slogger.h" #include "common/time_utils.h" #include "common/unique_queue.h" #include "devtools/TracePrinter.h" #include "devtools/request_log.h" +#include "errors/saunafs_error_codes.h" #include "protocol/SFSCommunication.h" +#include "slogger/slogger.h" constexpr int kErrorLimit = 2; constexpr int kLastErrorTime = 60; @@ -2399,6 +2400,7 @@ void hddFreeResourcesThread() { while (!gTerminate) { gOpenChunks.freeUnused(eventloop_time(), gChunksMapMutex, kMaxFreeUnused); + ChunkTrashManager::instance().collectGarbage(); sleep(kDelayedStep); } } @@ -2648,9 +2650,16 @@ int hddInit() { { std::lock_guard disksLockGuard(gDisksMutex); - for (const auto& disk : gDisks) { + for (const auto &disk: gDisks) { safs_pretty_syslog(LOG_INFO, "hdd space manager: disk to scan: %s", disk->getPaths().c_str()); + ChunkTrashManager::instance().init(disk->metaPath()); + if (disk->isZonedDevice()) { + continue; + } + if (disk->metaPath() != disk->dataPath()) { + ChunkTrashManager::instance().init(disk->dataPath()); + } } } diff --git a/src/chunkserver/init.h b/src/chunkserver/init.h index 322d2e8f5..f8d568b1d 100644 --- a/src/chunkserver/init.h +++ b/src/chunkserver/init.h @@ -36,7 +36,8 @@ inline const std::vector earlyRunTabs = {}; inline const std::vector runTabs = { RunTab{rnd_init, "random generator"}, RunTab{initDiskManager, "disk manager"}, // Always before "plugin manager" - RunTab{loadPlugins, "plugin manager"}, RunTab{hddInit, "hdd space manager"}, + RunTab{loadPlugins, "plugin manager"}, + RunTab{hddInit, "hdd space manager"}, // Has to be before "masterconn" RunTab{mainNetworkThreadInit, "main server module"}, RunTab{masterconn_init, "master connection module"}, diff --git a/src/data/sfschunkserver.cfg.in b/src/data/sfschunkserver.cfg.in index ab136538e..10cb86fd7 100644 --- a/src/data/sfschunkserver.cfg.in +++ b/src/data/sfschunkserver.cfg.in @@ -184,6 +184,29 @@ ## of read requests is sent to other chunkservers. # REPLICATION_WAVE_TIMEOUT_MS = 500 +## Enables or disables the chunk trash feature. When enabled, deleted chunks are +## moved to a trash directory instead of being immediately removed. (Default: 1) +# CHUNK_TRASH_ENABLED = 1 + +## Specifies the timeout in seconds for chunks to remain in the trash before +## being permanently deleted. (Default: 259200) +# CHUNK_TRASH_EXPIRATION_SECONDS = 259200 + +## Sets the available space threshold in gigabytes. If the available space on +## the disk falls below this threshold, the system will start deleting older +## chunks from the trash to free up space. (Default: 1024) +# CHUNK_TRASH_FREE_SPACE_THRESHOLD_GB = 1024 + +## Defines the bulk size for the garbage collector when processing chunks in the +## trash. This determines how many files are processed in each garbage +## collection cycle. (Default: 1000) +# CHUNK_TRASH_GC_BATCH_SIZE = 1000 + +## [ADVANCED] The number of files to remove from the trash in a single GC cycle, +## in case the disk is full ans space needs to be recovered. (Default: 100) +## (Default: 100) +# CHUNK_TRASH_GC_SPACE_RECOVERY_BATCH_SIZE = 100 + ## Setup logging. Uses the environment variable SAUNAFS_LOG_LEVEL or config ## value LOG_LEVEL to determine logging level. ## Valid log levels are diff --git a/tests/test_suites/ShortSystemTests/test_unlink.sh b/tests/test_suites/ShortSystemTests/test_unlink.sh index aa4a1f60c..07f885461 100644 --- a/tests/test_suites/ShortSystemTests/test_unlink.sh +++ b/tests/test_suites/ShortSystemTests/test_unlink.sh @@ -5,6 +5,7 @@ CHUNKSERVERS=4 \ MASTER_EXTRA_CONFIG="CHUNKS_LOOP_MIN_TIME = 1` `|CHUNKS_LOOP_MAX_CPU = 90` `|OPERATIONS_DELAY_INIT = 0" \ + CHUNKSERVER_EXTRA_CONFIG="CHUNK_TRASH_ENABLED=0" \ USE_RAMDISK="YES" \ setup_local_empty_saunafs info @@ -17,6 +18,7 @@ saunafs setgoal xor3 "$xorfile" dd if=/dev/zero of="$file" bs=1MiB count=130 dd if=/dev/zero of="$xorfile" bs=1MiB count=130 saunafs settrashtime 0 "$file" "$xorfile" +chunks_count_before_files_removal="$(find_all_chunks | wc -l)" rm -f "$file" "$xorfile" # Wait for removing all the chunks diff --git a/tests/test_suites/ShortSystemTests/test_unlink_trash.sh b/tests/test_suites/ShortSystemTests/test_unlink_trash.sh new file mode 100644 index 000000000..d375e24fd --- /dev/null +++ b/tests/test_suites/ShortSystemTests/test_unlink_trash.sh @@ -0,0 +1,71 @@ +chunks_replicated_count() { + saunafs-admin chunks-health \ + --porcelain \ + --replication \ + localhost "${info[matocl]}" | awk '$3!=0{sum+=$3}END{print sum}' +} + +timeout_set 4 minutes +CHUNKSERVERS=4 \ + MOUNT_EXTRA_CONFIG="sfscachemode=NEVER" \ + MASTER_EXTRA_CONFIG="CHUNKS_LOOP_MIN_TIME = 1` + `|CHUNKS_LOOP_MAX_CPU = 90` + `|OPERATIONS_DELAY_INIT = 1" \ + CHUNKSERVER_EXTRA_CONFIG="CHUNK_TRASH_ENABLED=1` + `|CHUNK_TRASH_EXPIRATION_SECONDS=55" \ + USE_RAMDISK="YES" \ + setup_local_empty_saunafs info + +# Create a file consising of a couple of chunks and remove it +file="${info[mount0]}/file" +xorfile="${info[mount0]}/xorfile" +touch "${file}" "${xorfile}" +saunafs setgoal 3 "${file}" +saunafs setgoal xor3 "${xorfile}" +test_file_size_mb=130 +dd if=/dev/zero of="${file}" bs=1MiB count=130 +dd if=/dev/zero of="${xorfile}" bs=1MiB count=130 +saunafs settrashtime 0 "${file}" "${xorfile}" + +files_count=2 +chunk_size_mb=64 +chunks_count=$(( (test_file_size_mb / chunk_size_mb + \ + (test_file_size_mb % chunk_size_mb ? 1 : 0) ) * files_count )) + +waiting_timeout="3 minutes" + +# Wait for the chunks to be replicated +if ! wait_for '[ "$(chunks_replicated_count)" -eq "${chunks_count}" ]' \ + "${waiting_timeout}"; +then + test_add_failure $'The chunks replication timed out' +fi + +chunks_count_before_files_removal="$(find_all_chunks | wc -l)" +echo "Chunks count before files removal: ${chunks_count_before_files_removal}" + +rm -f "${file}" "${xorfile}" + +# Wait for removing all the chunks +if ! wait_for '[[ $(find_all_chunks | wc -l) == 0 ]]' "${waiting_timeout}"; then + test_add_failure $'The following chunks were not removed:\n'"$(find_all_chunks)" +fi + +# Ensure the "unlinked" files are trashed +trashed_chunks_count=$(find_all_trashed_chunks | wc -l) +echo "Trashed chunks count: ${trashed_chunks_count}" +if [ "${trashed_chunks_count}" -eq 0 ]; then + test_add_failure $'The removed chunks were not moved to the trash folder' +fi + +if [ "${trashed_chunks_count}" -ne "${chunks_count_before_files_removal}" ]; then + test_add_failure $'The removed chunks do not match the chunks number in the trash folder' +fi + +sleep 60 + +# Ensure the trashed chunks are removed after the trashing time +trashed_chunks_count=$(find_all_trashed_chunks | wc -l) +if [ "${trashed_chunks_count}" -ne 0 ]; then + test_add_failure $'The trashed chunks were not removed after the trashing time' +fi diff --git a/tests/tools/saunafs.sh b/tests/tools/saunafs.sh index f63aa9211..57ccc1e07 100644 --- a/tests/tools/saunafs.sh +++ b/tests/tools/saunafs.sh @@ -855,6 +855,39 @@ find_all_metadata_chunks() { done } + +# print absolute paths of all trashed chunk files on selected server, one per +# line +find_chunkserver_trashed_chunks() { + local chunkserver_number=$1 + local chunk_metadata_pattern="chunk*${chunk_metadata_extension}.*" + local chunk_data_pattern="chunk*${chunk_data_extension}.*" + shift + local trash_bins=$(sed \ + -e 's/*//' \ + -e 's/zonefs://' \ + -e 's/|//' \ + -e 's@\/?$@/.trash.bin/@' \ + ${saunafs_info_[chunkserver${chunkserver_number}_hdd]}) + if (($# > 0)); then + find ${trash_bins} "(" -name "${chunk_data_pattern}" \ + -o -name "${chunk_metadata_pattern}" ")" -a "(" "$@" ")" + else + find ${trash_bins} "(" -name "${chunk_data_pattern}" \ + -o -name "${chunk_metadata_pattern}" ")" + fi +} + +# print absolute paths of all trashed chunk files on all servers used in test, +# one per line +find_all_trashed_chunks() { + local count=${saunafs_info_[chunkserver_count]} + local chunkserver + for ((chunkserver = 0; chunkserver < count; ++chunkserver)); do + find_chunkserver_trashed_chunks ${chunkserver} "$@" + done +} + # A useful shortcut for saunafs-admin # Usage: saunafs_admin_master_no_password [option...] # Calls saunafs-admin with the given command and and automatically adds address