Commit c36e3cac authored by Ramon Nou's avatar Ramon Nou
Browse files

Complete shared repair journal ownership

parent 9ed87b56
Loading
Loading
Loading
Loading
Loading
+5 −0
Changes for README.md: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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).
+6 −0
Changes for docs/sphinx/users/running.md: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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
+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
@@ -21,6 +21,7 @@
#include <cstring>
#include <fstream>
#include <iomanip>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
@@ -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.
 *
@@ -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) {
+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
@@ -14,6 +14,7 @@
#include <filesystem>
#include <fstream>
#include <sstream>
#include <sys/wait.h>
#include <unistd.h>

namespace {
@@ -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);
}