Commit 6532a9a0 authored by Ramon Nou's avatar Ramon Nou
Browse files

Add shared repair journal ownership lock

parent fe32e11e
Loading
Loading
Loading
Loading
Loading
+2 −0
Changes for include/client/preload_context.hpp: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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>
@@ -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;

+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
+1 −0
Changes for src/client/preload_context.cpp: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -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);
+1 −0
Changes for tests/unit/CMakeLists.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -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)
+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());
}