Loading README.md +5 −0 Changes for README.md: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1046,6 +1046,11 @@ Client-metrics require the CMake argument `-DGKFS_ENABLE_CLIENT_METRICS=ON` (see environment. - `LIBGKFS_PROXY_PID_FILE` - Path to the proxy pid file (when using the GekkoFS proxy). - `LIBGKFS_NUM_REPL` - Number of replicas for data. - `LIBGKFS_REPAIR_STATUS_PATH` - Optional path for client-local repair counters and queue status. - `LIBGKFS_REPAIR_JOURNAL_PATH` - Optional path for client-local pending repair tasks. The journal is single-owner: do not share it between client processes. Its `.lock` sidecar records the owning host, PID, acquisition time, and a fingerprint of the configured hosts file; reuse it only with the same topology. A changed topology requires a new journal path or removal of the completed/stale sidecar. #### Optimization - `LIBGKFS_CREATE_WRITE_OPTIMIZATION` - Optimization for write operations (default: OFF). - `LIBGKFS_READ_INLINE_PREFETCH` - Prefetch inline data when opening files (default: OFF). Loading docs/sphinx/users/running.md +6 −0 Changes for docs/sphinx/users/running.md: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -367,3 +367,9 @@ The user can enable the data replication feature by setting the replication envi `LIBGKFS_NUM_REPL=<num repl>`. The number of replicas should go from `0` to the `number of servers - 1`. The replication environment variable can be set up for each client independently. For client-local repair recovery, set `LIBGKFS_REPAIR_JOURNAL_PATH` to a writable journal path. One client process owns a journal at a time. The adjacent `.lock` sidecar records the owner and a fingerprint of the configured hosts file; do not share a journal across client processes or reuse it after changing the topology. Use `LIBGKFS_REPAIR_STATUS_PATH` to publish a client-local repair queue snapshot. No newline at end of file include/client/rpc/repair_journal_lock.hpp +29 −0 Changes for include/client/rpc/repair_journal_lock.hpp: 29 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ #include <cstring> #include <fstream> #include <iomanip> #include <optional> #include <sstream> #include <stdexcept> #include <string> Loading Loading @@ -57,6 +58,23 @@ repair_journal_topology_identity(const std::string& hostfile_path) { return result.str(); } inline std::optional<std::string> read_repair_journal_topology_identity(const std::string& lock_path) { std::ifstream input(lock_path); if(!input.is_open()) { return std::nullopt; } std::string line; while(std::getline(input, line)) { constexpr auto prefix = "topology="; if(line.rfind(prefix, 0) == 0) { return line.substr(std::char_traits<char>::length(prefix)); } } return std::string{}; } /** * @brief Owns the advisory process lock for a client-local repair journal. * Loading Loading @@ -108,6 +126,17 @@ public: journal_path + "': " + ::strerror(error)); } if(const auto previous_topology = read_repair_journal_topology_identity(lock_path_); previous_topology && !previous_topology->empty() && topology_identity != "unknown" && *previous_topology != topology_identity) { throw std::runtime_error( "Replica repair journal topology mismatch: expected '" + topology_identity + "', found '" + *previous_topology + "'"); } repair_journal_owner owner; owner.hostname.resize(255); if(::gethostname(owner.hostname.data(), owner.hostname.size()) != 0) { Loading tests/unit/test_repair_journal_lock.cpp +61 −0 Changes for tests/unit/test_repair_journal_lock.cpp: 61 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ #include <filesystem> #include <fstream> #include <sstream> #include <sys/wait.h> #include <unistd.h> namespace { Loading Loading @@ -78,3 +79,63 @@ TEST_CASE("empty repair journal path does not create a lock", REQUIRE_FALSE(lock.owns_lock()); REQUIRE(lock.owner().hostname.empty()); } TEST_CASE("repair journal lock rejects a different topology", "[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(), "topology-a"); first.release(); gkfs::rpc::repair_journal_lock second; REQUIRE_THROWS_WITH( second.acquire(journal.string(), "topology-b"), "Replica repair journal topology mismatch: expected 'topology-b', found 'topology-a'"); std::filesystem::remove(lock_path); } TEST_CASE("repair journal lock ownership is exclusive across processes", "[replication][repair][journal]") { const auto journal = test_journal_path(); const auto lock_path = journal.string() + ".lock"; std::filesystem::remove(lock_path); int ready[2]{}; REQUIRE(::pipe(ready) == 0); const auto child = ::fork(); REQUIRE(child >= 0); if(child == 0) { ::close(ready[0]); gkfs::rpc::repair_journal_lock child_lock; try { child_lock.acquire(journal.string(), "topology-child"); const char signal = '1'; ::write(ready[1], &signal, sizeof(signal)); ::sleep(1); _exit(0); } catch(...) { _exit(1); } } ::close(ready[1]); char signal{}; REQUIRE(::read(ready[0], &signal, sizeof(signal)) == sizeof(signal)); ::close(ready[0]); gkfs::rpc::repair_journal_lock parent_lock; REQUIRE_THROWS_WITH( parent_lock.acquire(journal.string(), "topology-child"), "Replica repair journal is already owned: " + journal.string()); int status = 0; REQUIRE(::waitpid(child, &status, 0) == child); REQUIRE(WIFEXITED(status)); REQUIRE(WEXITSTATUS(status) == 0); REQUIRE_NOTHROW(parent_lock.acquire(journal.string(), "topology-child")); parent_lock.release(); std::filesystem::remove(lock_path); } Loading
README.md +5 −0 Changes for README.md: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1046,6 +1046,11 @@ Client-metrics require the CMake argument `-DGKFS_ENABLE_CLIENT_METRICS=ON` (see environment. - `LIBGKFS_PROXY_PID_FILE` - Path to the proxy pid file (when using the GekkoFS proxy). - `LIBGKFS_NUM_REPL` - Number of replicas for data. - `LIBGKFS_REPAIR_STATUS_PATH` - Optional path for client-local repair counters and queue status. - `LIBGKFS_REPAIR_JOURNAL_PATH` - Optional path for client-local pending repair tasks. The journal is single-owner: do not share it between client processes. Its `.lock` sidecar records the owning host, PID, acquisition time, and a fingerprint of the configured hosts file; reuse it only with the same topology. A changed topology requires a new journal path or removal of the completed/stale sidecar. #### Optimization - `LIBGKFS_CREATE_WRITE_OPTIMIZATION` - Optimization for write operations (default: OFF). - `LIBGKFS_READ_INLINE_PREFETCH` - Prefetch inline data when opening files (default: OFF). Loading
docs/sphinx/users/running.md +6 −0 Changes for docs/sphinx/users/running.md: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -367,3 +367,9 @@ The user can enable the data replication feature by setting the replication envi `LIBGKFS_NUM_REPL=<num repl>`. The number of replicas should go from `0` to the `number of servers - 1`. The replication environment variable can be set up for each client independently. For client-local repair recovery, set `LIBGKFS_REPAIR_JOURNAL_PATH` to a writable journal path. One client process owns a journal at a time. The adjacent `.lock` sidecar records the owner and a fingerprint of the configured hosts file; do not share a journal across client processes or reuse it after changing the topology. Use `LIBGKFS_REPAIR_STATUS_PATH` to publish a client-local repair queue snapshot. No newline at end of file
include/client/rpc/repair_journal_lock.hpp +29 −0 Changes for include/client/rpc/repair_journal_lock.hpp: 29 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ #include <cstring> #include <fstream> #include <iomanip> #include <optional> #include <sstream> #include <stdexcept> #include <string> Loading Loading @@ -57,6 +58,23 @@ repair_journal_topology_identity(const std::string& hostfile_path) { return result.str(); } inline std::optional<std::string> read_repair_journal_topology_identity(const std::string& lock_path) { std::ifstream input(lock_path); if(!input.is_open()) { return std::nullopt; } std::string line; while(std::getline(input, line)) { constexpr auto prefix = "topology="; if(line.rfind(prefix, 0) == 0) { return line.substr(std::char_traits<char>::length(prefix)); } } return std::string{}; } /** * @brief Owns the advisory process lock for a client-local repair journal. * Loading Loading @@ -108,6 +126,17 @@ public: journal_path + "': " + ::strerror(error)); } if(const auto previous_topology = read_repair_journal_topology_identity(lock_path_); previous_topology && !previous_topology->empty() && topology_identity != "unknown" && *previous_topology != topology_identity) { throw std::runtime_error( "Replica repair journal topology mismatch: expected '" + topology_identity + "', found '" + *previous_topology + "'"); } repair_journal_owner owner; owner.hostname.resize(255); if(::gethostname(owner.hostname.data(), owner.hostname.size()) != 0) { Loading
tests/unit/test_repair_journal_lock.cpp +61 −0 Changes for tests/unit/test_repair_journal_lock.cpp: 61 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ #include <filesystem> #include <fstream> #include <sstream> #include <sys/wait.h> #include <unistd.h> namespace { Loading Loading @@ -78,3 +79,63 @@ TEST_CASE("empty repair journal path does not create a lock", REQUIRE_FALSE(lock.owns_lock()); REQUIRE(lock.owner().hostname.empty()); } TEST_CASE("repair journal lock rejects a different topology", "[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(), "topology-a"); first.release(); gkfs::rpc::repair_journal_lock second; REQUIRE_THROWS_WITH( second.acquire(journal.string(), "topology-b"), "Replica repair journal topology mismatch: expected 'topology-b', found 'topology-a'"); std::filesystem::remove(lock_path); } TEST_CASE("repair journal lock ownership is exclusive across processes", "[replication][repair][journal]") { const auto journal = test_journal_path(); const auto lock_path = journal.string() + ".lock"; std::filesystem::remove(lock_path); int ready[2]{}; REQUIRE(::pipe(ready) == 0); const auto child = ::fork(); REQUIRE(child >= 0); if(child == 0) { ::close(ready[0]); gkfs::rpc::repair_journal_lock child_lock; try { child_lock.acquire(journal.string(), "topology-child"); const char signal = '1'; ::write(ready[1], &signal, sizeof(signal)); ::sleep(1); _exit(0); } catch(...) { _exit(1); } } ::close(ready[1]); char signal{}; REQUIRE(::read(ready[0], &signal, sizeof(signal)) == sizeof(signal)); ::close(ready[0]); gkfs::rpc::repair_journal_lock parent_lock; REQUIRE_THROWS_WITH( parent_lock.acquire(journal.string(), "topology-child"), "Replica repair journal is already owned: " + journal.string()); int status = 0; REQUIRE(::waitpid(child, &status, 0) == child); REQUIRE(WIFEXITED(status)); REQUIRE(WEXITSTATUS(status) == 0); REQUIRE_NOTHROW(parent_lock.acquire(journal.string(), "topology-child")); parent_lock.release(); std::filesystem::remove(lock_path); }