Loading include/client/preload_context.hpp +2 −0 Changes for include/client/preload_context.hpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ #include <unordered_set> #include <config.hpp> #include <client/rpc/repair_queue.hpp> #include <client/rpc/repair_journal_lock.hpp> #include <client/rpc/replica.hpp> #include <bitset> Loading Loading @@ -203,6 +204,7 @@ private: std::vector<gkfs::rpc::repair_task> in_flight_repairs_; std::string repair_status_path_; std::string repair_journal_path_; gkfs::rpc::repair_journal_lock repair_journal_lock_; static constexpr uint32_t max_repairs_per_host_ = 1; static constexpr uint32_t repair_worker_count_ = 2; Loading include/client/rpc/repair_journal_lock.hpp 0 → 100644 +100 −0 Changes for include/client/rpc/repair_journal_lock.hpp: 100 added lines, 0 removed lines. Original line number Diff line number Diff line /* Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany This file is part of GekkoFS. SPDX-License-Identifier: LGPL-3.0-or-later */ #ifndef GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP #define GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP #include <common/unique_fd.hpp> #include <fcntl.h> #include <sys/file.h> #include <unistd.h> #include <cerrno> #include <cstring> #include <stdexcept> #include <string> namespace gkfs::rpc { /** * @brief Owns the advisory process lock for a client-local repair journal. * * The lock is stored in a sidecar file so the journal format remains * unchanged. It is held until the owning client context is destroyed. */ class repair_journal_lock { public: repair_journal_lock() = default; repair_journal_lock(const repair_journal_lock&) = delete; repair_journal_lock& operator=(const repair_journal_lock&) = delete; ~repair_journal_lock() { release(); } void acquire(const std::string& journal_path) { if(journal_path.empty() || fd_) { return; } lock_path_ = journal_path + ".lock"; const auto fd = ::open(lock_path_.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600); if(fd < 0) { throw std::runtime_error( "Unable to open replica repair journal lock '" + lock_path_ + "': " + ::strerror(errno)); } gkfs::utils::unique_fd candidate(fd); if(::flock(candidate.get(), LOCK_EX | LOCK_NB) != 0) { const auto error = errno; if(error == EWOULDBLOCK || error == EAGAIN) { throw std::runtime_error( "Replica repair journal is already owned: " + journal_path); } throw std::runtime_error("Unable to lock replica repair journal '" + journal_path + "': " + ::strerror(error)); } fd_ = std::move(candidate); } void release() noexcept { if(fd_) { ::flock(fd_.get(), LOCK_UN); fd_.reset(); } lock_path_.clear(); } [[nodiscard]] bool owns_lock() const noexcept { return static_cast<bool>(fd_); } [[nodiscard]] const std::string& path() const noexcept { return lock_path_; } private: gkfs::utils::unique_fd fd_; std::string lock_path_; }; } // namespace gkfs::rpc #endif // GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP src/client/preload_context.cpp +1 −0 Changes for src/client/preload_context.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -106,6 +106,7 @@ PreloadContext::PreloadContext() PreloadContext::set_replicas(replicas); repair_status_path_ = gkfs::env::get_var(gkfs::env::REPAIR_STATUS_PATH); repair_journal_path_ = gkfs::env::get_var(gkfs::env::REPAIR_JOURNAL_PATH); repair_journal_lock_.acquire(repair_journal_path_); const std::string env_dirents_buff_size = gkfs::env::get_var(gkfs::env::DIRENTS_BUFF_SIZE); Loading tests/unit/CMakeLists.txt +1 −0 Changes for tests/unit/CMakeLists.txt: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ target_sources(unit_tests ${CMAKE_CURRENT_LIST_DIR}/test_env_util.cpp ${CMAKE_CURRENT_LIST_DIR}/test_stats.cpp ${CMAKE_CURRENT_LIST_DIR}/test_unique_fd.cpp ${CMAKE_CURRENT_LIST_DIR}/test_repair_journal_lock.cpp ${CMAKE_CURRENT_LIST_DIR}/test_fs_data_faults.cpp ${CMAKE_SOURCE_DIR}/src/common/hostfile_management.cpp ${CMAKE_SOURCE_DIR}/src/daemon/classes/fs_data.cpp) Loading tests/unit/test_repair_journal_lock.cpp 0 → 100644 +58 −0 Changes for tests/unit/test_repair_journal_lock.cpp: 58 added lines, 0 removed lines. Original line number Diff line number Diff line /* Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany This file is part of GekkoFS. SPDX-License-Identifier: GPL-3.0-or-later */ #include <client/rpc/repair_journal_lock.hpp> #include <catch2/catch_all.hpp> #include <filesystem> #include <unistd.h> namespace { std::filesystem::path test_journal_path() { return std::filesystem::temp_directory_path() / ("gkfs-repair-journal-" + std::to_string(::getpid())); } } // namespace TEST_CASE("repair journal lock has exclusive process ownership", "[replication][repair][journal]") { const auto journal = test_journal_path(); const auto lock_path = journal.string() + ".lock"; std::filesystem::remove(lock_path); { gkfs::rpc::repair_journal_lock first; first.acquire(journal.string()); REQUIRE(first.owns_lock()); REQUIRE(first.path() == lock_path); gkfs::rpc::repair_journal_lock second; REQUIRE_THROWS_WITH(second.acquire(journal.string()), "Replica repair journal is already owned: " + journal.string()); REQUIRE_FALSE(second.owns_lock()); } gkfs::rpc::repair_journal_lock after_release; REQUIRE_NOTHROW(after_release.acquire(journal.string())); REQUIRE(after_release.owns_lock()); after_release.release(); std::filesystem::remove(lock_path); } TEST_CASE("empty repair journal path does not create a lock", "[replication][repair][journal]") { gkfs::rpc::repair_journal_lock lock; REQUIRE_NOTHROW(lock.acquire("")); REQUIRE_FALSE(lock.owns_lock()); } Loading
include/client/preload_context.hpp +2 −0 Changes for include/client/preload_context.hpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ #include <unordered_set> #include <config.hpp> #include <client/rpc/repair_queue.hpp> #include <client/rpc/repair_journal_lock.hpp> #include <client/rpc/replica.hpp> #include <bitset> Loading Loading @@ -203,6 +204,7 @@ private: std::vector<gkfs::rpc::repair_task> in_flight_repairs_; std::string repair_status_path_; std::string repair_journal_path_; gkfs::rpc::repair_journal_lock repair_journal_lock_; static constexpr uint32_t max_repairs_per_host_ = 1; static constexpr uint32_t repair_worker_count_ = 2; Loading
include/client/rpc/repair_journal_lock.hpp 0 → 100644 +100 −0 Changes for include/client/rpc/repair_journal_lock.hpp: 100 added lines, 0 removed lines. Original line number Diff line number Diff line /* Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany This file is part of GekkoFS. SPDX-License-Identifier: LGPL-3.0-or-later */ #ifndef GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP #define GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP #include <common/unique_fd.hpp> #include <fcntl.h> #include <sys/file.h> #include <unistd.h> #include <cerrno> #include <cstring> #include <stdexcept> #include <string> namespace gkfs::rpc { /** * @brief Owns the advisory process lock for a client-local repair journal. * * The lock is stored in a sidecar file so the journal format remains * unchanged. It is held until the owning client context is destroyed. */ class repair_journal_lock { public: repair_journal_lock() = default; repair_journal_lock(const repair_journal_lock&) = delete; repair_journal_lock& operator=(const repair_journal_lock&) = delete; ~repair_journal_lock() { release(); } void acquire(const std::string& journal_path) { if(journal_path.empty() || fd_) { return; } lock_path_ = journal_path + ".lock"; const auto fd = ::open(lock_path_.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600); if(fd < 0) { throw std::runtime_error( "Unable to open replica repair journal lock '" + lock_path_ + "': " + ::strerror(errno)); } gkfs::utils::unique_fd candidate(fd); if(::flock(candidate.get(), LOCK_EX | LOCK_NB) != 0) { const auto error = errno; if(error == EWOULDBLOCK || error == EAGAIN) { throw std::runtime_error( "Replica repair journal is already owned: " + journal_path); } throw std::runtime_error("Unable to lock replica repair journal '" + journal_path + "': " + ::strerror(error)); } fd_ = std::move(candidate); } void release() noexcept { if(fd_) { ::flock(fd_.get(), LOCK_UN); fd_.reset(); } lock_path_.clear(); } [[nodiscard]] bool owns_lock() const noexcept { return static_cast<bool>(fd_); } [[nodiscard]] const std::string& path() const noexcept { return lock_path_; } private: gkfs::utils::unique_fd fd_; std::string lock_path_; }; } // namespace gkfs::rpc #endif // GKFS_CLIENT_RPC_REPAIR_JOURNAL_LOCK_HPP
src/client/preload_context.cpp +1 −0 Changes for src/client/preload_context.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -106,6 +106,7 @@ PreloadContext::PreloadContext() PreloadContext::set_replicas(replicas); repair_status_path_ = gkfs::env::get_var(gkfs::env::REPAIR_STATUS_PATH); repair_journal_path_ = gkfs::env::get_var(gkfs::env::REPAIR_JOURNAL_PATH); repair_journal_lock_.acquire(repair_journal_path_); const std::string env_dirents_buff_size = gkfs::env::get_var(gkfs::env::DIRENTS_BUFF_SIZE); Loading
tests/unit/CMakeLists.txt +1 −0 Changes for tests/unit/CMakeLists.txt: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ target_sources(unit_tests ${CMAKE_CURRENT_LIST_DIR}/test_env_util.cpp ${CMAKE_CURRENT_LIST_DIR}/test_stats.cpp ${CMAKE_CURRENT_LIST_DIR}/test_unique_fd.cpp ${CMAKE_CURRENT_LIST_DIR}/test_repair_journal_lock.cpp ${CMAKE_CURRENT_LIST_DIR}/test_fs_data_faults.cpp ${CMAKE_SOURCE_DIR}/src/common/hostfile_management.cpp ${CMAKE_SOURCE_DIR}/src/daemon/classes/fs_data.cpp) Loading
tests/unit/test_repair_journal_lock.cpp 0 → 100644 +58 −0 Changes for tests/unit/test_repair_journal_lock.cpp: 58 added lines, 0 removed lines. Original line number Diff line number Diff line /* Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany This file is part of GekkoFS. SPDX-License-Identifier: GPL-3.0-or-later */ #include <client/rpc/repair_journal_lock.hpp> #include <catch2/catch_all.hpp> #include <filesystem> #include <unistd.h> namespace { std::filesystem::path test_journal_path() { return std::filesystem::temp_directory_path() / ("gkfs-repair-journal-" + std::to_string(::getpid())); } } // namespace TEST_CASE("repair journal lock has exclusive process ownership", "[replication][repair][journal]") { const auto journal = test_journal_path(); const auto lock_path = journal.string() + ".lock"; std::filesystem::remove(lock_path); { gkfs::rpc::repair_journal_lock first; first.acquire(journal.string()); REQUIRE(first.owns_lock()); REQUIRE(first.path() == lock_path); gkfs::rpc::repair_journal_lock second; REQUIRE_THROWS_WITH(second.acquire(journal.string()), "Replica repair journal is already owned: " + journal.string()); REQUIRE_FALSE(second.owns_lock()); } gkfs::rpc::repair_journal_lock after_release; REQUIRE_NOTHROW(after_release.acquire(journal.string())); REQUIRE(after_release.owns_lock()); after_release.release(); std::filesystem::remove(lock_path); } TEST_CASE("empty repair journal path does not create a lock", "[replication][repair][journal]") { gkfs::rpc::repair_journal_lock lock; REQUIRE_NOTHROW(lock.acquire("")); REQUIRE_FALSE(lock.owns_lock()); }