From e4e3ad3f209832974c75f2fb98ecd829c3cc257d Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sat, 18 Jul 2026 04:40:57 -0700 Subject: [PATCH 01/24] add raft for peer communications --- CMakeLists.txt | 6 ++-- conanfile.py | 6 ++-- include/craft/types.hpp | 3 +- include/craft/wire.hpp | 16 +++++++-- src/mem/peer_comm.cpp | 45 ++++++++++++++++++++++++ src/mem/peer_comm.hpp | 61 ++++++++++++++++++++++++++++++++ src/mem/raft_state_mgr.hpp | 72 ++++++++++++++++++++++++++++++++++++++ src/mem/replica.cpp | 42 +++++++++++++++++++++- src/mem/replica.hpp | 17 +++++++-- src/net/tcp_server.cpp | 47 ++++++++++++++++++++++--- src/net/tcp_server.hpp | 4 +++ src/wire.cpp | 32 +++++++++-------- test/test_tcp.cpp | 42 ++++++++++++++++++++++ test/test_wire.cpp | 4 ++- 14 files changed, 366 insertions(+), 31 deletions(-) create mode 100644 src/mem/peer_comm.cpp create mode 100644 src/mem/peer_comm.hpp create mode 100644 src/mem/raft_state_mgr.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 233de75..24b29ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ endif () find_package(GTest REQUIRED) find_package(sisl REQUIRED) find_package(liburing REQUIRED) +find_package(NuraftMesg REQUIRED) # ── craft_wire: the std-only wire codec LEAF (the future standalone dependency) ── add_library(craft_wire STATIC src/wire.cpp) @@ -62,10 +63,11 @@ add_library(craft_reference STATIC src/mem/cluster.cpp src/local.cpp # craft/local.hpp: the public "no-remote, in-process" backend builder over the mem model src/net/cluster_server.cpp - src/net/tcp_server.cpp) + src/net/tcp_server.cpp + src/mem/peer_comm.cpp) target_include_directories(craft_reference PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # the internal headers it implements -target_link_libraries(craft_reference PUBLIC craft_client) +target_link_libraries(craft_reference PUBLIC craft_client nuraft_mesg::proto) target_compile_features(craft_reference PUBLIC cxx_std_23) # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to diff --git a/conanfile.py b/conanfile.py index 82c49b9..05d60e2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -50,10 +50,12 @@ def build_requirements(self): def requirements(self): # craft_wire is a std-only leaf and needs nothing. craft_types / craft_client (added as they land) pull - # sisl (result / async::result / sg_list) and liburing (the io_uring transport); declared here so the - # package graph is right from the start. + # sisl (result / async::result / sg_list) and liburing (the io_uring transport); + # nuraft_mesg is used by the tcp_server for the tests only + # declared here so the package graph is right from the start. self.requires("sisl/[^14.8]@oss/dev", transitive_headers=True) self.requires("liburing/[^2.4]", transitive_headers=True) + self.requires("nuraft_mesg/[^5.0]@oss/dev", transitive_headers=True) def validate(self): if self.info.settings.compiler.cppstd: diff --git a/include/craft/types.hpp b/include/craft/types.hpp index 8eba519..356d567 100644 --- a/include/craft/types.hpp +++ b/include/craft/types.hpp @@ -128,7 +128,8 @@ ENUM(craft_error, uint16_t, NO_QUORUM, // could not reach a quorum of live replicas WRONG_TOKEN, // client_token is not the current owner NOT_ELIGIBLE, // replica cannot serve this read (Missing overlap / below login-dLSN L) - REPLICA_DOWN); // addressed replica is down (fault injection / unreachable) + REPLICA_DOWN, // addressed replica is down (fault injection / unreachable) + INTERNAL); // unexpected internal failure class craft_error_category : public std::error_category { public: diff --git a/include/craft/wire.hpp b/include/craft/wire.hpp index 88707d9..f0025dc 100644 --- a/include/craft/wire.hpp +++ b/include/craft/wire.hpp @@ -77,7 +77,9 @@ enum class op : uint8_t { // 1. keep the request=odd / response=even convention, and // 2. bump k_max_op below -- is_response() is a range check, so a peer op added without it is silently // misclassified as "not a response". That is the trap this constant exists to close. - k_max_op = 14, // highest allocated opcode; raise when the peer plane lands + create_volume = 15, // client-requested volume creation (leader-only) + create_volume_rsp = 16, + k_max_op = 16, // highest allocated opcode; raise when the peer plane lands }; // Response `status` byte; 1-6 mirror craft_error (craft_types.hpp). @@ -214,6 +216,16 @@ struct resolve_rsp { uint32_t reserved; }; +// CREATE_VOLUME: leader-only, requests a new volume with the given data-member set. +// volume_create_req body: member_count x { uint8_t id[16]; uint16_t addr_len; char addr[addr_len]; } +struct volume_create_req { + std::array< uint8_t, 16 > volume_id; + uint64_t capacity; + uint32_t lba_size; + uint32_t member_count; // how many members follow in the body +}; +// volume_create_rsp: status only (no operation header, no body). + #pragma pack(pop) static_assert(sizeof(msg_hdr) == 8); @@ -231,7 +243,7 @@ static_assert(sizeof(keepalive_rsp) == 16); static_assert(sizeof(logout_req) == 16); static_assert(sizeof(resolve_req) == 24); static_assert(sizeof(resolve_rsp) == 16); - +static_assert(sizeof(volume_create_req) == 32); // The fixed operation-header size for an op code (0 for a status-only response). nullopt = unknown op, which // is unframeable -- the caller resets the connection. std::optional< std::size_t > op_hdr_size(uint8_t op_code) noexcept; diff --git a/src/mem/peer_comm.cpp b/src/mem/peer_comm.cpp new file mode 100644 index 0000000..328ecc3 --- /dev/null +++ b/src/mem/peer_comm.cpp @@ -0,0 +1,45 @@ +#include +#include "peer_comm.hpp" +#include "raft_state_mgr.hpp" + +#include +#include +#include + +namespace craft { + +std::shared_ptr< peer_comm > peer_comm::instance() { + static std::shared_ptr< peer_comm > instance{new peer_comm()}; + return instance; +} + +consensus_handle peer_comm::get_consensus() { return consensus_; } + +void peer_comm::init_raft_server(boost::uuids::uuid const& server_uuid, uint16_t port) { + auto params = nuraft_mesg::manager::params{ + .server_uuid_ = server_uuid, + .mesg_port_ = port, + .default_group_type_ = default_group_type_, + }; + consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true); + auto raft_params = nuraft::raft_params{}; + consensus_->register_mgr_type(default_group_type_, raft_params); + LOGINFO("Initialized peer_comm for {} with raft consensus manager, port {}", params.server_uuid_, params.mesg_port_); +} + +std::string peer_comm::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { + auto it = peer_lookup_map_.find(peer_id); + if (it == peer_lookup_map_.end()) { + LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); + return {}; + } + return it->second; +} + +std::shared_ptr< nuraft_mesg::mesg_state_mgr > peer_comm::create_state_mgr(int32_t const srv_id, + nuraft_mesg::group_id_t const& group_id) { + LOGINFO("Creating raft state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); + return std::make_shared< raft_state_mgr >(srv_id, group_id, consensus_); +} + +} // namespace craft diff --git a/src/mem/peer_comm.hpp b/src/mem/peer_comm.hpp new file mode 100644 index 0000000..2460a34 --- /dev/null +++ b/src/mem/peer_comm.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace nuraft_mesg { +class manager; +} + +using consensus_handle = std::shared_ptr< nuraft_mesg::manager >; + +namespace craft { + +// Process-wide bridge for the peer-to-peer consensus engine used by the TCP server and replica-side code. +// The concrete nuraft_mesg::manager instance is installed once and then shared by anyone that needs to create +// groups, add members, or issue consensus operations. +class peer_comm : public nuraft_mesg::messaging_application, + public std::enable_shared_from_this< peer_comm > { +public: + inline static const std::string default_group_type_{"peer_comm_raft"}; + + virtual ~peer_comm() = default; + static std::shared_ptr< peer_comm > instance(); + consensus_handle get_consensus(); + void init_raft_server(boost::uuids::uuid const& server_uuid, uint16_t port); + + // messaging_application overrides + std::string lookup_peer(nuraft_mesg::peer_id_t const&) override; + std::shared_ptr< nuraft_mesg::mesg_state_mgr > create_state_mgr(int32_t const srv_id, + nuraft_mesg::group_id_t const& group_id) override; + +private: + peer_comm() = default; + consensus_handle consensus_; + std::map< nuraft_mesg::peer_id_t, std::string > peer_lookup_map_; +}; + +// helper methods + +inline static boost::uuids::uuid to_uuid(std::array< uint8_t, 16 > const& arr) { + boost::uuids::uuid u{}; + std::copy(arr.begin(), arr.end(), u.begin()); + return u; +} + +// make sync coro calls, taken from homestore +template < typename Task > +inline auto sync_get(Task&& task) { + auto result = stdexec::sync_wait(std::forward< Task >(task)).value(); + if constexpr (std::tuple_size_v< decltype(result) > == 0) { + return; + } else { + return std::get< 0 >(std::move(result)); + } +} + +} // namespace craft diff --git a/src/mem/raft_state_mgr.hpp b/src/mem/raft_state_mgr.hpp new file mode 100644 index 0000000..9ef5cbf --- /dev/null +++ b/src/mem/raft_state_mgr.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace craft { + +class raft_state_machine : public nuraft::state_machine { +public: + raft_state_machine() = default; + + nuraft::ptr< nuraft::buffer > commit(const ulong, nuraft::buffer&) override { return nullptr; } + bool apply_snapshot(nuraft::snapshot&) override { return true; } + nuraft::ptr< nuraft::snapshot > last_snapshot() override { return nullptr; } + ulong last_commit_index() override { return 0; } + void create_snapshot(nuraft::snapshot&, nuraft::async_result< bool >::handler_type&) override {} +}; + +class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { +public: + raft_state_mgr(int32_t server_id, nuraft_mesg::group_id_t group_id, std::weak_ptr< nuraft_mesg::manager > weak_manager) : server_id_{server_id}, group_id_{std::move(group_id)}, weak_manager_{std::move(weak_manager)} {} + + nuraft::ptr< nuraft::cluster_config > load_config() override { return nullptr; } + void save_config(const nuraft::cluster_config&) override {} + void save_state(const nuraft::srv_state&) override {} + nuraft::ptr< nuraft::srv_state > read_state() override { return nullptr; } + nuraft::ptr< nuraft::log_store > load_log_store() override { + return nullptr; + } + int32_t server_id() override { return server_id_; } + void system_exit(const int) override {} + + uint32_t get_logstore_id() const override { return 0; } + std::shared_ptr< nuraft::state_machine > get_state_machine() override { + return std::make_shared< raft_state_machine >(); + } + void permanent_destroy() override {} + void leave() override {} + + bool bind_non_raft_service() { + auto mgr = weak_manager_.lock(); + if (!mgr) { + return false; + } + + if (!mgr->bind_data_service_request("GetRSCommitLSN", group_id_, [this](boost::intrusive_ptr< sisl::GenericRpcData >& rpc_data) { + LOGINFO("Received non raft service request for group_id={}", group_id_); + })) { + LOGERROR("Failed to bind non raft service request GetRSCommitLSN for group_id={}", group_id_); + return false; + } + + if (!mgr->bind_data_service_request("SyncRSCommitLSN", group_id_, [this](boost::intrusive_ptr< sisl::GenericRpcData >& rpc_data) { + LOGINFO("Received non raft service request for group_id={}", group_id_); + })) { + LOGERROR("Failed to bind non raft service request SyncRSCommitLSN for group_id={}", group_id_); + return false; + } + return true; + } + +private: + int32_t server_id_; + nuraft_mesg::group_id_t group_id_; + std::weak_ptr< nuraft_mesg::manager > weak_manager_; +}; + +} // namespace craft::net diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 1aa1ed7..06b6c97 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -15,6 +15,7 @@ #include "mem/replica.hpp" #include "mem/cluster.hpp" // the full MemTransport type +#include "mem/peer_comm.hpp" // for raft channel #include #include @@ -23,6 +24,7 @@ #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares +#include namespace craft { @@ -48,12 +50,16 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } } // namespace -MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : +MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net, + std::optional< uint16_t > raft_port) : ep_{std::move(ep)}, page_size_{page_size}, net_{std::move(net)} { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); fault_retired_.push_back(std::move(initial)); + + // init raft channel (if raft_port is provided) + if (raft_port.has_value()) { peer_comm::instance()->init_raft_server(ep_.id, raft_port.value()); } } // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── @@ -523,6 +529,15 @@ void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { state_.last_append_lsn = std::min(state_.last_append_lsn, rs_commit_lsn); } +// ── peer comm hooks (driven by raft) ── + +void MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term) { + // place holder + cold_apply_login(client_token, term); + // Phase 1: collect replica LSN state (non-RAFT broadcast) + auto vol_uuid = craft::to_uuid(volume_id); +} + // ── resolution-round hooks (driven by MemTransport::run_resolution) ── std::optional< MemCraftReplica::MemJournalSlot > MemCraftReplica::peek_slot(int64_t dlsn) { @@ -560,4 +575,29 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { return out; } +// create peer raft group and add members to it. +result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< wire::member > const& members) { + auto const group_id = craft::to_uuid(volume_id); + auto consensus = peer_comm::instance()->get_consensus(); + + // Seat THIS replica as leader by creating the group. + if (auto const status = craft::sync_get(consensus->create_group(group_id, peer_comm::default_group_type_)); + !status) { + return fail(craft_error::INTERNAL); + } + + // Add every OTHER member as a follower. + for (auto const& m : members) { + auto const member_id = craft::to_uuid(m.id); + if (member_id == ep_.id) continue; + + auto srv_cfg = nuraft::srv_config(nuraft_mesg::to_server_id(member_id), 0, m.addr, "", false); + if (auto const result = craft::sync_get(consensus->add_member(group_id, srv_cfg)); !result) { + return fail(craft_error::INTERNAL); + } + } + return {}; +} + } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index c1afe9c..64b42a3 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -36,6 +36,7 @@ #include #include // result types +#include #include "craft_peer.hpp" // the PEER plane: craft_peer + JournalSlot + lba_t (this model is its only implementer) #include "craft_replica.hpp" // the CLIENT plane: the craft_replica interface @@ -120,7 +121,8 @@ class MemCraftReplica final : public craft_replica, // How many Missing dLSNs stats() lists individually. The count is always exact. static constexpr std::size_t k_missing_sample = 16; - MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); + MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net, + std::optional< uint16_t > raft_port = std::nullopt); // Snapshot this replica's state. Takes mu_ and deliberately does NOT consult net_: do_write() locks // the transport before mu_, so reading net_ under mu_ here would invert that order. Callers that want @@ -193,10 +195,15 @@ class MemCraftReplica final : public craft_replica, // The standalone (one-process = one-replica) resolution round: itself lacking a slot IS the quorum-lacks // evidence at N=1, so every hole <= upto is verdicted Empty and the frontier advances through it. result< resolution_result > srv_resolve(client_hdr hdr, int64_t upto) { return do_resolve_local(hdr, upto); } - void srv_establish(uint64_t client_token, uint64_t term) { cold_apply_login(client_token, term); } + void srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term) { + apply_login(volume_id, client_token, term); + } void srv_end() { cold_apply_logout(); } lsn_pair srv_lsns() { return peek_lsns(); } + result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< wire::member > const& members); + private: friend class MemTransport; // the cold path drives the cold_* / peek helpers below directly, and the IO // path (send_*) reads fault_snapshot() to decide deliverability / latency @@ -262,6 +269,12 @@ class MemCraftReplica final : public craft_replica, void cold_apply_logout(); void cold_truncate_above(int64_t rs_commit_lsn); + // real hooks using raft channel + // void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); + void apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); + // void apply_logout(); + // void apply_truncate_above(int64_t rs_commit_lsn); + // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. std::optional< MemJournalSlot > peek_slot(int64_t dlsn); // copy of the slot, or nullopt if absent diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 2cb4845..b3ab8db 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -36,16 +36,20 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(server_geometry geo) : geo_{std::move(geo)} { +craft_tcp_server::craft_tcp_server(server_geometry geo, std::optional< uint16_t > raft_port) : geo_{std::move(geo)} { replica_endpoint ep; if (!geo_.members.empty()) { std::copy(geo_.members[0].id.begin(), geo_.members[0].id.end(), ep.id.begin()); // wire id[16] -> uuid ep.addr = geo_.members[0].addr; } // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). - replica_ = std::make_shared< MemCraftReplica >(std::move(ep), geo_.lba_size, nullptr); + replica_ = std::make_shared< MemCraftReplica >(std::move(ep), geo_.lba_size, nullptr, raft_port); } +craft_tcp_server::craft_tcp_server(server_geometry geo) : craft_tcp_server(std::move(geo), std::nullopt) {} +craft_tcp_server::craft_tcp_server(server_geometry geo, uint16_t raft_port) : + craft_tcp_server(std::move(geo), std::optional< uint16_t >(raft_port)) {} + craft_tcp_server::~craft_tcp_server() = default; void craft_tcp_server::log_stats() const { @@ -91,6 +95,9 @@ void craft_tcp_server::serve(craft_conn conn) { case wire::op::logout: on_logout(conn, *parsed); break; + case wire::op::create_volume: + on_create_volume(conn, *parsed); + break; default: return; // a client sends only request ops we serve; anything else resets the connection } @@ -100,10 +107,20 @@ void craft_tcp_server::serve(craft_conn conn) { void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { // login_req names the volume; this standalone reference server fronts exactly one, so any presented id is // accepted (like its fake HELO cold path). A multi-volume server routes the session-establishment by it. + + std::vector< uint8_t > out; + // fail if there is an active session + if (session_active_) { + wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_eligible), + req.hdr.request_id, {}, {}); + conn.send_all(out); + return; + } + auto const lr = wire::decode< wire::login_req >(req.op_header); session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection session_active_ = true; - replica_->srv_establish(lr.client_token, session_term_); + replica_->srv_establish(lr.volume_id, lr.client_token, session_term_); auto const lsns = replica_->srv_lsns(); wire::login_rsp rsp{}; @@ -121,7 +138,7 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { std::vector< uint8_t > body; for (auto const& m : geo_.members) wire::put_member(body, m); - std::vector< uint8_t > out; + wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, as_bytes(rsp), body); conn.send_all(out); @@ -135,7 +152,7 @@ void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { // subsequent IO at this term is accepted. Re-HELO after a term bump just re-establishes at the new term. session_term_ = hr.term; session_active_ = true; - replica_->srv_establish(hr.client_token, hr.term); + replica_->srv_establish(hr.volume_id, hr.client_token, session_term_); std::vector< uint8_t > out; wire::frame_message(out, wire::op::helo_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, {}, {}); conn.send_all(out); @@ -283,4 +300,24 @@ void craft_tcp_server::on_keep_alive(craft_conn& conn, wire::message const& req) conn.send_all(out); } +void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& req) { + auto const cr = wire::decode< wire::volume_create_req >(req.op_header); + auto const members = wire::decode_members(req.body, cr.member_count); + + wire::status code = wire::status::ok; + if (!members) { + code = wire::status::invalid_argument; // body shorter than member_count implies -- malformed request + } else { + auto const r = replica_->srv_create_volume(cr.volume_id, *members); + if (!r) code = to_wire_status(r.error()); + } + + LOGINFO("craft_srv CREATE_VOLUME [rid:{}] members={} status={}", req.hdr.request_id, cr.member_count, + static_cast< int >(code)); + + std::vector< uint8_t > out; + wire::frame_message(out, wire::op::create_volume_rsp, static_cast< uint8_t >(code), req.hdr.request_id, {}, {}); + conn.send_all(out); +} + } // namespace craft::net diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index 2a2feea..77489e3 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -49,6 +49,7 @@ struct server_geometry { class craft_tcp_server { public: explicit craft_tcp_server(server_geometry geo); + explicit craft_tcp_server(server_geometry geo, uint16_t raft_port); ~craft_tcp_server(); craft_tcp_server(craft_tcp_server&&) = default; craft_tcp_server& operator=(craft_tcp_server&&) = default; @@ -70,6 +71,8 @@ class craft_tcp_server { uint64_t session_term_ = 0; // the current session's term, stamped on every IO bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced + craft_tcp_server(server_geometry geo, std::optional< uint16_t > raft_port); + void on_login(craft_conn&, wire::message const&); void on_helo(craft_conn&, wire::message const&); void on_logout(craft_conn&, wire::message const&); @@ -77,6 +80,7 @@ class craft_tcp_server { void on_read(craft_conn&, wire::message const&); void on_keep_alive(craft_conn&, wire::message const&); void on_resolve(craft_conn&, wire::message const&); + void on_create_volume(craft_conn&, wire::message const&); }; } // namespace craft::net diff --git a/src/wire.cpp b/src/wire.cpp index 608fb75..d3a5e90 100644 --- a/src/wire.cpp +++ b/src/wire.cpp @@ -22,21 +22,23 @@ std::optional< std::size_t > op_hdr_size(uint8_t op_code) noexcept { // A peer-plane op (15+, see craft_peer.hpp) must add its row here AND bump op::k_max_op -- the static_assert // below makes forgetting either one a compile error rather than an unknown_op at runtime. static constexpr std::size_t k[] = { - 0, // 0 unused - sizeof(login_req), // 1 login - sizeof(login_rsp), // 2 login_rsp - sizeof(helo_req), // 3 helo - 0, // 4 helo_rsp (status only) - sizeof(write_req), // 5 write - sizeof(write_rsp), // 6 write_rsp - sizeof(read_req), // 7 read - sizeof(read_rsp), // 8 read_rsp - sizeof(keepalive_req), // 9 keepalive - sizeof(keepalive_rsp), // 10 keepalive_rsp - sizeof(logout_req), // 11 logout - 0, // 12 logout_rsp (status only) - sizeof(resolve_req), // 13 resolve - sizeof(resolve_rsp), // 14 resolve_rsp + 0, // 0 unused + sizeof(login_req), // 1 login + sizeof(login_rsp), // 2 login_rsp + sizeof(helo_req), // 3 helo + 0, // 4 helo_rsp (status only) + sizeof(write_req), // 5 write + sizeof(write_rsp), // 6 write_rsp + sizeof(read_req), // 7 read + sizeof(read_rsp), // 8 read_rsp + sizeof(keepalive_req), // 9 keepalive + sizeof(keepalive_rsp), // 10 keepalive_rsp + sizeof(logout_req), // 11 logout + 0, // 12 logout_rsp (status only) + sizeof(resolve_req), // 13 resolve + sizeof(resolve_rsp), // 14 resolve_rsp + sizeof(volume_create_req), // 15 create_volume + 0, // 16 create_volume_rsp (status only) }; static_assert(std::size(k) == static_cast< std::size_t >(op::k_max_op) + 1, "op_hdr_size table and op::k_max_op disagree -- a new opcode was added without a header size, " diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index 847996a..dc42faa 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -294,3 +294,45 @@ TEST(CraftTcp, ConnectDeadlineBoundsABlackholedPeer) { EXPECT_FALSE(c.has_value()); EXPECT_LT(elapsed, std::chrono::seconds{5}) << "connect must fail at the deadline, not the SYN-retry window"; } + +TEST(CraftTcp, ServerWithRaft) { + auto lst = craft_listener::bind_listen(0); + ASSERT_TRUE(lst.has_value()); + uint16_t const port = lst->port(); + + craft_tcp_server server{make_geo(), 6666}; + std::jthread srv([&] { + auto conn = lst->accept(); + if (conn) server.serve(std::move(*conn)); + }); + + { + auto cli = wire_client::connect("127.0.0.1", port); + ASSERT_TRUE(cli.has_value()); + + auto lr = cli->login(/*volume_id=*/{}, 0xABCD); + ASSERT_TRUE(lr.has_value()); + EXPECT_EQ(lr->term, 1u); + EXPECT_EQ(lr->capacity, uint64_t{1} << 30); + EXPECT_EQ(lr->lba_size, k_lba); + EXPECT_EQ(lr->max_tx, 512u * 1024); + // The login WATERMARK: the LAST dLSN already durable, which on a fresh replica is -1. NOT the next dLSN + // to use -- the client derives that itself (next_dlsn_ = dlsn + 1). This assertion used to read `0` (and + // the server used to send last_append_lsn + 1 to match), which meant the client started at dLSN 1 and + // slot 0 was never written: every replica sat permanently Missing dLSN 0, apply_up_to() stalled there, + // and commit_lsn pinned at -1 forever. Reads still served correct bytes off the journal-tail overlay, so + // nothing failed -- it only showed up as a read walking the entire journal tail. Hence the guard below. + EXPECT_EQ(lr->dlsn, -1); + ASSERT_EQ(lr->members.size(), 1u); + EXPECT_EQ(lr->members[0].addr, "127.0.0.1:0"); + EXPECT_EQ(lr->members[0].id[0], 0x01); + + auto lo = cli->logout(); + ASSERT_TRUE(lo.has_value()); + EXPECT_EQ(*lo, wire::status::ok); + + auto lo2 = cli->logout(); // the session is gone -> fenced + ASSERT_TRUE(lo2.has_value()); + EXPECT_EQ(*lo2, wire::status::stale_term); + } +} diff --git a/test/test_wire.cpp b/test/test_wire.cpp index 7929cb3..cac68a8 100644 --- a/test/test_wire.cpp +++ b/test/test_wire.cpp @@ -72,8 +72,10 @@ TEST(CraftWire, OpHeaderSizes) { EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::logout)), 16u); EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::resolve)), 24u); EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::resolve_rsp)), 16u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::create_volume)), 24u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::create_volume_rsp)), 0u); EXPECT_FALSE(op_hdr_size(0).has_value()); - EXPECT_FALSE(op_hdr_size(15).has_value()); + EXPECT_FALSE(op_hdr_size(17).has_value()); EXPECT_FALSE(op_hdr_size(99).has_value()); EXPECT_TRUE(is_response(static_cast< uint8_t >(op::write_rsp))); From 551e983b999f630d044da528c6def78585bfb346 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Mon, 20 Jul 2026 01:21:30 -0700 Subject: [PATCH 02/24] Add wire ops and structures for non raft peer to peer communication --- CMakeLists.txt | 22 +- include/craft/client.hpp | 2 + include/craft/wire.hpp | 44 +++- src/craft_peer.hpp | 3 +- src/mem/helper.hpp | 40 ++++ src/mem/peer_comm.cpp | 45 ---- src/mem/raft/in_memory_log_store.cpp | 218 ++++++++++++++++++ src/mem/raft/in_memory_log_store.hpp | 91 ++++++++ src/mem/raft/raft_service.cpp | 84 +++++++ .../{peer_comm.hpp => raft/raft_service.hpp} | 40 ++-- src/mem/raft/raft_state_machine.hpp | 47 ++++ src/mem/raft/raft_state_manager.cpp | 126 ++++++++++ src/mem/raft/raft_state_manager.hpp | 32 +++ src/mem/raft_state_mgr.hpp | 72 ------ src/mem/replica.cpp | 38 +-- src/mem/replica.hpp | 19 +- src/net/tcp_peer.cpp | 84 +++++++ src/net/tcp_peer.hpp | 49 ++++ src/net/tcp_server.cpp | 90 +++++++- src/net/tcp_server.hpp | 7 +- src/replica_mgr.cpp | 68 ++++++ src/replica_mgr.hpp | 50 ++++ src/wire.cpp | 47 ++-- test/test_async_tcp.cpp | 2 - test/test_tcp.cpp | 42 ---- test/test_wire.cpp | 8 +- tools/craft_reference_tcp_srv.cpp | 26 ++- tools/raft_bootstrap.py | 93 ++++++++ tools/server_config.json | 22 ++ 29 files changed, 1247 insertions(+), 264 deletions(-) create mode 100644 src/mem/helper.hpp delete mode 100644 src/mem/peer_comm.cpp create mode 100644 src/mem/raft/in_memory_log_store.cpp create mode 100644 src/mem/raft/in_memory_log_store.hpp create mode 100644 src/mem/raft/raft_service.cpp rename src/mem/{peer_comm.hpp => raft/raft_service.hpp} (50%) create mode 100644 src/mem/raft/raft_state_machine.hpp create mode 100644 src/mem/raft/raft_state_manager.cpp create mode 100644 src/mem/raft/raft_state_manager.hpp delete mode 100644 src/mem/raft_state_mgr.hpp create mode 100644 src/net/tcp_peer.cpp create mode 100644 src/net/tcp_peer.hpp create mode 100644 src/replica_mgr.cpp create mode 100644 src/replica_mgr.hpp create mode 100644 tools/raft_bootstrap.py create mode 100644 tools/server_config.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 24b29ab..34af253 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,17 +57,35 @@ target_include_directories(craft_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(craft_client PUBLIC craft_wire craft_types sisl::sisl liburing::liburing) target_compile_features(craft_client PUBLIC cxx_std_23) +# ── craft_replica_mgr: the internal replica manager (testing only) ── +add_library(craft_replica_mgr STATIC + src/replica_mgr.cpp + src/net/tcp_peer.cpp) +target_include_directories(craft_replica_mgr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(craft_replica_mgr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(craft_replica_mgr PUBLIC craft_client) +target_compile_features(craft_replica_mgr PUBLIC cxx_std_23) + # ── craft_reference: the in-memory reference model + loopback cluster server (test-support only) ── +add_library(raft_service STATIC + src/mem/raft/raft_service.cpp + src/mem/raft/in_memory_log_store.cpp + src/mem/raft/raft_state_manager.cpp) +target_include_directories(raft_service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(raft_service PUBLIC nuraft_mesg::proto craft_replica_mgr) +target_compile_features(raft_service PUBLIC cxx_std_23) + add_library(craft_reference STATIC src/mem/replica.cpp src/mem/cluster.cpp src/local.cpp # craft/local.hpp: the public "no-remote, in-process" backend builder over the mem model src/net/cluster_server.cpp src/net/tcp_server.cpp - src/mem/peer_comm.cpp) + src/net/tcp_peer.cpp) target_include_directories(craft_reference PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # the internal headers it implements -target_link_libraries(craft_reference PUBLIC craft_client nuraft_mesg::proto) +target_link_libraries(craft_reference PUBLIC craft_replica_mgr raft_service) target_compile_features(craft_reference PUBLIC cxx_std_23) # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to diff --git a/include/craft/client.hpp b/include/craft/client.hpp index 25cca1c..8f8899c 100644 --- a/include/craft/client.hpp +++ b/include/craft/client.hpp @@ -56,6 +56,8 @@ class craft_replica; template < typename T > using async_result = sisl::async::light_result< T >; using async_status = sisl::async::light_status; +template < typename T > +using result = sisl::result< T >; // ── construction: the ONE seam ── // diff --git a/include/craft/wire.hpp b/include/craft/wire.hpp index f0025dc..ad4546a 100644 --- a/include/craft/wire.hpp +++ b/include/craft/wire.hpp @@ -79,7 +79,11 @@ enum class op : uint8_t { // misclassified as "not a response". That is the trap this constant exists to close. create_volume = 15, // client-requested volume creation (leader-only) create_volume_rsp = 16, - k_max_op = 16, // highest allocated opcode; raise when the peer plane lands + get_rs_commit_lsn = 17, + get_rs_commit_lsn_rsp = 18, + fetch_data = 19, + fetch_data_rsp = 20, + k_max_op = 20, // highest allocated opcode; raise when the peer plane lands }; // Response `status` byte; 1-6 mirror craft_error (craft_types.hpp). @@ -226,6 +230,39 @@ struct volume_create_req { }; // volume_create_rsp: status only (no operation header, no body). +// GetRSCommitLSN: non-RAFT peer query of a replica's {commit_lsn, last_append_lsn}. is_login triggers the +// quiesce barrier on the responder (see CRAFT Design's Login section). my_commit/my_append are the LEADER's +// own watermarks, riding the request per "the poll set includes the leader itself". +struct get_rs_commit_lsn_req { + uint64_t term; + uint8_t is_login; // bool, but keep POD-packed layout consistent with the rest of this file + uint8_t reserved[7]; +}; +struct get_rs_commit_lsn_rsp { + int64_t commit_lsn; + int64_t last_append_lsn; +}; + +// used during login and recovery +struct fetch_data_req { + uint32_t lsn_count; + uint32_t reserved; +}; // body: lsn_count x int64_t + +struct fetch_slot_desc { + int64_t lsn; // which dLSN this is + uint64_t lba; // where it writes to + uint32_t len; // how many blocks + uint8_t is_empty; // Empty verdict? (no data follows) + uint8_t all_zeros; // zero write? (no data follows) + uint8_t reserved[2]; // padding +}; +struct fetch_data_rsp { + uint32_t slot_count; // how many fetch_slot_desc entries are in the body + uint32_t reserved; +}; +// body: slot_count x fetch_slot_desc, THEN the raw data bytes for slots that have real data + #pragma pack(pop) static_assert(sizeof(msg_hdr) == 8); @@ -244,6 +281,11 @@ static_assert(sizeof(logout_req) == 16); static_assert(sizeof(resolve_req) == 24); static_assert(sizeof(resolve_rsp) == 16); static_assert(sizeof(volume_create_req) == 32); +static_assert(sizeof(get_rs_commit_lsn_req) == 16); +static_assert(sizeof(get_rs_commit_lsn_rsp) == 16); +static_assert(sizeof(fetch_data_req) == 8); +static_assert(sizeof(fetch_slot_desc) == 24); +static_assert(sizeof(fetch_data_rsp) == 8); // The fixed operation-header size for an op code (0 for a status-only response). nullopt = unknown op, which // is unframeable -- the caller resets the connection. std::optional< std::size_t > op_hdr_size(uint8_t op_code) noexcept; diff --git a/src/craft_peer.hpp b/src/craft_peer.hpp index 155e232..29db72f 100644 --- a/src/craft_peer.hpp +++ b/src/craft_peer.hpp @@ -47,6 +47,7 @@ #include // sisl::sg_list #include // the CRAFT vocabulary + the result / async_result aliases +#include // result types namespace craft { @@ -78,7 +79,7 @@ class craft_peer { // Snapshot {commit_lsn, last_append_lsn} for this replica -- the leader's GetRSCommitLSN poll. Identical to // what the client plane's keep_alive returns, but asked by a peer, not a client. virtual async_result< lsn_pair > get_lsns() = 0; - virtual async_result< lsn_pair > get_rs_commit_lsn() = 0; + virtual async_result< lsn_pair > get_rs_commit_lsn(uint64_t term, bool is_login) = 0; // Pull raw journal data for the requested dLSNs -- the resync fetch. A slot this replica has verdicted Empty // comes back as JournalSlot{.is_empty = true} rather than an error; a slot it simply does not hold is omitted. diff --git a/src/mem/helper.hpp b/src/mem/helper.hpp new file mode 100644 index 0000000..0a873b6 --- /dev/null +++ b/src/mem/helper.hpp @@ -0,0 +1,40 @@ +#include +#include + + +namespace craft { + +using json = nlohmann::json; + +inline boost::uuids::uuid to_uuid(std::array< uint8_t, 16 > const& arr) { + boost::uuids::uuid u{}; + std::copy(arr.begin(), arr.end(), u.begin()); + return u; +} + +// make sync coro calls, taken from homestore +template < typename Task > +inline auto sync_get(Task&& task) { + auto result = stdexec::sync_wait(std::forward< Task >(task)).value(); + if constexpr (std::tuple_size_v< decltype(result) > == 0) { + return; + } else { + return std::get< 0 >(std::move(result)); + } +} + +inline std::error_condition jsonObjectFromFile(std::string const& filename, json& json_object) { + std::ifstream istrm(filename, std::ios::binary); + if (!istrm.is_open()) { + return std::make_error_condition(std::errc::no_such_file_or_directory); + } + + istrm >> json_object; + if (!json_object.is_object()) { + LOGERROR("Could not parse file: {}", filename); + return std::make_error_condition(std::errc::invalid_argument); + } + return std::error_condition(); +} + +} \ No newline at end of file diff --git a/src/mem/peer_comm.cpp b/src/mem/peer_comm.cpp deleted file mode 100644 index 328ecc3..0000000 --- a/src/mem/peer_comm.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include -#include "peer_comm.hpp" -#include "raft_state_mgr.hpp" - -#include -#include -#include - -namespace craft { - -std::shared_ptr< peer_comm > peer_comm::instance() { - static std::shared_ptr< peer_comm > instance{new peer_comm()}; - return instance; -} - -consensus_handle peer_comm::get_consensus() { return consensus_; } - -void peer_comm::init_raft_server(boost::uuids::uuid const& server_uuid, uint16_t port) { - auto params = nuraft_mesg::manager::params{ - .server_uuid_ = server_uuid, - .mesg_port_ = port, - .default_group_type_ = default_group_type_, - }; - consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true); - auto raft_params = nuraft::raft_params{}; - consensus_->register_mgr_type(default_group_type_, raft_params); - LOGINFO("Initialized peer_comm for {} with raft consensus manager, port {}", params.server_uuid_, params.mesg_port_); -} - -std::string peer_comm::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { - auto it = peer_lookup_map_.find(peer_id); - if (it == peer_lookup_map_.end()) { - LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); - return {}; - } - return it->second; -} - -std::shared_ptr< nuraft_mesg::mesg_state_mgr > peer_comm::create_state_mgr(int32_t const srv_id, - nuraft_mesg::group_id_t const& group_id) { - LOGINFO("Creating raft state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); - return std::make_shared< raft_state_mgr >(srv_id, group_id, consensus_); -} - -} // namespace craft diff --git a/src/mem/raft/in_memory_log_store.cpp b/src/mem/raft/in_memory_log_store.cpp new file mode 100644 index 0000000..5076d44 --- /dev/null +++ b/src/mem/raft/in_memory_log_store.cpp @@ -0,0 +1,218 @@ +/************************************************************************ +Copyright 2017-2019 eBay Inc. +Author/Developer(s): Jung-Sang Ahn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +**************************************************************************/ + +#include "in_memory_log_store.hpp" + +#include + +#include + +namespace nuraft { + +inmem_log_store::inmem_log_store() : start_idx_(1) { + // Dummy entry for index 0. + ptr< buffer > buf = buffer::alloc(sz_ulong); + logs_[0] = cs_new< log_entry >(0, buf); +} + +inmem_log_store::~inmem_log_store() {} + +ptr< log_entry > inmem_log_store::make_clone(const ptr< log_entry >& entry) { + // NOTE: + // Timestamp is used only when `replicate_log_timestamp_` option is on. + // Otherwise, log store does not need to store or load it. + ptr< log_entry > clone = cs_new< log_entry >(entry->get_term(), buffer::clone(entry->get_buf()), + entry->get_val_type(), entry->get_timestamp()); + return clone; +} + +ulong inmem_log_store::next_slot() const { + std::lock_guard< std::mutex > l(logs_lock_); + // Exclude the dummy entry. + return start_idx_ + logs_.size() - 1; +} + +ptr< log_entry > inmem_log_store::last_entry() const { + ulong next_idx = next_slot(); + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.find(next_idx - 1); + if (entry == logs_.end()) { entry = logs_.find(0); } + + return make_clone(entry->second); +} + +ulong inmem_log_store::append(ptr< log_entry >& entry) { + ptr< log_entry > clone = make_clone(entry); + + std::lock_guard< std::mutex > l(logs_lock_); + size_t idx = start_idx_ + logs_.size() - 1; + logs_[idx] = clone; + return idx; +} + +void inmem_log_store::write_at(ulong index, ptr< log_entry >& entry) { + ptr< log_entry > clone = make_clone(entry); + + // Discard all logs equal to or greater than `index. + std::lock_guard< std::mutex > l(logs_lock_); + auto itr = logs_.lower_bound(index); + while (itr != logs_.end()) { + itr = logs_.erase(itr); + } + logs_[index] = clone; +} + +ptr< std::vector< ptr< log_entry > > > inmem_log_store::log_entries(ulong start, ulong end) { + ptr< std::vector< ptr< log_entry > > > ret = cs_new< std::vector< ptr< log_entry > > >(); + + ret->resize(end - start); + ulong cc = 0; + for (ulong ii = start; ii < end; ++ii) { + ptr< log_entry > src = nullptr; + { + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.find(ii); + if (entry == logs_.end()) { + entry = logs_.find(0); + assert(0); + } + src = entry->second; + } + (*ret)[cc++] = make_clone(src); + } + return ret; +} + +ptr< std::vector< ptr< log_entry > > > inmem_log_store::log_entries_ext(ulong start, ulong end, + int64 batch_size_hint_in_bytes) { + ptr< std::vector< ptr< log_entry > > > ret = cs_new< std::vector< ptr< log_entry > > >(); + + if (batch_size_hint_in_bytes < 0) { return ret; } + + size_t accum_size = 0; + for (ulong ii = start; ii < end; ++ii) { + ptr< log_entry > src = nullptr; + { + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.find(ii); + if (entry == logs_.end()) { + entry = logs_.find(0); + assert(0); + } + src = entry->second; + } + ret->push_back(make_clone(src)); + accum_size += src->get_buf().size(); + if (batch_size_hint_in_bytes && accum_size >= (ulong)batch_size_hint_in_bytes) break; + } + return ret; +} + +ptr< log_entry > inmem_log_store::entry_at(ulong index) { + ptr< log_entry > src = nullptr; + { + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.find(index); + if (entry == logs_.end()) { entry = logs_.find(0); } + src = entry->second; + } + return make_clone(src); +} + +ulong inmem_log_store::term_at(ulong index) { + ulong term = 0; + { + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.find(index); + if (entry == logs_.end()) { entry = logs_.find(0); } + term = entry->second->get_term(); + } + return term; +} + +ptr< buffer > inmem_log_store::pack(ulong index, int32 cnt) { + std::vector< ptr< buffer > > logs; + + size_t size_total = 0; + for (ulong ii = index; ii < index + cnt; ++ii) { + ptr< log_entry > le = nullptr; + { + std::lock_guard< std::mutex > l(logs_lock_); + le = logs_[ii]; + } + assert(le.get()); + ptr< buffer > buf = le->serialize(); + size_total += buf->size(); + logs.push_back(buf); + } + + ptr< buffer > buf_out = buffer::alloc(sizeof(int32) + cnt * sizeof(int32) + size_total); + buf_out->pos(0); + buf_out->put((int32)cnt); + + for (auto& entry : logs) { + ptr< buffer >& bb = entry; + buf_out->put((int32)bb->size()); + buf_out->put(*bb); + } + return buf_out; +} + +void inmem_log_store::apply_pack(ulong index, buffer& pack) { + pack.pos(0); + int32 num_logs = pack.get_int(); + + for (int32 ii = 0; ii < num_logs; ++ii) { + ulong cur_idx = index + ii; + int32 buf_size = pack.get_int(); + + ptr< buffer > buf_local = buffer::alloc(buf_size); + pack.get(buf_local); + + ptr< log_entry > le = log_entry::deserialize(*buf_local); + { + std::lock_guard< std::mutex > l(logs_lock_); + logs_[cur_idx] = le; + } + } + + { + std::lock_guard< std::mutex > l(logs_lock_); + auto entry = logs_.upper_bound(0); + if (entry != logs_.end()) { + start_idx_ = entry->first; + } else { + start_idx_ = 1; + } + } +} + +bool inmem_log_store::compact(ulong last_log_index) { + std::lock_guard< std::mutex > l(logs_lock_); + for (ulong ii = start_idx_; ii <= last_log_index; ++ii) { + auto entry = logs_.find(ii); + if (entry != logs_.end()) { logs_.erase(entry); } + } + + // WARNING: + // Even though nothing has been erased, + // we should set `start_idx_` to new index. + if (start_idx_ <= last_log_index) { start_idx_ = last_log_index + 1; } + return true; +} + +} // namespace nuraft diff --git a/src/mem/raft/in_memory_log_store.hpp b/src/mem/raft/in_memory_log_store.hpp new file mode 100644 index 0000000..6cc8e1f --- /dev/null +++ b/src/mem/raft/in_memory_log_store.hpp @@ -0,0 +1,91 @@ +/************************************************************************ +Copyright 2017-2019 eBay Inc. +Author/Developer(s): Jung-Sang Ahn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +**************************************************************************/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace nuraft { + +class raft_server; + +class inmem_log_store : public log_store { +public: + inmem_log_store(); + + ~inmem_log_store(); + + __nocopy__(inmem_log_store); + +public: + ulong next_slot() const; + + ulong start_index() const { return start_idx_; } + + ptr< log_entry > last_entry() const; + + ulong append(ptr< log_entry >& entry); + + void write_at(ulong index, ptr< log_entry >& entry); + + ptr< std::vector< ptr< log_entry > > > log_entries(ulong start, ulong end); + + ptr< std::vector< ptr< log_entry > > > log_entries_ext(ulong start, ulong end, int64 batch_size_hint_in_bytes = 0); + + ptr< log_entry > entry_at(ulong index); + + ulong term_at(ulong index); + + ptr< buffer > pack(ulong index, int32 cnt); + + void apply_pack(ulong index, buffer& pack); + + bool compact(ulong last_log_index); + + bool flush() override { return true; } + + void close() {} + + ulong last_durable_index() { return next_slot() - 1; } + +private: + static ptr< log_entry > make_clone(const ptr< log_entry >& entry); + + /** + * Map of . + */ + std::map< ulong, ptr< log_entry > > logs_; + + /** + * Lock for `logs_`. + */ + mutable std::mutex logs_lock_; + + /** + * The index of the first log. + */ + std::atomic< ulong > start_idx_; +}; + +} // namespace nuraft + diff --git a/src/mem/raft/raft_service.cpp b/src/mem/raft/raft_service.cpp new file mode 100644 index 0000000..1aaae92 --- /dev/null +++ b/src/mem/raft/raft_service.cpp @@ -0,0 +1,84 @@ +#include +#include "raft_service.hpp" +#include "raft_state_manager.hpp" +#include "mem/helper.hpp" +#include "replica_mgr.hpp" + +#include +#include +#include +#include + +namespace craft { + +namespace { +auto fail(craft_error e) { return std::unexpected(make_error_condition(e)); } +} // namespace + +std::shared_ptr< raft_service > raft_service::instance() { + static std::shared_ptr< raft_service > instance{new raft_service()}; + return instance; +} + +consensus_handle raft_service::get_consensus() { return consensus_; } + +void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { + std::call_once(raft_started_, [&] { + auto const my_port = replica_manager::instance()->get(server_uuid)->raft_port; + auto params = nuraft_mesg::manager::params{ + .server_uuid_ = server_uuid, + .mesg_port_ = my_port, + .default_group_type_ = default_group_type_, + }; + consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), false /*with_data_svc*/); + auto raft_params = nuraft::raft_params{}; + consensus_->register_mgr_type(default_group_type_, raft_params); + server_uuid_ = server_uuid; + LOGINFO("Initialized raft_service for {} with raft consensus manager, port {}", params.server_uuid_, + params.mesg_port_); + }); +} + +result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< wire::member > const& members) { + auto const group_id = craft::to_uuid(volume_id); + auto consensus = raft_service::instance()->get_consensus(); + + // Seat THIS replica as leader by creating the group. + if (auto const status = craft::sync_get(consensus->create_group(group_id, raft_service::default_group_type_)); + !status) { + return fail(craft_error::INTERNAL); + } + + // Add every OTHER member as a follower. + for (auto const& m : members) { + auto const member_id = craft::to_uuid(m.id); + if (lookup_peer(member_id).empty()) { + return fail(craft_error::INTERNAL); // sanity: member missing in the server config json + } + if (member_id == server_uuid_) continue; + + auto srv_cfg = + nuraft::srv_config(nuraft_mesg::to_server_id(member_id), 0, boost::uuids::to_string(member_id), "", false); + if (auto const result = craft::sync_get(consensus->add_member(group_id, srv_cfg)); !result) { + return fail(craft_error::INTERNAL); + } + } + return {}; +} + +std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { + + auto peer_addr = replica_manager::instance()->lookup_peer(peer_id); + if (peer_addr.empty()) { LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); } + return peer_addr; +} + +std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, + nuraft_mesg::group_id_t const& group_id) { + LOGINFO("Creating raft state manager for server_id={} group_id={}, server_uuid={}", srv_id, + boost::uuids::to_string(group_id), boost::uuids::to_string(server_uuid_)); + return std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id); +} + +} // namespace craft diff --git a/src/mem/peer_comm.hpp b/src/mem/raft/raft_service.hpp similarity index 50% rename from src/mem/peer_comm.hpp rename to src/mem/raft/raft_service.hpp index 2460a34..acac753 100644 --- a/src/mem/peer_comm.hpp +++ b/src/mem/raft/raft_service.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include // result types namespace nuraft_mesg { class manager; @@ -18,15 +20,16 @@ namespace craft { // Process-wide bridge for the peer-to-peer consensus engine used by the TCP server and replica-side code. // The concrete nuraft_mesg::manager instance is installed once and then shared by anyone that needs to create // groups, add members, or issue consensus operations. -class peer_comm : public nuraft_mesg::messaging_application, - public std::enable_shared_from_this< peer_comm > { +class raft_service : public nuraft_mesg::messaging_application, public std::enable_shared_from_this< raft_service > { public: - inline static const std::string default_group_type_{"peer_comm_raft"}; + inline static const std::string default_group_type_{"raft_service_raft"}; - virtual ~peer_comm() = default; - static std::shared_ptr< peer_comm > instance(); + virtual ~raft_service() = default; + static std::shared_ptr< raft_service > instance(); consensus_handle get_consensus(); - void init_raft_server(boost::uuids::uuid const& server_uuid, uint16_t port); + void start_raft_service(boost::uuids::uuid const& server_uuid); + result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< wire::member > const& members); // messaging_application overrides std::string lookup_peer(nuraft_mesg::peer_id_t const&) override; @@ -34,28 +37,11 @@ class peer_comm : public nuraft_mesg::messaging_application, nuraft_mesg::group_id_t const& group_id) override; private: - peer_comm() = default; + raft_service() = default; consensus_handle consensus_; - std::map< nuraft_mesg::peer_id_t, std::string > peer_lookup_map_; + nuraft_mesg::peer_id_t server_uuid_; + std::once_flag raft_started_; + nlohmann::json server_config_; }; -// helper methods - -inline static boost::uuids::uuid to_uuid(std::array< uint8_t, 16 > const& arr) { - boost::uuids::uuid u{}; - std::copy(arr.begin(), arr.end(), u.begin()); - return u; -} - -// make sync coro calls, taken from homestore -template < typename Task > -inline auto sync_get(Task&& task) { - auto result = stdexec::sync_wait(std::forward< Task >(task)).value(); - if constexpr (std::tuple_size_v< decltype(result) > == 0) { - return; - } else { - return std::get< 0 >(std::move(result)); - } -} - } // namespace craft diff --git a/src/mem/raft/raft_state_machine.hpp b/src/mem/raft/raft_state_machine.hpp new file mode 100644 index 0000000..6a40fe3 --- /dev/null +++ b/src/mem/raft/raft_state_machine.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace craft { + +class echo_state_machine : public nuraft::state_machine { +public: + echo_state_machine() : lock_(), last_commit_idx_(0) {} + +public: + virtual nuraft::ptr< nuraft::buffer > commit(const nuraft::ulong log_idx, nuraft::buffer& data) { + auto_lock(lock_); + + LOGINFO("Commit message [{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); + last_commit_idx_ = log_idx; + return nullptr; + } + + virtual nuraft::ptr< nuraft::buffer > pre_commit(const nuraft::ulong log_idx, nuraft::buffer& data) { + auto_lock(lock_); + LOGINFO("Pre-Commit message [{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); + return nullptr; + } + + virtual void rollback(const nuraft::ulong log_idx, nuraft::buffer& data) { + auto_lock(lock_); + LOGINFO("Rollback[{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); + } + + virtual void save_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) {} + virtual bool apply_snapshot(nuraft::snapshot& s) { return true; } + + virtual int read_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) { return 0; } + + virtual nuraft::ptr< nuraft::snapshot > last_snapshot() { return nuraft::ptr< nuraft::snapshot >(); } + + virtual void create_snapshot(nuraft::snapshot& s, nuraft::async_result< bool >::handler_type& when_done) {} + + virtual nuraft::ulong last_commit_index() { return last_commit_idx_; } + +private: + std::mutex lock_; + nuraft::ulong last_commit_idx_; +}; +} \ No newline at end of file diff --git a/src/mem/raft/raft_state_manager.cpp b/src/mem/raft/raft_state_manager.cpp new file mode 100644 index 0000000..80bfd9d --- /dev/null +++ b/src/mem/raft/raft_state_manager.cpp @@ -0,0 +1,126 @@ +#include "raft_state_manager.hpp" +#include "raft_state_machine.hpp" + +#include +#include + +#include + +#include "in_memory_log_store.hpp" +#include "mem/helper.hpp" + +namespace craft { + +std::error_condition loadConfigFile(json& config_map, std::string const& _group_id, int32_t const _srv_id) { + auto const config_file = fmt::format(FMT_STRING("{}_s{}/config.json"), _group_id, _srv_id); + return jsonObjectFromFile(config_file, config_map); +} + +std::error_condition loadStateFile(json& state_map, std::string const& _group_id, int32_t const _srv_id) { + auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); + return jsonObjectFromFile(state_file, state_map); +} + +nuraft::ptr< nuraft::srv_config > fromServer(json const& server) { + auto const id = static_cast< int32_t >(server["id"]); + auto const dc_id = static_cast< int32_t >(server["dc_id"]); + auto const endpoint = server["endpoint"]; + auto const aux = server["aux"]; + auto const learner = server["learner"]; + auto const prior = static_cast< int32_t >(server["priority"]); + return nuraft::cs_new< nuraft::srv_config >(id, dc_id, endpoint, aux, learner, prior); +} + +void fromServers(json const& servers, std::list< nuraft::ptr< nuraft::srv_config > >& server_list) { + for (auto const& server_conf : servers) { + server_list.push_back(fromServer(server_conf)); + } +} + +json toServers(std::list< nuraft::ptr< nuraft::srv_config > > const& server_list) { + auto servers = json::array(); + for (auto const& server_conf : server_list) { + servers.push_back(json{{"id", server_conf->get_id()}, + {"dc_id", server_conf->get_dc_id()}, + {"endpoint", server_conf->get_endpoint()}, + {"aux", server_conf->get_aux()}, + {"learner", server_conf->is_learner()}, + {"priority", server_conf->get_priority()}}); + } + return servers; +} + +nuraft::ptr< nuraft::cluster_config > fromClusterConfig(json const& cluster_config) { + auto const& log_idx = cluster_config["log_idx"]; + auto const& prev_log_idx = cluster_config["prev_log_idx"]; + auto const& eventual = cluster_config["eventual_consistency"]; + + auto raft_config = nuraft::cs_new< nuraft::cluster_config >(log_idx, prev_log_idx, eventual); + fromServers(cluster_config["servers"], raft_config->get_servers()); + return raft_config; +} + +raft_state_mgr::raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, + nuraft_mesg::group_id_t const& group_id) : + nuraft_mesg::mesg_state_mgr(), _srv_id(srv_id), _srv_addr(to_string(srv_addr)), _group_id(to_string(group_id)) {} + +nuraft::ptr< nuraft::cluster_config > raft_state_mgr::load_config() { + LOGDEBUG("Loading config for [{}]", _group_id); + json config_map; + if (auto err = loadConfigFile(config_map, _group_id, _srv_id); !err) { return fromClusterConfig(config_map); } + auto conf = nuraft::cs_new< nuraft::cluster_config >(); + conf->get_servers().push_back(nuraft::cs_new< nuraft::srv_config >(_srv_id, _srv_addr)); + return conf; +} + +nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { + return nuraft::cs_new< nuraft::inmem_log_store >(); +} + +nuraft::ptr< nuraft::srv_state > raft_state_mgr::read_state() { + LOGDEBUG("Loading state for server: {}", _srv_id); + json state_map; + auto state = nuraft::cs_new< nuraft::srv_state >(); + if (auto err = loadStateFile(state_map, _group_id, _srv_id); !err) { + try { + state->set_term(static_cast< uint64_t >(state_map["term"])); + state->set_voted_for(static_cast< int >(state_map["voted_for"])); + } catch (std::out_of_range& e) { LOGWARN("State file was not in the expected format!"); } + } + return state; +} + +void raft_state_mgr::save_config(const nuraft::cluster_config& config) { + auto const config_file = fmt::format(FMT_STRING("{}_s{}/config.json"), _group_id, _srv_id); + auto json_obj = json{{"log_idx", config.get_log_idx()}, + {"prev_log_idx", config.get_prev_log_idx()}, + {"eventual_consistency", config.is_async_replication()}, + {"user_ctx", config.get_user_ctx()}, + {"servers", toServers(const_cast< nuraft::cluster_config& >(config).get_servers())}}; + try { + std::ofstream ostrm(config_file, std::ios::binary); + if (ostrm.is_open()) { ostrm << json_obj; } + } catch (std::exception& e) { LOGERROR("Failed to write config values: {}", e.what()); } +} + +void raft_state_mgr::save_state(const nuraft::srv_state& state) { + auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); + auto json_obj = json{{"term", state.get_term()}, {"voted_for", state.get_voted_for()}}; + + try { + std::ofstream ostrm(state_file, std::ios::binary); + if (ostrm.is_open()) { ostrm << json_obj; } + } catch (std::exception& e) { LOGERROR("Failed to write config values: {}", e.what()); } +} + +uint32_t raft_state_mgr::get_logstore_id() const { return 0; } + +std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { + return std::make_shared< echo_state_machine >(); +} + +void raft_state_mgr::permanent_destroy() {} + +void raft_state_mgr::leave() {} + +} diff --git a/src/mem/raft/raft_state_manager.hpp b/src/mem/raft/raft_state_manager.hpp new file mode 100644 index 0000000..09843b8 --- /dev/null +++ b/src/mem/raft/raft_state_manager.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace craft { + +class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { +public: + raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id); + + nuraft::ptr< nuraft::cluster_config > load_config() override; + void save_config(const nuraft::cluster_config& config) override; + void save_state(const nuraft::srv_state& state) override; + nuraft::ptr< nuraft::srv_state > read_state() override; + nuraft::ptr< nuraft::log_store > load_log_store() override; + int32_t server_id() override { return _srv_id; } + + void system_exit(const int exit_code) override { LOGINFO("System exiting with code [{}]", exit_code); } + + uint32_t get_logstore_id() const override; + std::shared_ptr< nuraft::state_machine > get_state_machine() override; + void leave() override; + void permanent_destroy() override; + +private: + int32_t const _srv_id; + std::string const _srv_addr; + std::string const _group_id; +}; + +} diff --git a/src/mem/raft_state_mgr.hpp b/src/mem/raft_state_mgr.hpp deleted file mode 100644 index 9ef5cbf..0000000 --- a/src/mem/raft_state_mgr.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -namespace craft { - -class raft_state_machine : public nuraft::state_machine { -public: - raft_state_machine() = default; - - nuraft::ptr< nuraft::buffer > commit(const ulong, nuraft::buffer&) override { return nullptr; } - bool apply_snapshot(nuraft::snapshot&) override { return true; } - nuraft::ptr< nuraft::snapshot > last_snapshot() override { return nullptr; } - ulong last_commit_index() override { return 0; } - void create_snapshot(nuraft::snapshot&, nuraft::async_result< bool >::handler_type&) override {} -}; - -class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { -public: - raft_state_mgr(int32_t server_id, nuraft_mesg::group_id_t group_id, std::weak_ptr< nuraft_mesg::manager > weak_manager) : server_id_{server_id}, group_id_{std::move(group_id)}, weak_manager_{std::move(weak_manager)} {} - - nuraft::ptr< nuraft::cluster_config > load_config() override { return nullptr; } - void save_config(const nuraft::cluster_config&) override {} - void save_state(const nuraft::srv_state&) override {} - nuraft::ptr< nuraft::srv_state > read_state() override { return nullptr; } - nuraft::ptr< nuraft::log_store > load_log_store() override { - return nullptr; - } - int32_t server_id() override { return server_id_; } - void system_exit(const int) override {} - - uint32_t get_logstore_id() const override { return 0; } - std::shared_ptr< nuraft::state_machine > get_state_machine() override { - return std::make_shared< raft_state_machine >(); - } - void permanent_destroy() override {} - void leave() override {} - - bool bind_non_raft_service() { - auto mgr = weak_manager_.lock(); - if (!mgr) { - return false; - } - - if (!mgr->bind_data_service_request("GetRSCommitLSN", group_id_, [this](boost::intrusive_ptr< sisl::GenericRpcData >& rpc_data) { - LOGINFO("Received non raft service request for group_id={}", group_id_); - })) { - LOGERROR("Failed to bind non raft service request GetRSCommitLSN for group_id={}", group_id_); - return false; - } - - if (!mgr->bind_data_service_request("SyncRSCommitLSN", group_id_, [this](boost::intrusive_ptr< sisl::GenericRpcData >& rpc_data) { - LOGINFO("Received non raft service request for group_id={}", group_id_); - })) { - LOGERROR("Failed to bind non raft service request SyncRSCommitLSN for group_id={}", group_id_); - return false; - } - return true; - } - -private: - int32_t server_id_; - nuraft_mesg::group_id_t group_id_; - std::weak_ptr< nuraft_mesg::manager > weak_manager_; -}; - -} // namespace craft::net diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 06b6c97..9d2368c 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -15,7 +15,8 @@ #include "mem/replica.hpp" #include "mem/cluster.hpp" // the full MemTransport type -#include "mem/peer_comm.hpp" // for raft channel +#include "raft/raft_service.hpp" // for raft channel +#include "helper.hpp" #include #include @@ -24,7 +25,6 @@ #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares -#include namespace craft { @@ -50,16 +50,12 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } } // namespace -MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net, - std::optional< uint16_t > raft_port) : +MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : ep_{std::move(ep)}, page_size_{page_size}, net_{std::move(net)} { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); fault_retired_.push_back(std::move(initial)); - - // init raft channel (if raft_port is provided) - if (raft_port.has_value()) { peer_comm::instance()->init_raft_server(ep_.id, raft_port.value()); } } // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── @@ -220,7 +216,7 @@ async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring } async_result< lsn_pair > MemCraftReplica::get_lsns() { co_return do_lsns(); } -async_result< lsn_pair > MemCraftReplica::get_rs_commit_lsn() { co_return do_lsns(); } +async_result< lsn_pair > MemCraftReplica::get_rs_commit_lsn(uint64_t term, bool is_login) { co_return do_lsns(); } async_result< std::vector< JournalSlot > > MemCraftReplica::fetch_data(std::vector< int64_t > lsns) { co_return do_fetch(lsns); } @@ -288,6 +284,11 @@ result< lsn_pair > MemCraftReplica::do_keep_alive(client_hdr hdr) { } result< lsn_pair > MemCraftReplica::do_lsns() { + std::lock_guard< std::mutex > g{mu_}; + return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; +} + +result< lsn_pair > MemCraftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { if (net_ && !net_->is_up(ep_.id)) return fail(craft_error::REPLICA_DOWN); std::lock_guard< std::mutex > g{mu_}; return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; @@ -578,26 +579,7 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { // create peer raft group and add members to it. result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< wire::member > const& members) { - auto const group_id = craft::to_uuid(volume_id); - auto consensus = peer_comm::instance()->get_consensus(); - - // Seat THIS replica as leader by creating the group. - if (auto const status = craft::sync_get(consensus->create_group(group_id, peer_comm::default_group_type_)); - !status) { - return fail(craft_error::INTERNAL); - } - - // Add every OTHER member as a follower. - for (auto const& m : members) { - auto const member_id = craft::to_uuid(m.id); - if (member_id == ep_.id) continue; - - auto srv_cfg = nuraft::srv_config(nuraft_mesg::to_server_id(member_id), 0, m.addr, "", false); - if (auto const result = craft::sync_get(consensus->add_member(group_id, srv_cfg)); !result) { - return fail(craft_error::INTERNAL); - } - } - return {}; + return raft_service::instance()->srv_create_volume(volume_id, members); } } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 64b42a3..aaaa3cd 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -35,8 +35,7 @@ #include #include -#include // result types -#include +#include // result types #include "craft_peer.hpp" // the PEER plane: craft_peer + JournalSlot + lba_t (this model is its only implementer) #include "craft_replica.hpp" // the CLIENT plane: the craft_replica interface @@ -44,9 +43,11 @@ namespace craft { class MemTransport; // in-process network + cold path +namespace wire { +struct member; +} + using sisl::ok; -template < typename T > -using result = sisl::result< T >; using status = sisl::status; // Per-partition CRAFT state, internal to a replica implementation. Authoritative in memory; a production replica @@ -121,8 +122,7 @@ class MemCraftReplica final : public craft_replica, // How many Missing dLSNs stats() lists individually. The count is always exact. static constexpr std::size_t k_missing_sample = 16; - MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net, - std::optional< uint16_t > raft_port = std::nullopt); + MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); // Snapshot this replica's state. Takes mu_ and deliberately does NOT consult net_: do_write() locks // the transport before mu_, so reading net_ under mu_ here would invert that order. Callers that want @@ -170,7 +170,7 @@ class MemCraftReplica final : public craft_replica, // a friend and drives the cold_* / peek_* helpers below directly. Routing it through this interface is step // one of making the peer plane real; step two is allocating its opcodes (wire::op stops at 14). async_result< lsn_pair > get_lsns() override; - async_result< lsn_pair > get_rs_commit_lsn() override; + async_result< lsn_pair > get_rs_commit_lsn(uint64_t term, bool is_login) override; async_result< std::vector< JournalSlot > > fetch_data(std::vector< int64_t > lsns) override; async_status truncate(int64_t lsn) override; @@ -203,6 +203,10 @@ class MemCraftReplica final : public craft_replica, result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< wire::member > const& members); + result< lsn_pair > srv_get_rs_commit_lsn(uint64_t term, bool is_login) { + return do_get_rs_commit_lsn(term, is_login); + } + result< std::vector< JournalSlot > > srv_fetch_data(std::vector< int64_t > const& lsns) { return do_fetch(lsns); } private: friend class MemTransport; // the cold path drives the cold_* / peek helpers below directly, and the IO @@ -239,6 +243,7 @@ class MemCraftReplica final : public craft_replica, result< read_result > do_read(client_hdr hdr, int64_t read_lsn, uint64_t addr, uint64_t len, sisl::sg_list dest); result< lsn_pair > do_keep_alive(client_hdr hdr); result< lsn_pair > do_lsns(); + result< lsn_pair > do_get_rs_commit_lsn(uint64_t term, bool is_login); status do_truncate(int64_t lsn); result< std::vector< JournalSlot > > do_fetch(std::vector< int64_t > const& lsns); result< resolution_result > do_resolve_local(client_hdr hdr, int64_t upto); // N=1 resolution (srv seam) diff --git a/src/net/tcp_peer.cpp b/src/net/tcp_peer.cpp new file mode 100644 index 0000000..ae12331 --- /dev/null +++ b/src/net/tcp_peer.cpp @@ -0,0 +1,84 @@ +#include "net/tcp_peer.hpp" + +#include + +namespace craft::net { + +namespace { +auto fail(craft_error e) { return std::unexpected(make_error_condition(e)); } + +std::error_condition net_to_error(net_error e) { + // Same mapping as tcp_replica.cpp's net_to_error -- kept local for now since this file has no other + // reason to depend on tcp_replica.cpp. Worth factoring into a shared helper once both exist stably. + switch (e) { + case net_error::send: + case net_error::setup: + case net_error::connect: + return make_error_condition(craft_error::REPLICA_DOWN); + case net_error::recv: + case net_error::closed: + case net_error::malformed: + case net_error::timed_out: + return std::make_error_condition(std::errc::timed_out); + case net_error::invalid_argument: + return std::make_error_condition(std::errc::invalid_argument); + } + return make_error_condition(craft_error::REPLICA_DOWN); +} +} // namespace + +CraftTcpPeer::CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, std::chrono::milliseconds op_timeout) : + host_{std::move(host)}, port_{port}, id_{id}, op_timeout_{op_timeout} {} + +CraftTcpPeer::~CraftTcpPeer() = default; + +bool CraftTcpPeer::ensure_connected() { + if (connected_) return true; + auto c = craft_conn::connect(host_, port_, op_timeout_ > std::chrono::milliseconds{0} ? op_timeout_ + : k_connect_timeout); + if (!c) return false; + conn_ = std::move(*c); + connected_ = true; + return true; +} + +async_result< lsn_pair > CraftTcpPeer::get_rs_commit_lsn(uint64_t term, bool is_login) { + if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); + + wire::get_rs_commit_lsn_req req{}; + req.term = term; + req.is_login = is_login ? 1 : 0; + + std::vector< uint8_t > out; + wire::frame_message(out, wire::op::get_rs_commit_lsn, 0, next_request_id(), + {reinterpret_cast< uint8_t const* >(&req), sizeof(req)}, {}); + if (!conn_.send_all(out)) { + connected_ = false; + co_return fail(craft_error::REPLICA_DOWN); + } + + auto msg = conn_.recv_message(wire::k_default_max_tx); + if (!msg) { + connected_ = false; + co_return std::unexpected(net_to_error(msg.error())); + } + auto parsed = wire::parse_message(*msg, wire::k_default_max_tx); + if (!parsed) { + connected_ = false; + co_return fail(craft_error::INTERNAL); // malformed reply + } + auto const rsp = wire::decode< wire::get_rs_commit_lsn_rsp >(parsed->op_header); + if (static_cast< wire::status >(parsed->hdr.status) != wire::status::ok) { + co_return fail(craft_error::INTERNAL); // TODO: proper status_to_error mapping, mirrors tcp_replica.cpp + } + co_return lsn_pair{rsp.commit_lsn, rsp.last_append_lsn}; +} + +// ── existing craft_peer methods: not yet implemented over the wire (no opcodes allocated for these yet) ── +async_result< lsn_pair > CraftTcpPeer::get_lsns() { co_return fail(craft_error::INTERNAL); } +async_result< std::vector< JournalSlot > > CraftTcpPeer::fetch_data(std::vector< int64_t > lsns) { + co_return fail(craft_error::INTERNAL); +} +async_status CraftTcpPeer::truncate(int64_t lsn) { co_return fail(craft_error::INTERNAL); } + +} // namespace craft::net \ No newline at end of file diff --git a/src/net/tcp_peer.hpp b/src/net/tcp_peer.hpp new file mode 100644 index 0000000..c4c67e3 --- /dev/null +++ b/src/net/tcp_peer.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include // craft_conn -- raw transport, NOT wire_client (that's client-plane only) +#include + +#include "craft_peer.hpp" + +namespace craft::net { + +class CraftTcpPeer final : public craft_peer { +public: + CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, + std::chrono::milliseconds op_timeout = std::chrono::milliseconds{0}); + ~CraftTcpPeer() override; + + CraftTcpPeer(CraftTcpPeer const&) = delete; + CraftTcpPeer& operator=(CraftTcpPeer const&) = delete; + + // ── craft_peer: existing (unchanged) ── + async_result< lsn_pair > get_lsns() override; + virtual async_result< lsn_pair > get_rs_commit_lsn(uint64_t term, bool is_login) override; + async_result< std::vector< JournalSlot > > fetch_data(std::vector< int64_t > lsns) override; + async_status truncate(int64_t lsn) override; + + // ── NEW, not yet on craft_peer.hpp: Login's Phase 1 poll specifically -- carries the caller's own + // watermarks + the quiesce flag. Add to craft_peer.hpp as a pure virtual once this compiles standalone, + // at which point MemCraftReplica also needs a stub implementation. + async_result< lsn_pair > login_poll(uint64_t term, bool is_login, int64_t my_commit, int64_t my_append); + +private: + bool ensure_connected(); + uint16_t next_request_id() { return next_rid_++; } + + std::string host_; + uint16_t port_; + peer_id_t id_; + std::chrono::milliseconds op_timeout_{0}; + + craft_conn conn_; + bool connected_{false}; + uint16_t next_rid_{1}; +}; + +} // namespace craft::net \ No newline at end of file diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index b3ab8db..bfbbe41 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -26,6 +26,8 @@ #include "mem/replica.hpp" // the full MemCraftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) #include // to_wire_status (the shared wire <-> craft_error bridge) +#include "mem/raft/raft_service.hpp" +#include "replica_mgr.hpp" namespace craft::net { @@ -36,20 +38,21 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(server_geometry geo, std::optional< uint16_t > raft_port) : geo_{std::move(geo)} { +craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : geo_{std::move(geo)} { replica_endpoint ep; if (!geo_.members.empty()) { std::copy(geo_.members[0].id.begin(), geo_.members[0].id.end(), ep.id.begin()); // wire id[16] -> uuid ep.addr = geo_.members[0].addr; } // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). - replica_ = std::make_shared< MemCraftReplica >(std::move(ep), geo_.lba_size, nullptr, raft_port); + // start replica service and raft service if server_config_file is provided + if (!server_config_file.empty()) { + replica_manager::instance()->start_replica_service(server_config_file); + raft_service::instance()->start_raft_service(ep.id); + } + replica_ = std::make_shared< MemCraftReplica >(std::move(ep), geo_.lba_size, nullptr); } -craft_tcp_server::craft_tcp_server(server_geometry geo) : craft_tcp_server(std::move(geo), std::nullopt) {} -craft_tcp_server::craft_tcp_server(server_geometry geo, uint16_t raft_port) : - craft_tcp_server(std::move(geo), std::optional< uint16_t >(raft_port)) {} - craft_tcp_server::~craft_tcp_server() = default; void craft_tcp_server::log_stats() const { @@ -98,6 +101,12 @@ void craft_tcp_server::serve(craft_conn conn) { case wire::op::create_volume: on_create_volume(conn, *parsed); break; + case wire::op::get_rs_commit_lsn: + on_get_rs_commit_lsn(conn, *parsed); + break; + case wire::op::fetch_data: + on_fetch_data(conn, *parsed); + break; default: return; // a client sends only request ops we serve; anything else resets the connection } @@ -303,7 +312,7 @@ void craft_tcp_server::on_keep_alive(craft_conn& conn, wire::message const& req) void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& req) { auto const cr = wire::decode< wire::volume_create_req >(req.op_header); auto const members = wire::decode_members(req.body, cr.member_count); - + LOGINFO("craft_srv CREATE_VOLUME [rid:{}] member_count={}", req.hdr.request_id, cr.member_count); wire::status code = wire::status::ok; if (!members) { code = wire::status::invalid_argument; // body shorter than member_count implies -- malformed request @@ -320,4 +329,71 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r conn.send_all(out); } +void craft_tcp_server::on_get_rs_commit_lsn(craft_conn& conn, wire::message const& req) { + auto const gr = wire::decode< wire::get_rs_commit_lsn_req >(req.op_header); + wire::get_rs_commit_lsn_rsp rsp{-1, -1}; + wire::status code = wire::status::ok; + + auto const r = replica_->srv_get_rs_commit_lsn(gr.term, gr.is_login != 0); + if (!r) { + code = to_wire_status(r.error()); + } else { + rsp.commit_lsn = r->commit_lsn; + rsp.last_append_lsn = r->last_append_lsn; + } + + LOGINFO("craft_srv GET_RS_COMMIT_LSN [rid:{}] term={} is_login={} status={} commit_lsn={} last_append_lsn={}", + req.hdr.request_id, gr.term, gr.is_login, static_cast< int >(code), rsp.commit_lsn, rsp.last_append_lsn); + + std::vector< uint8_t > out; + wire::frame_message(out, wire::op::get_rs_commit_lsn_rsp, static_cast< uint8_t >(code), req.hdr.request_id, + as_bytes(rsp), {}); + conn.send_all(out); +} + +void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) { + auto const fr = wire::decode< wire::fetch_data_req >(req.op_header); + auto const lsns = wire::decode_lsns(req.body, fr.lsn_count); + + wire::fetch_data_rsp rsp{}; + std::vector< uint8_t > body; + wire::status code = wire::status::ok; + + if (!lsns) { + code = wire::status::invalid_argument; // body shorter than lsn_count implies -- malformed request + } else { + auto const r = replica_->srv_fetch_data(*lsns); + if (!r) { + code = to_wire_status(r.error()); + } else { + rsp.slot_count = static_cast< uint32_t >(r->size()); + // descriptors first (fixed-size, easy to walk), then concatenated data for non-empty/non-zero slots + for (auto const& slot : *r) { + wire::fetch_slot_desc sd{}; + sd.lsn = slot.lsn; + sd.lba = slot.lba; + sd.len = slot.len; + sd.is_empty = slot.is_empty ? 1 : 0; + sd.all_zeros = slot.all_zeros ? 1 : 0; + wire::put(body, sd); + } + for (auto const& slot : *r) { + if (slot.is_empty || slot.all_zeros) continue; + for (auto const& iov : slot.data.iovs) { + auto const* p = static_cast< uint8_t const* >(iov.iov_base); + body.insert(body.end(), p, p + iov.iov_len); + } + } + } + } + + LOGTRACE("craft_srv FETCH_DATA [rid:{}] requested={} status={} returned={}", req.hdr.request_id, fr.lsn_count, + static_cast< int >(code), rsp.slot_count); + + std::vector< uint8_t > out; + wire::frame_message(out, wire::op::fetch_data_rsp, static_cast< uint8_t >(code), req.hdr.request_id, + {reinterpret_cast< uint8_t const* >(&rsp), sizeof(rsp)}, body); + conn.send_all(out); +} + } // namespace craft::net diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index 77489e3..aa8fbf0 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -48,8 +48,7 @@ struct server_geometry { class craft_tcp_server { public: - explicit craft_tcp_server(server_geometry geo); - explicit craft_tcp_server(server_geometry geo, uint16_t raft_port); + explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}); ~craft_tcp_server(); craft_tcp_server(craft_tcp_server&&) = default; craft_tcp_server& operator=(craft_tcp_server&&) = default; @@ -71,8 +70,6 @@ class craft_tcp_server { uint64_t session_term_ = 0; // the current session's term, stamped on every IO bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced - craft_tcp_server(server_geometry geo, std::optional< uint16_t > raft_port); - void on_login(craft_conn&, wire::message const&); void on_helo(craft_conn&, wire::message const&); void on_logout(craft_conn&, wire::message const&); @@ -81,6 +78,8 @@ class craft_tcp_server { void on_keep_alive(craft_conn&, wire::message const&); void on_resolve(craft_conn&, wire::message const&); void on_create_volume(craft_conn&, wire::message const&); + void on_get_rs_commit_lsn(craft_conn& conn, wire::message const& req); + void on_fetch_data(craft_conn& conn, wire::message const& req); }; } // namespace craft::net diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp new file mode 100644 index 0000000..b209c9d --- /dev/null +++ b/src/replica_mgr.cpp @@ -0,0 +1,68 @@ +#include "replica_mgr.hpp" + +#include + +#include +#include +#include + +namespace craft { + +std::shared_ptr< replica_manager > replica_manager::instance() { + static std::shared_ptr< replica_manager > inst{new replica_manager()}; + return inst; +} + +void replica_manager::start_replica_service(std::string const& path) { + std::ifstream istrm(path, std::ios::binary); + if (!istrm.is_open()) { + LOGERROR("replica_manager: could not open {}", path); + return; + } + + nlohmann::json j; + try { + istrm >> j; + } catch (nlohmann::json::parse_error const& e) { + LOGERROR("replica_manager: parse error in {}: {}", path, e.what()); + return; + } + + replicas_.clear(); + for (auto const& m : j.at("members")) { + replica_info info; + info.id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); + info.host = m.at("host").get< std::string >(); + info.raft_port = m.at("raft_port").get< uint16_t >(); + info.tcp_port = m.at("tcp_port").get< uint16_t >(); + replicas_[info.id] = std::move(info); + } +} + +std::string replica_manager::lookup_peer(boost::uuids::uuid const& id) const { + std::lock_guard< std::mutex > g{mu_}; + auto const it = replicas_.find(id); + if (it == replicas_.end()) return {}; + return it->second.host + ":" + std::to_string(it->second.raft_port); +} + +std::shared_ptr< net::CraftTcpPeer > replica_manager::get_peer_client(boost::uuids::uuid const& id) { + std::lock_guard< std::mutex > g{mu_}; + if (auto it = peer_clients_.find(id); it != peer_clients_.end()) return it->second; + + auto const rit = replicas_.find(id); + if (rit == replicas_.end()) return nullptr; + + auto client = std::make_shared< net::CraftTcpPeer >(rit->second.host, rit->second.tcp_port, id); + peer_clients_[id] = client; + return client; +} + +std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) const { + std::lock_guard< std::mutex > g{mu_}; + auto const it = replicas_.find(id); + if (it == replicas_.end()) return std::nullopt; + return it->second; +} + +} // namespace craft \ No newline at end of file diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp new file mode 100644 index 0000000..177f65d --- /dev/null +++ b/src/replica_mgr.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include // peer_id_t +#include "net/tcp_peer.hpp" + +namespace craft { + +// One member's full identity + reachability, everything replica_manager needs to answer both raft's +// lookup_peer and the wire-plane peer client from a single source of truth. +struct replica_info { + boost::uuids::uuid id{}; + std::string host; + uint16_t raft_port{0}; + uint16_t tcp_port{0}; +}; + +// Process-wide registry: peer identity -> reachability, and (lazily) the open peer-plane connection to it. +// Replaces peer_comm::peer_lookup_map_ and net::peer_client_service's addrs_/peers_ split -- one map for +// static info (loaded once, rarely mutated), one for live connections (grown lazily, per actual use). +class replica_manager { +public: + static std::shared_ptr< replica_manager > instance(); + + void start_replica_service(std::string const& path); + + // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". + std::string lookup_peer(boost::uuids::uuid const& id) const; + + // wire-plane peer client: lazily connect-and-cache a CraftTcpPeer for this id. + std::shared_ptr< net::CraftTcpPeer > get_peer_client(boost::uuids::uuid const& id); + + std::optional< replica_info > get(boost::uuids::uuid const& id) const; + +private: + replica_manager() = default; + + mutable std::mutex mu_; + std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once + std::map< boost::uuids::uuid, std::shared_ptr< net::CraftTcpPeer > > peer_clients_; // lazy, grows on use +}; + +} // namespace craft \ No newline at end of file diff --git a/src/wire.cpp b/src/wire.cpp index d3a5e90..f0740a2 100644 --- a/src/wire.cpp +++ b/src/wire.cpp @@ -22,23 +22,27 @@ std::optional< std::size_t > op_hdr_size(uint8_t op_code) noexcept { // A peer-plane op (15+, see craft_peer.hpp) must add its row here AND bump op::k_max_op -- the static_assert // below makes forgetting either one a compile error rather than an unknown_op at runtime. static constexpr std::size_t k[] = { - 0, // 0 unused - sizeof(login_req), // 1 login - sizeof(login_rsp), // 2 login_rsp - sizeof(helo_req), // 3 helo - 0, // 4 helo_rsp (status only) - sizeof(write_req), // 5 write - sizeof(write_rsp), // 6 write_rsp - sizeof(read_req), // 7 read - sizeof(read_rsp), // 8 read_rsp - sizeof(keepalive_req), // 9 keepalive - sizeof(keepalive_rsp), // 10 keepalive_rsp - sizeof(logout_req), // 11 logout - 0, // 12 logout_rsp (status only) - sizeof(resolve_req), // 13 resolve - sizeof(resolve_rsp), // 14 resolve_rsp - sizeof(volume_create_req), // 15 create_volume - 0, // 16 create_volume_rsp (status only) + 0, // 0 unused + sizeof(login_req), // 1 login + sizeof(login_rsp), // 2 login_rsp + sizeof(helo_req), // 3 helo + 0, // 4 helo_rsp (status only) + sizeof(write_req), // 5 write + sizeof(write_rsp), // 6 write_rsp + sizeof(read_req), // 7 read + sizeof(read_rsp), // 8 read_rsp + sizeof(keepalive_req), // 9 keepalive + sizeof(keepalive_rsp), // 10 keepalive_rsp + sizeof(logout_req), // 11 logout + 0, // 12 logout_rsp (status only) + sizeof(resolve_req), // 13 resolve + sizeof(resolve_rsp), // 14 resolve_rsp + sizeof(volume_create_req), // 15 create_volume + 0, // 16 create_volume_rsp (status only) + sizeof(get_rs_commit_lsn_req), // 17 get_rs_commit_lsn + sizeof(get_rs_commit_lsn_rsp), // 18 get_rs_commit_lsn_rsp + sizeof(fetch_data_req), // 19 fetch_data + sizeof(fetch_data_rsp), // 20 fetch_data_rsp }; static_assert(std::size(k) == static_cast< std::size_t >(op::k_max_op) + 1, "op_hdr_size table and op::k_max_op disagree -- a new opcode was added without a header size, " @@ -54,10 +58,11 @@ bool is_response(uint8_t op_code) noexcept { } bool op_allows_body(uint8_t op_code) noexcept { - return op_code == static_cast< uint8_t >(op::write) || // data - op_code == static_cast< uint8_t >(op::login_rsp) || // member list - op_code == static_cast< uint8_t >(op::read_rsp) || // extents + data - op_code == static_cast< uint8_t >(op::resolve_rsp); // Empty-verdict dLSN list + return op_code == static_cast< uint8_t >(op::write) || // data + op_code == static_cast< uint8_t >(op::login_rsp) || // member list + op_code == static_cast< uint8_t >(op::read_rsp) || // extents + data + op_code == static_cast< uint8_t >(op::resolve_rsp) || // Empty-verdict dLSN list + op_code == static_cast< uint8_t >(op::create_volume); // member list } namespace { diff --git a/test/test_async_tcp.cpp b/test/test_async_tcp.cpp index 1522224..6ca7747 100644 --- a/test/test_async_tcp.cpp +++ b/test/test_async_tcp.cpp @@ -45,8 +45,6 @@ using namespace craft; using namespace craft::test; using sisl::ok; -template < typename T > -using result = sisl::result< T >; using status = sisl::status; namespace { diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index dc42faa..847996a 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -294,45 +294,3 @@ TEST(CraftTcp, ConnectDeadlineBoundsABlackholedPeer) { EXPECT_FALSE(c.has_value()); EXPECT_LT(elapsed, std::chrono::seconds{5}) << "connect must fail at the deadline, not the SYN-retry window"; } - -TEST(CraftTcp, ServerWithRaft) { - auto lst = craft_listener::bind_listen(0); - ASSERT_TRUE(lst.has_value()); - uint16_t const port = lst->port(); - - craft_tcp_server server{make_geo(), 6666}; - std::jthread srv([&] { - auto conn = lst->accept(); - if (conn) server.serve(std::move(*conn)); - }); - - { - auto cli = wire_client::connect("127.0.0.1", port); - ASSERT_TRUE(cli.has_value()); - - auto lr = cli->login(/*volume_id=*/{}, 0xABCD); - ASSERT_TRUE(lr.has_value()); - EXPECT_EQ(lr->term, 1u); - EXPECT_EQ(lr->capacity, uint64_t{1} << 30); - EXPECT_EQ(lr->lba_size, k_lba); - EXPECT_EQ(lr->max_tx, 512u * 1024); - // The login WATERMARK: the LAST dLSN already durable, which on a fresh replica is -1. NOT the next dLSN - // to use -- the client derives that itself (next_dlsn_ = dlsn + 1). This assertion used to read `0` (and - // the server used to send last_append_lsn + 1 to match), which meant the client started at dLSN 1 and - // slot 0 was never written: every replica sat permanently Missing dLSN 0, apply_up_to() stalled there, - // and commit_lsn pinned at -1 forever. Reads still served correct bytes off the journal-tail overlay, so - // nothing failed -- it only showed up as a read walking the entire journal tail. Hence the guard below. - EXPECT_EQ(lr->dlsn, -1); - ASSERT_EQ(lr->members.size(), 1u); - EXPECT_EQ(lr->members[0].addr, "127.0.0.1:0"); - EXPECT_EQ(lr->members[0].id[0], 0x01); - - auto lo = cli->logout(); - ASSERT_TRUE(lo.has_value()); - EXPECT_EQ(*lo, wire::status::ok); - - auto lo2 = cli->logout(); // the session is gone -> fenced - ASSERT_TRUE(lo2.has_value()); - EXPECT_EQ(*lo2, wire::status::stale_term); - } -} diff --git a/test/test_wire.cpp b/test/test_wire.cpp index cac68a8..832a8a7 100644 --- a/test/test_wire.cpp +++ b/test/test_wire.cpp @@ -72,10 +72,14 @@ TEST(CraftWire, OpHeaderSizes) { EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::logout)), 16u); EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::resolve)), 24u); EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::resolve_rsp)), 16u); - EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::create_volume)), 24u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::create_volume)), 32u); EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::create_volume_rsp)), 0u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::get_rs_commit_lsn)), 16u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::get_rs_commit_lsn_rsp)), 16u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::fetch_data)), 8u); + EXPECT_EQ(op_hdr_size(static_cast< uint8_t >(op::fetch_data_rsp)), 8u); EXPECT_FALSE(op_hdr_size(0).has_value()); - EXPECT_FALSE(op_hdr_size(17).has_value()); + EXPECT_FALSE(op_hdr_size(21).has_value()); EXPECT_FALSE(op_hdr_size(99).has_value()); EXPECT_TRUE(is_response(static_cast< uint8_t >(op::write_rsp))); diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 5c67885..4c99176 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -45,6 +46,10 @@ SISL_OPTION_GROUP(craft_srv, (port, "", "port", "Listen port (required)", ::cxxopts::value< uint16_t >()->default_value("0"), ""), + (server_uuid, "", "server_uuid", "Server UUID (optional; random if not provided)", + ::cxxopts::value< std::string >(), ""), + (server_config_file, "", "server_config_file", "Server configuration json (optional)", + ::cxxopts::value< std::string >(), ""), (capacity, "", "capacity", "Volume capacity in bytes (default 1 GiB)", ::cxxopts::value< uint64_t >()->default_value("0"), ""), (lba_size, "", "lba_size", "Block size in bytes", @@ -55,7 +60,7 @@ SISL_OPTION_GROUP(craft_srv, #define SRV_OPTIONS logging, craft_srv SISL_OPTIONS_ENABLE(SRV_OPTIONS) SISL_LOGGING_DEF(craft) // DEFINE the module (INIT alone only references it -> "undefined symbol module_level_craft") -SISL_LOGGING_INIT(craft) // register it for level control +SISL_LOGGING_INIT(craft, nuraft_mesg, grpc_server) // register it for level control namespace { std::atomic< bool > g_stop{false}; @@ -65,6 +70,8 @@ void on_signal(int) { g_stop.store(true); } int main(int argc, char** argv) { SISL_OPTIONS_LOAD(argc, argv, SRV_OPTIONS); sisl::logging::SetLogger("craft_reference_tcp_srv"); + sisl::logging::SetModuleLogLevel("nuraft_mesg", spdlog::level::info); + sisl::logging::SetModuleLogLevel("grpc_server", spdlog::level::info); auto const port = SISL_OPTIONS["port"].as< uint16_t >(); if (port == 0) { @@ -84,7 +91,15 @@ int main(int argc, char** argv) { geo.lba_size = lba_size; geo.max_tx = max_tx; craft::wire::member self{}; - auto const id = boost::uuids::random_generator()(); + auto id = boost::uuids::random_generator()(); + if (SISL_OPTIONS.count("server_uuid")) { + try { + id = boost::uuids::string_generator()(SISL_OPTIONS["server_uuid"].as< std::string >()); + } catch (std::exception const& e) { + std::cerr << "Invalid --server_uuid: " << e.what() << "\n"; + return 2; + } + } std::copy(id.begin(), id.end(), self.id.begin()); self.addr = "127.0.0.1:" + std::to_string(port); geo.members.push_back(self); @@ -97,7 +112,12 @@ int main(int argc, char** argv) { // The server (replica + session state) is shared across connections: the client keeps a login connection AND // an on-ring data connection open at once, and they must see the same session. Session state races are benign // -- login and the data HELO both set the same term -- so a plain shared instance is fine for the reference. - craft::net::craft_tcp_server server{std::move(geo)}; + // use peer comm port if provided + std::string server_config_file{}; + if (SISL_OPTIONS.count("server_config_file")) { + server_config_file = SISL_OPTIONS["server_config_file"].as< std::string >(); + } + craft::net::craft_tcp_server server{std::move(geo), server_config_file}; // sigaction WITHOUT SA_RESTART: glibc's signal() sets SA_RESTART, which auto-restarts the blocking accept() // after the handler runs, so the loop would never re-check g_stop and Ctrl-C could not stop the server. With diff --git a/tools/raft_bootstrap.py b/tools/raft_bootstrap.py new file mode 100644 index 0000000..1762632 --- /dev/null +++ b/tools/raft_bootstrap.py @@ -0,0 +1,93 @@ +import socket +import struct +import sys +import uuid + +# msg_hdr: op(u8) status(u8) request_id(u16) body_len(u32) -- 8 bytes, little-endian +MSG_HDR = " bytes: + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError( + f"peer closed after {len(buf)}/{n} bytes -- likely a server-side parse_message() " + f"rejection (unknown_op / bad_length / bad_digest);" + ) + buf += chunk + return bytes(buf) + + +def put_member(member_id: bytes, addr: str) -> bytes: + if len(member_id) != 16: + raise ValueError(f"member id must be 16 bytes, got {len(member_id)}") + addr_b = addr.encode() + return member_id + struct.pack(" int: + if len(volume_id) != 16: + raise ValueError(f"volume_id must be 16 bytes, got {len(volume_id)}") + + body = b"".join(put_member(mid, addr) for mid, addr in members) + op_header = struct.pack(VOLUME_CREATE_REQ, volume_id, capacity, lba_size, len(members)) + body_len = len(body) + hdr = struct.pack(MSG_HDR, OP_CREATE_VOLUME, 0, 1, body_len) + + try: + s = socket.create_connection((host, port), timeout=timeout) + except OSError as e: + raise ConnectionError(f"could not connect to {host}:{port}: {e}") from e + + try: + s.sendall(hdr + op_header + body) + resp_hdr = recv_exact(s, MSG_HDR_SIZE) + op, status, request_id, resp_body_len = struct.unpack(MSG_HDR, resp_hdr) + if resp_body_len: + recv_exact(s, resp_body_len) + return status + finally: + s.close() + + +if __name__ == "__main__": + volume_id = (1).to_bytes(16, "little") + + #member_ids = [uuid.uuid4() for _ in range(3)] + # Fixed for debugging -- swap back to uuid.uuid4() once the flow is finalized. + member_ids = [ + uuid.UUID("11111111-1111-1111-1111-111111111111"), + uuid.UUID("22222222-2222-2222-2222-222222222222"), + uuid.UUID("33333333-3333-3333-3333-333333333333"), + ] + addrs = ["127.0.0.1:8001", "127.0.0.1:8002", "127.0.0.1:8003"] + members = [(mid.bytes, addr) for mid, addr in zip(member_ids, addrs)] + + for mid, addr in zip(member_ids, addrs): + print(f"member {mid} @ {addr}") + + try: + status = create_volume("127.0.0.1", 7001, volume_id, capacity=1 << 30, + lba_size=4096, members=members) + except (ConnectionError, ValueError, socket.timeout) as e: + print(f"create_volume failed: {e}", file=sys.stderr) + sys.exit(1) + + name = STATUS_NAMES.get(status, f"unknown({status})") + print(f"status: {status} ({name})") + sys.exit(0 if status == 0 else 1) \ No newline at end of file diff --git a/tools/server_config.json b/tools/server_config.json new file mode 100644 index 0000000..ed82af6 --- /dev/null +++ b/tools/server_config.json @@ -0,0 +1,22 @@ +{ + "members": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "host": "127.0.0.1", + "raft_port": 8001, + "tcp_port": 7001 + }, + { + "uuid": "22222222-2222-2222-2222-222222222222", + "host": "127.0.0.1", + "raft_port": 8002, + "tcp_port": 7002 + }, + { + "uuid": "33333333-3333-3333-3333-333333333333", + "host": "127.0.0.1", + "raft_port": 8003, + "tcp_port": 7003 + } + ] +} \ No newline at end of file From de7d47b053af797685f57c4fbfe7a29ff35a8f99 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 22 Jul 2026 02:56:39 -0700 Subject: [PATCH 03/24] add true login logic in replica. Remove non tcp code from tcp_server --- CMakeLists.txt | 6 +- src/{mem => }/helper.hpp | 8 +- src/mem/replica.cpp | 88 ++++++++++++++++------ src/mem/replica.hpp | 35 ++++++--- src/net/tcp_server.cpp | 72 ++++++++---------- src/net/tcp_server.hpp | 13 +--- src/{mem => }/raft/in_memory_log_store.cpp | 0 src/{mem => }/raft/in_memory_log_store.hpp | 0 src/{mem => }/raft/raft_service.cpp | 46 ++++++++--- src/{mem => }/raft/raft_service.hpp | 11 ++- src/{mem => }/raft/raft_state_machine.hpp | 0 src/{mem => }/raft/raft_state_manager.cpp | 2 +- src/{mem => }/raft/raft_state_manager.hpp | 0 src/replica_mgr.cpp | 33 ++++++-- src/replica_mgr.hpp | 9 ++- test/test_tcp.cpp | 21 ++---- tools/craft_reference_tcp_srv.cpp | 39 +++++----- 17 files changed, 234 insertions(+), 149 deletions(-) rename src/{mem => }/helper.hpp (84%) rename src/{mem => }/raft/in_memory_log_store.cpp (100%) rename src/{mem => }/raft/in_memory_log_store.hpp (100%) rename src/{mem => }/raft/raft_service.cpp (62%) rename src/{mem => }/raft/raft_service.hpp (76%) rename src/{mem => }/raft/raft_state_machine.hpp (100%) rename src/{mem => }/raft/raft_state_manager.cpp (99%) rename src/{mem => }/raft/raft_state_manager.hpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34af253..2fb708d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,9 +68,9 @@ target_compile_features(craft_replica_mgr PUBLIC cxx_std_23) # ── craft_reference: the in-memory reference model + loopback cluster server (test-support only) ── add_library(raft_service STATIC - src/mem/raft/raft_service.cpp - src/mem/raft/in_memory_log_store.cpp - src/mem/raft/raft_state_manager.cpp) + src/raft/raft_service.cpp + src/raft/in_memory_log_store.cpp + src/raft/raft_state_manager.cpp) target_include_directories(raft_service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(raft_service PUBLIC nuraft_mesg::proto craft_replica_mgr) diff --git a/src/mem/helper.hpp b/src/helper.hpp similarity index 84% rename from src/mem/helper.hpp rename to src/helper.hpp index 0a873b6..99c2626 100644 --- a/src/mem/helper.hpp +++ b/src/helper.hpp @@ -1,6 +1,6 @@ #include #include - +#include namespace craft { @@ -12,6 +12,12 @@ inline boost::uuids::uuid to_uuid(std::array< uint8_t, 16 > const& arr) { return u; } +inline std::array< uint8_t, 16 > to_array(boost::uuids::uuid const& id) { + std::array< uint8_t, 16 > arr{}; + std::copy(id.begin(), id.end(), arr.begin()); + return arr; +} + // make sync coro calls, taken from homestore template < typename Task > inline auto sync_get(Task&& task) { diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 9d2368c..bc778db 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -16,7 +16,9 @@ #include "mem/replica.hpp" #include "mem/cluster.hpp" // the full MemTransport type #include "raft/raft_service.hpp" // for raft channel +#include "replica_mgr.hpp" #include "helper.hpp" +#include "craft/types.hpp" #include #include @@ -50,14 +52,20 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } } // namespace -MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : - ep_{std::move(ep)}, page_size_{page_size}, net_{std::move(net)} { +void MemCraftReplica::init_faults() { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); fault_retired_.push_back(std::move(initial)); } +MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : + geo_{.lba_size = page_size, .ep = std::move(ep)}, net_{std::move(net)} { + init_faults(); +} + +MemCraftReplica::MemCraftReplica(server_geometry geo) : geo_{std::move(geo)} { init_faults(); } + // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── template < class Fn > @@ -231,7 +239,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 std::shared_ptr< std::vector< uint8_t > > bytes) { // Deliverability is the transport's verdict, not ours: by the time we are called, the request arrived. // byte-based API: addr/len must be block-aligned (the model works in page_size blocks internally). - if (addr % page_size_ != 0 || len % page_size_ != 0 || len == 0) { + if (addr % geo_.lba_size != 0 || len % geo_.lba_size != 0 || len == 0) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } if (bytes && (bytes->size() != len)) { @@ -248,8 +256,8 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 MemJournalSlot slot; slot.term = hdr.term; - slot.lba = addr / page_size_; // byte offset -> block index - slot.len = static_cast< lba_count_t >(len / page_size_); // byte length -> block count + slot.lba = addr / geo_.lba_size; // byte offset -> block index + slot.len = static_cast< lba_count_t >(len / geo_.lba_size); // byte length -> block count slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again journal_[dlsn] = std::move(slot); @@ -262,7 +270,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 result< read_result > MemCraftReplica::do_read(client_hdr hdr, int64_t read_lsn, uint64_t addr, uint64_t len, sisl::sg_list dest) { // byte-based API: addr/len block-aligned; dest is a single contiguous buffer covering [addr,addr+len) - if (addr % page_size_ != 0 || len % page_size_ != 0 || len == 0) { + if (addr % geo_.lba_size != 0 || len % geo_.lba_size != 0 || len == 0) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } if (dest.size < len) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } @@ -289,7 +297,7 @@ result< lsn_pair > MemCraftReplica::do_lsns() { } result< lsn_pair > MemCraftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { - if (net_ && !net_->is_up(ep_.id)) return fail(craft_error::REPLICA_DOWN); + if (net_ && !net_->is_up(geo_.ep.id)) return fail(craft_error::REPLICA_DOWN); std::lock_guard< std::mutex > g{mu_}; return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; } @@ -357,7 +365,7 @@ void MemCraftReplica::apply_slot(int64_t dlsn, MemJournalSlot const& s) { } } else { for (lba_count_t i = 0; i < s.len; ++i) { - index_[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * page_size_}; + index_[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * geo_.lba_size}; } } } @@ -388,11 +396,11 @@ MemCraftReplica::MemJournalSlot const* MemCraftReplica::highest_slot_le(lba_t x, std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, uint64_t len, sisl::sg_list const& dest) { - lba_t const lba0 = addr / page_size_; - lba_count_t const nblk = static_cast< lba_count_t >(len / page_size_); + lba_t const lba0 = addr / geo_.lba_size; + lba_count_t const nblk = static_cast< lba_count_t >(len / geo_.lba_size); std::vector< io_extent > layout; - // Scatter writer: advances through dest's iovecs sequentially, one page_size_ chunk at a time. + // Scatter writer: advances through dest's iovecs sequentially, one geo_.lba_size chunk at a time. std::size_t iov_idx{0}, iov_off{0}; auto sg_write = [&](uint8_t const* src, std::size_t n) { while (n > 0 && iov_idx < dest.iovs.size()) { @@ -423,7 +431,7 @@ std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, u bool hole = true; if (auto* s = highest_slot_le(x, H)) { if (!s->all_zeros) { - page = s->bytes->data() + static_cast< std::size_t >(x - s->lba) * page_size_; + page = s->bytes->data() + static_cast< std::size_t >(x - s->lba) * geo_.lba_size; hole = false; } // else: zero write => hole } else if (auto it = index_.find(x); it != index_.end()) { @@ -431,17 +439,17 @@ std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, u hole = false; } // read-time scan: an all-zero data page reads back thin (as a hole). - if (!hole && all_zero(page, page_size_)) hole = true; + if (!hole && all_zero(page, geo_.lba_size)) hole = true; // fill the caller's scatter-gather buffer: data pages get bytes, holes get zeros. - sg_write(hole ? nullptr : page, page_size_); + sg_write(hole ? nullptr : page, geo_.lba_size); // coalesce the returned layout, in BYTES, with the previous extent if contiguous. - uint64_t const x_addr = static_cast< uint64_t >(x) * page_size_; + uint64_t const x_addr = static_cast< uint64_t >(x) * geo_.lba_size; if (!layout.empty() && layout.back().hole == hole && layout.back().addr + layout.back().len == x_addr) { - layout.back().len += page_size_; + layout.back().len += geo_.lba_size; } else { - layout.push_back(io_extent{x_addr, page_size_, hole}); + layout.push_back(io_extent{x_addr, geo_.lba_size, hole}); } } return layout; @@ -453,9 +461,9 @@ replica_stats MemCraftReplica::stats() const { std::lock_guard< std::mutex > g{mu_}; replica_stats s; - s.id = ep_.id; - s.addr = ep_.addr; - s.page_size = page_size_; + s.id = geo_.ep.id; + s.addr = geo_.ep.addr; + s.page_size = geo_.lba_size; s.commit_lsn = state_.commit_lsn; s.last_append_lsn = state_.last_append_lsn; s.term = state_.term; @@ -473,7 +481,7 @@ replica_stats MemCraftReplica::stats() const { } else if (slot.all_zeros) { ++s.zero_write_slots; } else { - s.journal_data_bytes += static_cast< uint64_t >(slot.len) * page_size_; + s.journal_data_bytes += static_cast< uint64_t >(slot.len) * geo_.lba_size; } } @@ -532,11 +540,39 @@ void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { // ── peer comm hooks (driven by raft) ── -void MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term) { +result< login_establish_result > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, + uint64_t client_token) { // place holder - cold_apply_login(client_token, term); // Phase 1: collect replica LSN state (non-RAFT broadcast) + // 1.1: accepted by leader only. + // TODO: what happens if the leader changes before the login is complete? auto vol_uuid = craft::to_uuid(volume_id); + if (!raft_service::instance()->is_leader(vol_uuid)) { + return std::unexpected(make_error_condition(craft_error::NOT_LEADER)); + } + + // 1.2 collect replica LSN state (non-RAFT broadcast) + login_establish_result login_resp; + std::vector< lsn_pair > peer_resp; + { + std::lock_guard< std::mutex > g{mu_}; + login_resp = login_establish_result{.geo = geo_, .term = ++state_.term, .dlsn = state_.last_append_lsn}; + peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); + } + + auto const members = replica_manager::instance()->get_volume(vol_uuid); + for (auto const m : members) { + login_resp.members.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); + if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(login_resp.term, true /* is_login */)); r) { + peer_resp.emplace_back(r.value()); + } + } + // compute watermark as max(quorum.last_append) + if (peer_resp.size() <= members.size() / 2) { + return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); + } + + return login_resp; } // ── resolution-round hooks (driven by MemTransport::run_resolution) ── @@ -578,8 +614,10 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { // create peer raft group and add members to it. result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< wire::member > const& members) { - return raft_service::instance()->srv_create_volume(volume_id, members); + std::vector< replica_endpoint > const& members) { + auto const r = raft_service::instance()->srv_create_volume(volume_id, members); + if (r) { replica_manager::instance()->register_volume(volume_id, members); } + return r; } } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index aaaa3cd..8722d67 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -43,10 +43,6 @@ namespace craft { class MemTransport; // in-process network + cold path -namespace wire { -struct member; -} - using sisl::ok; using status = sisl::status; @@ -107,6 +103,21 @@ struct replica_faults { std::chrono::milliseconds delay{0}; // injected network latency to this replica }; +// The server's per-volume geometry -- what LOGIN advertises; the replica's journal/index is built from it. +struct server_geometry { + uint64_t capacity; + uint32_t lba_size; + replica_endpoint ep; +}; + +// login resp passed to tcp layer +struct login_establish_result { + server_geometry geo; + uint64_t term; + int64_t dlsn; + std::vector< replica_endpoint > members; +}; + // enable_shared_from_this: a write the transport timed out is delivered late, from the transport's timer // thread. That closure must hold a WEAK reference here (a strong one would cycle: replica -> net_ -> closure // -> replica), so the replica must be reachable as a shared_ptr. It always is; make_mem_replica_group is the @@ -123,6 +134,7 @@ class MemCraftReplica final : public craft_replica, static constexpr std::size_t k_missing_sample = 16; MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); + MemCraftReplica(server_geometry geo); // Snapshot this replica's state. Takes mu_ and deliberately does NOT consult net_: do_write() locks // the transport before mu_, so reading net_ under mu_ here would invert that order. Callers that want @@ -174,7 +186,7 @@ class MemCraftReplica final : public craft_replica, async_result< std::vector< JournalSlot > > fetch_data(std::vector< int64_t > lsns) override; async_status truncate(int64_t lsn) override; - peer_id_t id() const override { return ep_.id; } // craft_replica + peer_id_t id() const override { return geo_.ep.id; } // craft_replica // ── local-server surface: drive this replica directly, with an EXTERNAL transport (the TCP frontend, // craft_tcp_server, or any real network) as the wire. Each wraps a synchronous core WITHOUT a @@ -195,14 +207,14 @@ class MemCraftReplica final : public craft_replica, // The standalone (one-process = one-replica) resolution round: itself lacking a slot IS the quorum-lacks // evidence at N=1, so every hole <= upto is verdicted Empty and the frontier advances through it. result< resolution_result > srv_resolve(client_hdr hdr, int64_t upto) { return do_resolve_local(hdr, upto); } - void srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term) { - apply_login(volume_id, client_token, term); + result< login_establish_result > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { + return apply_login(volume_id, client_token); } void srv_end() { cold_apply_logout(); } lsn_pair srv_lsns() { return peek_lsns(); } result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< wire::member > const& members); + std::vector< replica_endpoint > const& members); result< lsn_pair > srv_get_rs_commit_lsn(uint64_t term, bool is_login) { return do_get_rs_commit_lsn(term, is_login); } @@ -276,10 +288,12 @@ class MemCraftReplica final : public craft_replica, // real hooks using raft channel // void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); - void apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); + result< login_establish_result > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token); // void apply_logout(); // void apply_truncate_above(int64_t rs_commit_lsn); + // Misc + void init_faults(); // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. std::optional< MemJournalSlot > peek_slot(int64_t dlsn); // copy of the slot, or nullopt if absent @@ -301,8 +315,7 @@ class MemCraftReplica final : public craft_replica, std::mutex fault_mu_; // serializes mutators only std::vector< std::unique_ptr< replica_faults const > > fault_retired_; // guarded by fault_mu_ - replica_endpoint ep_; - uint32_t page_size_; + server_geometry geo_; std::shared_ptr< MemTransport > net_; CraftPartitionState state_; diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index bfbbe41..4a370c3 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -26,8 +26,9 @@ #include "mem/replica.hpp" // the full MemCraftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) #include // to_wire_status (the shared wire <-> craft_error bridge) -#include "mem/raft/raft_service.hpp" +#include "raft/raft_service.hpp" #include "replica_mgr.hpp" +#include "helper.hpp" namespace craft::net { @@ -38,19 +39,15 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : geo_{std::move(geo)} { - replica_endpoint ep; - if (!geo_.members.empty()) { - std::copy(geo_.members[0].id.begin(), geo_.members[0].id.end(), ep.id.begin()); // wire id[16] -> uuid - ep.addr = geo_.members[0].addr; - } +craft_tcp_server::craft_tcp_server(uint32_t max_tx, server_geometry geo, std::string const& server_config_file) : + max_tx_{max_tx} { // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { replica_manager::instance()->start_replica_service(server_config_file); - raft_service::instance()->start_raft_service(ep.id); + raft_service::instance()->start_raft_service(geo.ep.id); } - replica_ = std::make_shared< MemCraftReplica >(std::move(ep), geo_.lba_size, nullptr); + replica_ = std::make_shared< MemCraftReplica >(std::move(geo)); } craft_tcp_server::~craft_tcp_server() = default; @@ -72,9 +69,9 @@ void craft_tcp_server::log_stats() const { void craft_tcp_server::serve(craft_conn conn) { for (;;) { - auto msg = conn.recv_message(geo_.max_tx); + auto msg = conn.recv_message(max_tx_); if (!msg) return; // peer closed, or a framing error -- done with this connection - auto parsed = wire::parse_message(*msg, geo_.max_tx); + auto parsed = wire::parse_message(*msg, max_tx_); if (!parsed) return; switch (static_cast< wire::op >(parsed->hdr.op)) { case wire::op::login: @@ -118,35 +115,31 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { // accepted (like its fake HELO cold path). A multi-volume server routes the session-establishment by it. std::vector< uint8_t > out; - // fail if there is an active session - if (session_active_) { - wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_eligible), + + auto const lr = wire::decode< wire::login_req >(req.op_header); + auto result = replica_->srv_establish(lr.volume_id, lr.client_token); + if (!result) { + wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(to_wire_status(result.error())), req.hdr.request_id, {}, {}); conn.send_all(out); return; } - - auto const lr = wire::decode< wire::login_req >(req.op_header); - session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection - session_active_ = true; - replica_->srv_establish(lr.volume_id, lr.client_token, session_term_); - - auto const lsns = replica_->srv_lsns(); + auto const srv_rsp = result.value(); wire::login_rsp rsp{}; - rsp.term = session_term_; - // The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one to use -- the - // client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, which skipped slot 0 on - // a fresh cluster: every replica was then permanently Missing dLSN 0, apply_up_to() stalled there forever, and - // commit_lsn pinned at -1 -- so no journal reclaimed and every read walked the whole tail. See wire.hpp. - rsp.dlsn = lsns.last_append_lsn; - rsp.capacity = geo_.capacity; - rsp.lba_size = geo_.lba_size; - rsp.max_tx = geo_.max_tx; - rsp.member_count = static_cast< uint32_t >(geo_.members.size()); + rsp.term = srv_rsp.term; + rsp.dlsn = srv_rsp.dlsn; + rsp.capacity = srv_rsp.geo.capacity; + rsp.lba_size = srv_rsp.geo.lba_size; + rsp.max_tx = max_tx_; + rsp.member_count = static_cast< uint32_t >(srv_rsp.members.size()); std::vector< uint8_t > body; - for (auto const& m : geo_.members) - wire::put_member(body, m); + for (auto const& m : srv_rsp.members) { + wire::member wm{}; + std::memcpy(wm.id.data(), &m.id, 16); + wm.addr = m.addr; + wire::put_member(body, wm); + } wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, as_bytes(rsp), body); @@ -155,13 +148,6 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { auto const hr = wire::decode< wire::helo_req >(req.op_header); - // FAKE cold path (until peer-to-peer replica comms): a follower this client never logged into ADOPTS the - // presented (leader's) session term and establishes locally. A fresh cluster starts empty (dLSN -1 on every - // replica), so no cross-replica RS-commit-lsn sync is needed yet; term-fencing is what HELO must restore so - // subsequent IO at this term is accepted. Re-HELO after a term bump just re-establishes at the new term. - session_term_ = hr.term; - session_active_ = true; - replica_->srv_establish(hr.volume_id, hr.client_token, session_term_); std::vector< uint8_t > out; wire::frame_message(out, wire::op::helo_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, {}, {}); conn.send_all(out); @@ -317,7 +303,11 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r if (!members) { code = wire::status::invalid_argument; // body shorter than member_count implies -- malformed request } else { - auto const r = replica_->srv_create_volume(cr.volume_id, *members); + std::vector< replica_endpoint > replica_members; + for (auto const& m : *members) { + replica_members.emplace_back(replica_endpoint{.id = craft::to_uuid(m.id), .addr = m.addr}); + } + auto const r = replica_->srv_create_volume(cr.volume_id, replica_members); if (!r) code = to_wire_status(r.error()); } diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index aa8fbf0..9b93f64 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -34,21 +34,14 @@ namespace craft { class MemCraftReplica; // the server's state backing (pimpl; included only in craft_tcp_server.cpp) +struct server_geometry; } namespace craft::net { -// The server's per-volume geometry -- what LOGIN advertises; the replica's journal/index is built from it. -struct server_geometry { - uint64_t capacity = 0; - uint32_t lba_size = 0; - uint32_t max_tx = 0; - std::vector< wire::member > members; // members[0] is this replica (its id + addr) -}; - class craft_tcp_server { public: - explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}); + explicit craft_tcp_server(uint32_t max_tx, server_geometry geo, std::string const& server_config_file = {}); ~craft_tcp_server(); craft_tcp_server(craft_tcp_server&&) = default; craft_tcp_server& operator=(craft_tcp_server&&) = default; @@ -64,7 +57,7 @@ class craft_tcp_server { void log_stats() const; private: - server_geometry geo_; + uint32_t max_tx_; std::shared_ptr< MemCraftReplica > replica_; // the real state; driven via its srv_* local-server seam uint64_t next_term_ = 0; // monotonic term source; a fresh LOGIN takes ++next_term_ uint64_t session_term_ = 0; // the current session's term, stamped on every IO diff --git a/src/mem/raft/in_memory_log_store.cpp b/src/raft/in_memory_log_store.cpp similarity index 100% rename from src/mem/raft/in_memory_log_store.cpp rename to src/raft/in_memory_log_store.cpp diff --git a/src/mem/raft/in_memory_log_store.hpp b/src/raft/in_memory_log_store.hpp similarity index 100% rename from src/mem/raft/in_memory_log_store.hpp rename to src/raft/in_memory_log_store.hpp diff --git a/src/mem/raft/raft_service.cpp b/src/raft/raft_service.cpp similarity index 62% rename from src/mem/raft/raft_service.cpp rename to src/raft/raft_service.cpp index 1aaae92..4fc513c 100644 --- a/src/mem/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -1,7 +1,7 @@ #include #include "raft_service.hpp" #include "raft_state_manager.hpp" -#include "mem/helper.hpp" +#include "helper.hpp" #include "replica_mgr.hpp" #include @@ -40,7 +40,7 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { } result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< wire::member > const& members) { + std::vector< replica_endpoint > const& members) { auto const group_id = craft::to_uuid(volume_id); auto consensus = raft_service::instance()->get_consensus(); @@ -52,14 +52,13 @@ result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& // Add every OTHER member as a follower. for (auto const& m : members) { - auto const member_id = craft::to_uuid(m.id); - if (lookup_peer(member_id).empty()) { + if (lookup_peer(m.id).empty()) { return fail(craft_error::INTERNAL); // sanity: member missing in the server config json } - if (member_id == server_uuid_) continue; + if (m.id == server_uuid_) continue; auto srv_cfg = - nuraft::srv_config(nuraft_mesg::to_server_id(member_id), 0, boost::uuids::to_string(member_id), "", false); + nuraft::srv_config(nuraft_mesg::to_server_id(m.id), 0, boost::uuids::to_string(m.id), "", false); if (auto const result = craft::sync_get(consensus->add_member(group_id, srv_cfg)); !result) { return fail(craft_error::INTERNAL); } @@ -76,9 +75,38 @@ std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, nuraft_mesg::group_id_t const& group_id) { - LOGINFO("Creating raft state manager for server_id={} group_id={}, server_uuid={}", srv_id, - boost::uuids::to_string(group_id), boost::uuids::to_string(server_uuid_)); - return std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id); + auto result = get_state_mgr(group_id); + if (result) { + LOGINFO("RAFT state manager for group_id={} already exists, returning existing instance", + boost::uuids::to_string(group_id)); + return result.value(); + } + LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); + auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id); + add_state_mgr(group_id, mgr); + return mgr; +} + +result< std::shared_ptr< raft_state_mgr > > raft_service::get_state_mgr(nuraft_mesg::group_id_t const& group_id) { + std::shared_lock< std::shared_mutex > g{mu_}; + auto const it = state_mgrs_.find(group_id); + if (it == state_mgrs_.end()) return fail(craft_error::INTERNAL); + return it->second; +} + +void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr) { + std::lock_guard< std::shared_mutex > g{mu_}; + state_mgrs_[group_id] = std::move(mgr); +} + +bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { + auto const state_mgr = get_state_mgr(group_id); + if (!state_mgr) { + LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); + return false; + } + auto* raft_ctx = state_mgr.value()->repl_ctx(); + return raft_ctx && raft_ctx->is_raft_leader(); } } // namespace craft diff --git a/src/mem/raft/raft_service.hpp b/src/raft/raft_service.hpp similarity index 76% rename from src/mem/raft/raft_service.hpp rename to src/raft/raft_service.hpp index acac753..34ae39b 100644 --- a/src/mem/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include // result types namespace nuraft_mesg { @@ -17,6 +16,8 @@ using consensus_handle = std::shared_ptr< nuraft_mesg::manager >; namespace craft { +class raft_state_mgr; + // Process-wide bridge for the peer-to-peer consensus engine used by the TCP server and replica-side code. // The concrete nuraft_mesg::manager instance is installed once and then shared by anyone that needs to create // groups, add members, or issue consensus operations. @@ -29,7 +30,8 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< wire::member > const& members); + std::vector< replica_endpoint > const& members); + bool is_leader(nuraft_mesg::group_id_t const& group_id); // messaging_application overrides std::string lookup_peer(nuraft_mesg::peer_id_t const&) override; @@ -42,6 +44,11 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab nuraft_mesg::peer_id_t server_uuid_; std::once_flag raft_started_; nlohmann::json server_config_; + std::shared_mutex mu_; + std::map< nuraft_mesg::group_id_t, std::shared_ptr< raft_state_mgr > > state_mgrs_; + + result< std::shared_ptr< raft_state_mgr > > get_state_mgr(nuraft_mesg::group_id_t const& group_id); + void add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr); }; } // namespace craft diff --git a/src/mem/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp similarity index 100% rename from src/mem/raft/raft_state_machine.hpp rename to src/raft/raft_state_machine.hpp diff --git a/src/mem/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp similarity index 99% rename from src/mem/raft/raft_state_manager.cpp rename to src/raft/raft_state_manager.cpp index 80bfd9d..ec517ab 100644 --- a/src/mem/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -7,7 +7,7 @@ #include #include "in_memory_log_store.hpp" -#include "mem/helper.hpp" +#include "helper.hpp" namespace craft { diff --git a/src/mem/raft/raft_state_manager.hpp b/src/raft/raft_state_manager.hpp similarity index 100% rename from src/mem/raft/raft_state_manager.hpp rename to src/raft/raft_state_manager.hpp diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index b209c9d..fdf3d87 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -1,4 +1,5 @@ #include "replica_mgr.hpp" +#include "helper.hpp" #include @@ -40,29 +41,45 @@ void replica_manager::start_replica_service(std::string const& path) { } std::string replica_manager::lookup_peer(boost::uuids::uuid const& id) const { - std::lock_guard< std::mutex > g{mu_}; + std::shared_lock< std::shared_mutex > g(mu_); auto const it = replicas_.find(id); if (it == replicas_.end()) return {}; return it->second.host + ":" + std::to_string(it->second.raft_port); } std::shared_ptr< net::CraftTcpPeer > replica_manager::get_peer_client(boost::uuids::uuid const& id) { - std::lock_guard< std::mutex > g{mu_}; - if (auto it = peer_clients_.find(id); it != peer_clients_.end()) return it->second; + std::lock_guard< std::shared_mutex > g{mu_}; auto const rit = replicas_.find(id); if (rit == replicas_.end()) return nullptr; - - auto client = std::make_shared< net::CraftTcpPeer >(rit->second.host, rit->second.tcp_port, id); - peer_clients_[id] = client; - return client; + if (!rit->second.peer_client) { + rit->second.peer_client = std::make_shared< net::CraftTcpPeer >(rit->second.host, rit->second.tcp_port, id); + } + return rit->second.peer_client; } std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) const { - std::lock_guard< std::mutex > g{mu_}; + std::shared_lock< std::shared_mutex > g(mu_); auto const it = replicas_.find(id); if (it == replicas_.end()) return std::nullopt; return it->second; } +void replica_manager::register_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< replica_endpoint > const& members) { + std::lock_guard< std::shared_mutex > g{mu_}; + std::vector< replica_info > rinfos; + for (auto const& m : members) { + rinfos.emplace_back(replicas_[m.id]); + } + volumes_[craft::to_uuid(volume_id)] = rinfos; +} + +std::vector< replica_info > replica_manager::get_volume(boost::uuids::uuid const& volume_id) { + std::shared_lock< std::shared_mutex > g(mu_); + auto const it = volumes_.find(volume_id); + if (it == volumes_.end()) return {}; + return it->second; +} + } // namespace craft \ No newline at end of file diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index 177f65d..b4bc280 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -20,6 +20,7 @@ struct replica_info { std::string host; uint16_t raft_port{0}; uint16_t tcp_port{0}; + std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; }; // Process-wide registry: peer identity -> reachability, and (lazily) the open peer-plane connection to it. @@ -33,18 +34,18 @@ class replica_manager { // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". std::string lookup_peer(boost::uuids::uuid const& id) const; - // wire-plane peer client: lazily connect-and-cache a CraftTcpPeer for this id. std::shared_ptr< net::CraftTcpPeer > get_peer_client(boost::uuids::uuid const& id); - std::optional< replica_info > get(boost::uuids::uuid const& id) const; + void register_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members); + std::vector< replica_info > get_volume(boost::uuids::uuid const& volume_id); private: replica_manager() = default; - mutable std::mutex mu_; + mutable std::shared_mutex mu_; std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once - std::map< boost::uuids::uuid, std::shared_ptr< net::CraftTcpPeer > > peer_clients_; // lazy, grows on use + std::map< boost::uuids::uuid, std::vector< replica_info > > volumes_; }; } // namespace craft \ No newline at end of file diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index 847996a..b1b2710 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -31,6 +31,7 @@ #include #include "net/wire_client.hpp" #include "net/tcp_server.hpp" +#include "mem/replica.hpp" using namespace craft::net; namespace wire = craft::wire; @@ -38,17 +39,11 @@ namespace wire = craft::wire; namespace { constexpr uint32_t k_lba = 4096; +constexpr uint32_t g_max_tx = 512 * 1024; -server_geometry make_geo() { - server_geometry geo{}; - geo.capacity = uint64_t{1} << 30; - geo.lba_size = k_lba; - geo.max_tx = 512 * 1024; - wire::member self{}; - self.id[0] = 0x01; - self.addr = "127.0.0.1:0"; - geo.members.push_back(self); - return geo; +craft::server_geometry make_geo() { + return craft::server_geometry{ + .capacity = uint64_t{1} << 30, .lba_size = k_lba, .ep = {.id = boost::uuids::uuid{}, .addr = "127.0.0.1:0"}}; } // A per-byte-nonzero pattern of `n` bytes -- nonzero so no 4 KiB page collapses to a hole on the read path @@ -69,7 +64,7 @@ void with_session(F&& body) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{make_geo()}; + craft_tcp_server server{g_max_tx, make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); @@ -96,7 +91,7 @@ TEST(CraftTcp, CommitFrontierAdvances) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{make_geo()}; + craft_tcp_server server{g_max_tx, make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); @@ -138,7 +133,7 @@ TEST(CraftTcp, LoginLogoutRoundTrip) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{make_geo()}; + craft_tcp_server server{g_max_tx, make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 4c99176..06b174e 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -39,6 +39,7 @@ #include #include "net/tcp_server.hpp" +#include "mem/replica.hpp" #include // A 0 default means "unset" -> resolved in code (capacity to 1 GiB, max_tx to the single-sourced wire default), so @@ -84,26 +85,6 @@ int main(int argc, char** argv) { uint32_t max_tx = SISL_OPTIONS["max_tx"].as< uint32_t >(); if (max_tx == 0) max_tx = craft::wire::k_default_max_tx; - // Advertise this one replica in login_rsp. The id is cosmetic here (the client routes by index, and HELO - // fences by term, not id) -- a fresh random id is fine; the client's --craft-tcp supplies its own members. - craft::net::server_geometry geo; - geo.capacity = capacity; - geo.lba_size = lba_size; - geo.max_tx = max_tx; - craft::wire::member self{}; - auto id = boost::uuids::random_generator()(); - if (SISL_OPTIONS.count("server_uuid")) { - try { - id = boost::uuids::string_generator()(SISL_OPTIONS["server_uuid"].as< std::string >()); - } catch (std::exception const& e) { - std::cerr << "Invalid --server_uuid: " << e.what() << "\n"; - return 2; - } - } - std::copy(id.begin(), id.end(), self.id.begin()); - self.addr = "127.0.0.1:" + std::to_string(port); - geo.members.push_back(self); - auto lst = craft::net::craft_listener::bind_listen(port); if (!lst) { std::cerr << "craft_reference_tcp_srv: bind/listen failed on 127.0.0.1:" << port << "\n"; @@ -117,7 +98,23 @@ int main(int argc, char** argv) { if (SISL_OPTIONS.count("server_config_file")) { server_config_file = SISL_OPTIONS["server_config_file"].as< std::string >(); } - craft::net::craft_tcp_server server{std::move(geo), server_config_file}; + + // Advertise this one replica in login_rsp. The id is cosmetic here (the client routes by index, and HELO + // fences by term, not id) -- a fresh random id is fine; the client's --craft-tcp supplies its own members. + auto id = boost::uuids::random_generator()(); + if (SISL_OPTIONS.count("server_uuid")) { + try { + id = boost::uuids::string_generator()(SISL_OPTIONS["server_uuid"].as< std::string >()); + } catch (std::exception const& e) { + std::cerr << "Invalid --server_uuid: " << e.what() << "\n"; + return 2; + } + } + auto geo = + craft::server_geometry{.capacity = capacity, + .lba_size = lba_size, + .ep = craft::replica_endpoint{.id = id, .addr = fmt::format("127.0.0.1:{}", port)}}; + craft::net::craft_tcp_server server{max_tx, std::move(geo), server_config_file}; // sigaction WITHOUT SA_RESTART: glibc's signal() sets SA_RESTART, which auto-restarts the blocking accept() // after the handler runs, so the loop would never re-check g_stop and Ctrl-C could not stop the server. With From ec64e94c25ba2f2f1772e42c2463cda798946ed6 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sat, 25 Jul 2026 01:36:52 -0700 Subject: [PATCH 04/24] Add the login logic according to the craft design --- src/mem/replica.cpp | 164 ++++++++++++++++++++++++++++-- src/mem/replica.hpp | 24 ++--- src/net/tcp_server.cpp | 17 ++-- src/net/tcp_server.hpp | 2 +- src/raft/raft_helpers.hpp | 0 src/raft/raft_service.cpp | 64 +++++++++++- src/raft/raft_service.hpp | 11 +- src/raft/raft_state_machine.hpp | 71 +++++++++++-- src/raft/raft_state_manager.cpp | 12 ++- src/raft/raft_state_manager.hpp | 7 +- test/test_tcp.cpp | 13 +-- tools/craft_reference_tcp_srv.cpp | 5 +- 12 files changed, 335 insertions(+), 55 deletions(-) create mode 100644 src/raft/raft_helpers.hpp diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index bc778db..eb56b68 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -17,6 +17,7 @@ #include "mem/cluster.hpp" // the full MemTransport type #include "raft/raft_service.hpp" // for raft channel #include "replica_mgr.hpp" +#include "raft/raft_state_machine.hpp" // for raft message payload types #include "helper.hpp" #include "craft/types.hpp" @@ -540,30 +541,115 @@ void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { // ── peer comm hooks (driven by raft) ── -result< login_establish_result > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, - uint64_t client_token) { +MemCraftReplica::MemJournalSlot MemCraftReplica::to_mem_journal_slot(JournalSlot const& j, uint64_t term) { + return MemJournalSlot{ + .term = term, + .lba = j.lba, + .len = j.len, + .all_zeros = j.all_zeros, + .is_empty = j.is_empty, + .bytes = (j.data.size == 0) ? nullptr : take_payload(j.data), + }; +} + +std::vector< int64_t > MemCraftReplica::get_missing_slots(int64_t watermark) { + std::lock_guard< std::mutex > g{mu_}; + std::vector< int64_t > missing; + int64_t expect = state_.commit_lsn + 1; + auto it = journal_.lower_bound(expect); // first present entry >= expect + for (; it != journal_.end() && it->first <= watermark; ++it) { + for (; expect < it->first; ++expect) + missing.push_back(expect); // gap before this entry + expect = it->first + 1; + } + for (; expect <= watermark; ++expect) + missing.push_back(expect); // trailing gap after the last present entry + return missing; +} + +int64_t MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, + uint64_t term) { + auto const peers = replica_manager::instance()->get_volume(vol_uuid); + auto const missing_lsns = get_missing_slots(watermark); + + // Brute force, no optimizations for now + // Step 1: ask every peer for the full missing list, collect ALL responses first. + std::vector< std::vector< JournalSlot > > all_responses; + for (auto const& peer : peers) { + if (peer.id == geo_.ep.id) { continue; } + if (auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing_lsns)); r) { + all_responses.emplace_back(std::move(r.value())); + } + } + + // Step 2: for each requested lsn, look across every response and decide its fate. + int64_t stalled_lsn{-1}; + for (auto lsn : missing_lsns) { + JournalSlot const* found = nullptr; + uint32_t missing = 0; + bool is_data{false}; + + for (auto const& resp : all_responses) { + if (is_data) { break; } + if (auto const it = std::ranges::find_if(resp, [&](auto const& s) { return s.lsn == lsn; }); + it != resp.end()) { + if (it->is_empty) { + missing++; + } else { + is_data = true; + cold_install_slot(lsn, to_mem_journal_slot(*it, term)); + } + } + } + + if (missing > peers.size() / 2) { + cold_mark_empty(lsn); // adopt an already-committed verdict + } else if (!is_data) { + // unresolved lsn + stalled_lsn = (stalled_lsn == -1) ? lsn : std::min(lsn, stalled_lsn); + } + } + return stalled_lsn; +} + +result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, + uint64_t client_token, uint64_t term) { + if (auto const stalled = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); stalled != -1) { + // leader could not resolve all the missing lsns + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + if (auto const r = raft_service::instance()->propose( + vol_uuid, SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, .client_token = client_token}); + !r) { + // TODO: any cleanup required? + return std::unexpected(r.error()); + } + return {}; +} + +result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { // place holder // Phase 1: collect replica LSN state (non-RAFT broadcast) // 1.1: accepted by leader only. // TODO: what happens if the leader changes before the login is complete? auto vol_uuid = craft::to_uuid(volume_id); - if (!raft_service::instance()->is_leader(vol_uuid)) { - return std::unexpected(make_error_condition(craft_error::NOT_LEADER)); + auto raft_service_inst = raft_service::instance(); + if (!raft_service_inst->is_leader(vol_uuid)) { + return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; } // 1.2 collect replica LSN state (non-RAFT broadcast) - login_establish_result login_resp; std::vector< lsn_pair > peer_resp; + uint64_t new_term; { std::lock_guard< std::mutex > g{mu_}; - login_resp = login_establish_result{.geo = geo_, .term = ++state_.term, .dlsn = state_.last_append_lsn}; + new_term = state_.term + 1; // the term in the state is update as part of internal login raft commit peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); } auto const members = replica_manager::instance()->get_volume(vol_uuid); for (auto const m : members) { - login_resp.members.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); - if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(login_resp.term, true /* is_login */)); r) { + if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(new_term, true /* is_login */)); r) { peer_resp.emplace_back(r.value()); } } @@ -571,8 +657,34 @@ result< login_establish_result > MemCraftReplica::apply_login(std::array< uint8_ if (peer_resp.size() <= members.size() / 2) { return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); } + auto const rs_commit_lsn = std::ranges::max_element(peer_resp, {}, &lsn_pair::last_append_lsn)->last_append_lsn; + + // Phase 1b: Leader behind - resolve all the missing lsns and + // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) + if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, new_term - 1); !r) { + return std::unexpected(r.error()); + } + + // Phase 3: InternalLogin(token, term) via RAFT + if (auto const r = + raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = new_term}); + !r) { + // TODO: any cleanup required? + return std::unexpected(r.error()); + } - return login_resp; + // Phase 4: truncate above rs_commit_lsn + cold_truncate_above(rs_commit_lsn); + std::vector< replica_endpoint > replicas; + for (auto const& m : members) { + replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); + } + return LoginResult{.members = replicas, + .dLSN = rs_commit_lsn, + .term = new_term, + .lba_size = geo_.lba_size, + .capacity = geo_.capacity, + .max_tx = geo_.max_tx}; } // ── resolution-round hooks (driven by MemTransport::run_resolution) ── @@ -615,9 +727,41 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { // create peer raft group and add members to it. result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members) { - auto const r = raft_service::instance()->srv_create_volume(volume_id, members); + auto const commit_cb = [this](uint64_t log_idx, nlohmann::json const& j) { + auto const op_val = j.at("op").get< int >(); + switch (static_cast< Operation >(op_val)) { + case Operation::SyncRSCommitLSN: { + SyncRSCommitLSNMsg m; + try { + m = j.get< SyncRSCommitLSNMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); + return; + } + apply_sync(m.rs_commit_lsn, m.client_token); + break; + } + case Operation::InternalLogin: { + InternalLoginMsg m; + try { + m = j.get< InternalLoginMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); + return; + } + cold_apply_login(m.client_token, m.term); + break; + } + default: + LOGERROR("commit[{}]: unknown op={}", log_idx, op_val); + break; + } + }; + auto const r = raft_service::instance()->srv_create_volume(volume_id, members, commit_cb); if (r) { replica_manager::instance()->register_volume(volume_id, members); } return r; } +void MemCraftReplica::apply_sync(int64_t rs_commit_lsn, uint64_t client_token) {} + } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 8722d67..d2a6b45 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -108,14 +108,7 @@ struct server_geometry { uint64_t capacity; uint32_t lba_size; replica_endpoint ep; -}; - -// login resp passed to tcp layer -struct login_establish_result { - server_geometry geo; - uint64_t term; - int64_t dlsn; - std::vector< replica_endpoint > members; + uint32_t max_tx; }; // enable_shared_from_this: a write the transport timed out is delivered late, from the transport's timer @@ -207,7 +200,7 @@ class MemCraftReplica final : public craft_replica, // The standalone (one-process = one-replica) resolution round: itself lacking a slot IS the quorum-lacks // evidence at N=1, so every hole <= upto is verdicted Empty and the frontier advances through it. result< resolution_result > srv_resolve(client_hdr hdr, int64_t upto) { return do_resolve_local(hdr, upto); } - result< login_establish_result > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { + result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { return apply_login(volume_id, client_token); } void srv_end() { cold_apply_logout(); } @@ -287,13 +280,20 @@ class MemCraftReplica final : public craft_replica, void cold_truncate_above(int64_t rs_commit_lsn); // real hooks using raft channel - // void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); - result< login_establish_result > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token); + void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); + result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token); // void apply_logout(); // void apply_truncate_above(int64_t rs_commit_lsn); - // Misc + // Misc helpers void init_faults(); + MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); + std::vector< int64_t > get_missing_slots(int64_t watermark); + int64_t resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, + uint64_t term); + result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, + uint64_t term); + // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. std::optional< MemJournalSlot > peek_slot(int64_t dlsn); // copy of the slot, or nullopt if absent diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 4a370c3..da4ec0f 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -39,8 +39,7 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(uint32_t max_tx, server_geometry geo, std::string const& server_config_file) : - max_tx_{max_tx} { +craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : max_tx_{geo.max_tx} { // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { @@ -125,12 +124,18 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { return; } auto const srv_rsp = result.value(); + if (!srv_rsp.leader_hint.is_nil()) { + wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_leader), + req.hdr.request_id, {}, {}); + conn.send_all(out); + return; + } wire::login_rsp rsp{}; rsp.term = srv_rsp.term; - rsp.dlsn = srv_rsp.dlsn; - rsp.capacity = srv_rsp.geo.capacity; - rsp.lba_size = srv_rsp.geo.lba_size; - rsp.max_tx = max_tx_; + rsp.dlsn = srv_rsp.dLSN; + rsp.capacity = srv_rsp.capacity; + rsp.lba_size = srv_rsp.lba_size; + rsp.max_tx = srv_rsp.max_tx; rsp.member_count = static_cast< uint32_t >(srv_rsp.members.size()); std::vector< uint8_t > body; diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index 9b93f64..b8f19db 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -41,7 +41,7 @@ namespace craft::net { class craft_tcp_server { public: - explicit craft_tcp_server(uint32_t max_tx, server_geometry geo, std::string const& server_config_file = {}); + explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}); ~craft_tcp_server(); craft_tcp_server(craft_tcp_server&&) = default; craft_tcp_server& operator=(craft_tcp_server&&) = default; diff --git a/src/raft/raft_helpers.hpp b/src/raft/raft_helpers.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 4fc513c..1fd80b1 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -15,6 +15,14 @@ namespace { auto fail(craft_error e) { return std::unexpected(make_error_condition(e)); } } // namespace +static nuraft::ptr< nuraft::buffer > create_message(nlohmann::json const& j_obj) { + auto v_msgpack = nlohmann::json::to_msgpack(j_obj); + auto buf = nuraft::buffer::alloc(v_msgpack.size() + sizeof(int32_t)); + buf->put(&v_msgpack[0], v_msgpack.size()); + buf->pos(0); + return buf; +} + std::shared_ptr< raft_service > raft_service::instance() { static std::shared_ptr< raft_service > instance{new raft_service()}; return instance; @@ -40,7 +48,7 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { } result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members) { + std::vector< replica_endpoint > const& members, raft_commit_cb_t cb) { auto const group_id = craft::to_uuid(volume_id); auto consensus = raft_service::instance()->get_consensus(); @@ -63,6 +71,7 @@ result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& return fail(craft_error::INTERNAL); } } + add_commit_cb(group_id, cb); return {}; } @@ -82,7 +91,8 @@ std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(in return result.value(); } LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); - auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id); + auto const r = get_commit_cb(group_id); + auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, (r ? r.value() : nullptr)); add_state_mgr(group_id, mgr); return mgr; } @@ -99,6 +109,18 @@ void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::s state_mgrs_[group_id] = std::move(mgr); } +result< raft_commit_cb_t > raft_service::get_commit_cb(nuraft_mesg::group_id_t const& group_id) { + std::shared_lock< std::shared_mutex > g{mu_}; + auto const it = commit_cbs_.find(group_id); + if (it == commit_cbs_.end()) return fail(craft_error::INTERNAL); + return it->second; +} + +void raft_service::add_commit_cb(nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb) { + std::lock_guard< std::shared_mutex > g{mu_}; + commit_cbs_.emplace(group_id, std::move(cb)); +} + bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { auto const state_mgr = get_state_mgr(group_id); if (!state_mgr) { @@ -109,4 +131,42 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { return raft_ctx && raft_ctx->is_raft_leader(); } +nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& group_id) { + auto const state_mgr = get_state_mgr(group_id); + if (!state_mgr) { + LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); + return {}; + } + auto* raft_ctx = state_mgr.value()->repl_ctx(); + if (!raft_ctx) { + LOGWARN("No leader for the raft group {}", group_id); + return {}; + } + return boost::uuids::string_generator()(raft_ctx->raft_leader_id()); +} + +template < typename MsgT > +result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT const& payload) { + auto const state_mgr = get_state_mgr(group_id); + if (!state_mgr) { + LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + auto* raft_ctx = state_mgr.value()->repl_ctx(); + if (!raft_ctx) { + LOGWARN("RAFT state manager context for group_id={} not found", boost::uuids::to_string(group_id)); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + + auto const append_status = raft_ctx->raft_server()->append_entries({create_message(nlohmann::json(payload))}); + if (append_status && !append_status->get_accepted()) { + return std::unexpected(nuraft_mesg::to_condition(append_status->get_result_code())); + } + return {}; +} + +template result< void > raft_service::propose< SyncRSCommitLSNMsg >(boost::uuids::uuid const&, + SyncRSCommitLSNMsg const&); +template result< void > raft_service::propose< InternalLoginMsg >(boost::uuids::uuid const&, InternalLoginMsg const&); + } // namespace craft diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 34ae39b..05eaa22 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -7,6 +7,7 @@ #include #include #include // result types +#include "raft_state_machine.hpp" namespace nuraft_mesg { class manager; @@ -30,8 +31,13 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members); + std::vector< replica_endpoint > const& members, raft_commit_cb_t cb); bool is_leader(nuraft_mesg::group_id_t const& group_id); + nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); + + // raft append entrues + template < typename MsgT > + result< void > propose(boost::uuids::uuid const& group_id, MsgT const& payload); // messaging_application overrides std::string lookup_peer(nuraft_mesg::peer_id_t const&) override; @@ -46,9 +52,12 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab nlohmann::json server_config_; std::shared_mutex mu_; std::map< nuraft_mesg::group_id_t, std::shared_ptr< raft_state_mgr > > state_mgrs_; + std::map< nuraft_mesg::group_id_t, raft_commit_cb_t > commit_cbs_; result< std::shared_ptr< raft_state_mgr > > get_state_mgr(nuraft_mesg::group_id_t const& group_id); void add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr); + result< raft_commit_cb_t > get_commit_cb(nuraft_mesg::group_id_t const& group_id); + void add_commit_cb(nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb); }; } // namespace craft diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 6a40fe3..f70dbed 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -5,27 +5,80 @@ namespace craft { -class echo_state_machine : public nuraft::state_machine { -public: - echo_state_machine() : lock_(), last_commit_idx_(0) {} +inline auto unwrap_buffer(nuraft::buffer& data) { + size_t buffer_len{0}; + auto const buffer_begin = data.get_bytes(buffer_len); + auto const buffer_end = buffer_begin + buffer_len; + auto input = std::vector< uint8_t >(buffer_begin, buffer_end); + return nlohmann::json::from_msgpack(std::move(input)); +} + +enum class Operation { SyncRSCommitLSN = 0, InternalLogin }; + +// ── SyncRSCommitLSN payload ── +struct SyncRSCommitLSNMsg { + int64_t rs_commit_lsn{-1}; + uint64_t client_token{0}; + std::vector< int64_t > empty_slots; +}; + +inline void to_json(nlohmann::json& j, SyncRSCommitLSNMsg const& m) { + j = nlohmann::json{ + {"op", static_cast< int >(Operation::SyncRSCommitLSN)}, + {"rs_commit_lsn", m.rs_commit_lsn}, + {"client_token", m.client_token}, + {"empty_slots", m.empty_slots}, + }; +} +inline void from_json(nlohmann::json const& j, SyncRSCommitLSNMsg& m) { + j.at("rs_commit_lsn").get_to(m.rs_commit_lsn); + j.at("client_token").get_to(m.client_token); + j.at("empty_slots").get_to(m.empty_slots); +} +// ── InternalLogin payload ── +struct InternalLoginMsg { + uint64_t client_token{0}; + uint64_t term{0}; +}; + +inline void to_json(nlohmann::json& j, InternalLoginMsg const& m) { + j = nlohmann::json{ + {"op", static_cast< int >(Operation::InternalLogin)}, + {"client_token", m.client_token}, + {"term", m.term}, + }; +} +inline void from_json(nlohmann::json const& j, InternalLoginMsg& m) { + j.at("client_token").get_to(m.client_token); + j.at("term").get_to(m.term); +} + +using raft_commit_cb_t = std::function< void(uint64_t log_idx, nlohmann::json const& j) >; + +class echo_state_machine : public nuraft::state_machine { public: - virtual nuraft::ptr< nuraft::buffer > commit(const nuraft::ulong log_idx, nuraft::buffer& data) { - auto_lock(lock_); + echo_state_machine(raft_commit_cb_t cb) : commit_cb_(std::move(cb)), last_commit_idx_(0) {} - LOGINFO("Commit message [{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); + virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) { + nlohmann::json j; + try { + j = unwrap_buffer(data); + } catch (nlohmann::json::parse_error const& e) { + LOGERROR("commit[{}]: msgpack decode failed: {}", log_idx, e.what()); + return nullptr; + } + if (commit_cb_) { commit_cb_(log_idx, j); } last_commit_idx_ = log_idx; return nullptr; } virtual nuraft::ptr< nuraft::buffer > pre_commit(const nuraft::ulong log_idx, nuraft::buffer& data) { - auto_lock(lock_); LOGINFO("Pre-Commit message [{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); return nullptr; } virtual void rollback(const nuraft::ulong log_idx, nuraft::buffer& data) { - auto_lock(lock_); LOGINFO("Rollback[{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); } @@ -41,7 +94,7 @@ class echo_state_machine : public nuraft::state_machine { virtual nuraft::ulong last_commit_index() { return last_commit_idx_; } private: - std::mutex lock_; nuraft::ulong last_commit_idx_; + raft_commit_cb_t commit_cb_; }; } \ No newline at end of file diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index ec517ab..50c35d7 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -1,11 +1,9 @@ #include "raft_state_manager.hpp" -#include "raft_state_machine.hpp" #include #include #include - #include "in_memory_log_store.hpp" #include "helper.hpp" @@ -61,8 +59,12 @@ nuraft::ptr< nuraft::cluster_config > fromClusterConfig(json const& cluster_conf } raft_state_mgr::raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, - nuraft_mesg::group_id_t const& group_id) : - nuraft_mesg::mesg_state_mgr(), _srv_id(srv_id), _srv_addr(to_string(srv_addr)), _group_id(to_string(group_id)) {} + nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb) : + nuraft_mesg::mesg_state_mgr(), + _srv_id(srv_id), + _srv_addr(to_string(srv_addr)), + _group_id(to_string(group_id)), + _commit_cb(std::move(cb)) {} nuraft::ptr< nuraft::cluster_config > raft_state_mgr::load_config() { LOGDEBUG("Loading config for [{}]", _group_id); @@ -116,7 +118,7 @@ void raft_state_mgr::save_state(const nuraft::srv_state& state) { uint32_t raft_state_mgr::get_logstore_id() const { return 0; } std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { - return std::make_shared< echo_state_machine >(); + return std::make_shared< echo_state_machine >(std::move(_commit_cb)); } void raft_state_mgr::permanent_destroy() {} diff --git a/src/raft/raft_state_manager.hpp b/src/raft/raft_state_manager.hpp index 09843b8..18443bc 100644 --- a/src/raft/raft_state_manager.hpp +++ b/src/raft/raft_state_manager.hpp @@ -2,12 +2,16 @@ #include #include +#include "raft_state_machine.hpp" namespace craft { +class raft_service; + class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { public: - raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id); + raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id, + raft_commit_cb_t cb); nuraft::ptr< nuraft::cluster_config > load_config() override; void save_config(const nuraft::cluster_config& config) override; @@ -27,6 +31,7 @@ class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { int32_t const _srv_id; std::string const _srv_addr; std::string const _group_id; + raft_commit_cb_t _commit_cb; // temp storage, will be passed on to state machine }; } diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index b1b2710..ecbb76f 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -39,11 +39,12 @@ namespace wire = craft::wire; namespace { constexpr uint32_t k_lba = 4096; -constexpr uint32_t g_max_tx = 512 * 1024; craft::server_geometry make_geo() { - return craft::server_geometry{ - .capacity = uint64_t{1} << 30, .lba_size = k_lba, .ep = {.id = boost::uuids::uuid{}, .addr = "127.0.0.1:0"}}; + return craft::server_geometry{.capacity = uint64_t{1} << 30, + .lba_size = k_lba, + .ep = {.id = boost::uuids::uuid{}, .addr = "127.0.0.1:0"}, + .max_tx = 512 * 1024}; } // A per-byte-nonzero pattern of `n` bytes -- nonzero so no 4 KiB page collapses to a hole on the read path @@ -64,7 +65,7 @@ void with_session(F&& body) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{g_max_tx, make_geo()}; + craft_tcp_server server{make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); @@ -91,7 +92,7 @@ TEST(CraftTcp, CommitFrontierAdvances) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{g_max_tx, make_geo()}; + craft_tcp_server server{make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); @@ -133,7 +134,7 @@ TEST(CraftTcp, LoginLogoutRoundTrip) { ASSERT_TRUE(lst.has_value()); uint16_t const port = lst->port(); - craft_tcp_server server{g_max_tx, make_geo()}; + craft_tcp_server server{make_geo()}; std::jthread srv([&] { auto conn = lst->accept(); if (conn) server.serve(std::move(*conn)); diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 06b174e..49799df 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -113,8 +113,9 @@ int main(int argc, char** argv) { auto geo = craft::server_geometry{.capacity = capacity, .lba_size = lba_size, - .ep = craft::replica_endpoint{.id = id, .addr = fmt::format("127.0.0.1:{}", port)}}; - craft::net::craft_tcp_server server{max_tx, std::move(geo), server_config_file}; + .ep = craft::replica_endpoint{.id = id, .addr = fmt::format("127.0.0.1:{}", port)}, + .max_tx = max_tx}; + craft::net::craft_tcp_server server{std::move(geo), server_config_file}; // sigaction WITHOUT SA_RESTART: glibc's signal() sets SA_RESTART, which auto-restarts the blocking accept() // after the handler runs, so the loop would never re-check g_stop and Ctrl-C could not stop the server. With From 791f45b5145d81692ccce86ca370facf08a4b69f Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sun, 26 Jul 2026 04:23:06 -0700 Subject: [PATCH 05/24] fix test cases --- src/mem/replica.cpp | 30 +++++++++++++++++++++--------- src/mem/replica.hpp | 7 ++++--- src/net/tcp_server.cpp | 15 ++++++++++----- src/raft/raft_service.hpp | 1 + test/test_tcp.cpp | 4 +++- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index eb56b68..1d9d54e 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -627,29 +627,41 @@ result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol return {}; } -result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { - // place holder +result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, + uint64_t term) { + auto raft_service_inst = raft_service::instance(); + // return cold path if raft service has not started + if (!raft_service_inst->is_raft_enabled()) { + std::lock_guard< std::mutex > g{mu_}; + state_.term = term; + state_.client_token = client_token; + return LoginResult{.members = {geo_.ep}, + .dLSN = state_.last_append_lsn, + .term = state_.term, + .lba_size = geo_.lba_size, + .capacity = geo_.capacity, + .max_tx = geo_.max_tx}; + } // Phase 1: collect replica LSN state (non-RAFT broadcast) // 1.1: accepted by leader only. // TODO: what happens if the leader changes before the login is complete? auto vol_uuid = craft::to_uuid(volume_id); - auto raft_service_inst = raft_service::instance(); if (!raft_service_inst->is_leader(vol_uuid)) { return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; } // 1.2 collect replica LSN state (non-RAFT broadcast) std::vector< lsn_pair > peer_resp; - uint64_t new_term; + uint64_t current_term; { std::lock_guard< std::mutex > g{mu_}; - new_term = state_.term + 1; // the term in the state is update as part of internal login raft commit + current_term = state_.term; peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); } auto const members = replica_manager::instance()->get_volume(vol_uuid); for (auto const m : members) { - if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(new_term, true /* is_login */)); r) { + if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term, true /* is_login */)); r) { peer_resp.emplace_back(r.value()); } } @@ -661,13 +673,13 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con // Phase 1b: Leader behind - resolve all the missing lsns and // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) - if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, new_term - 1); !r) { + if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, current_term); !r) { return std::unexpected(r.error()); } // Phase 3: InternalLogin(token, term) via RAFT if (auto const r = - raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = new_term}); + raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term}); !r) { // TODO: any cleanup required? return std::unexpected(r.error()); @@ -681,7 +693,7 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con } return LoginResult{.members = replicas, .dLSN = rs_commit_lsn, - .term = new_term, + .term = term, .lba_size = geo_.lba_size, .capacity = geo_.capacity, .max_tx = geo_.max_tx}; diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index d2a6b45..bf5bb4e 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -200,8 +200,9 @@ class MemCraftReplica final : public craft_replica, // The standalone (one-process = one-replica) resolution round: itself lacking a slot IS the quorum-lacks // evidence at N=1, so every hole <= upto is verdicted Empty and the frontier advances through it. result< resolution_result > srv_resolve(client_hdr hdr, int64_t upto) { return do_resolve_local(hdr, upto); } - result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token) { - return apply_login(volume_id, client_token); + result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, + uint64_t term) { + return apply_login(volume_id, client_token, term); } void srv_end() { cold_apply_logout(); } lsn_pair srv_lsns() { return peek_lsns(); } @@ -281,7 +282,7 @@ class MemCraftReplica final : public craft_replica, // real hooks using raft channel void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); - result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token); + result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); // void apply_logout(); // void apply_truncate_above(int64_t rs_commit_lsn); diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index da4ec0f..594c917 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -110,13 +110,18 @@ void craft_tcp_server::serve(craft_conn conn) { } void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { - // login_req names the volume; this standalone reference server fronts exactly one, so any presented id is - // accepted (like its fake HELO cold path). A multi-volume server routes the session-establishment by it. - + // session_active_ is stoll maintained here, change it once we support multi volume std::vector< uint8_t > out; - + if (session_active_) { + wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_eligible), + req.hdr.request_id, {}, {}); + conn.send_all(out); + return; + } + session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection + session_active_ = true; auto const lr = wire::decode< wire::login_req >(req.op_header); - auto result = replica_->srv_establish(lr.volume_id, lr.client_token); + auto result = replica_->srv_establish(lr.volume_id, lr.client_token, session_term_); if (!result) { wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(to_wire_status(result.error())), req.hdr.request_id, {}, {}); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 05eaa22..e268a8c 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -28,6 +28,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab virtual ~raft_service() = default; static std::shared_ptr< raft_service > instance(); + bool is_raft_enabled() { return consensus_ != nullptr; } consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index ecbb76f..f679cf8 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -41,9 +41,11 @@ namespace { constexpr uint32_t k_lba = 4096; craft::server_geometry make_geo() { + boost::uuids::uuid ep_id{}; + ep_id.data[0] = 0x01; return craft::server_geometry{.capacity = uint64_t{1} << 30, .lba_size = k_lba, - .ep = {.id = boost::uuids::uuid{}, .addr = "127.0.0.1:0"}, + .ep = {.id = ep_id, .addr = "127.0.0.1:0"}, .max_tx = 512 * 1024}; } From bae9f61746579cdc5088e1c86e24137a863fa0bc Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sun, 26 Jul 2026 06:10:07 -0700 Subject: [PATCH 06/24] add session fencing login in helo --- src/mem/replica.cpp | 80 ++++++++++++++++++++++++++++++++------- src/mem/replica.hpp | 14 +++++-- src/net/tcp_server.cpp | 17 ++++++++- src/raft/raft_service.cpp | 3 +- src/raft/raft_service.hpp | 4 +- src/replica_mgr.cpp | 4 +- src/replica_mgr.hpp | 2 +- 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 1d9d54e..004c044 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -567,8 +567,9 @@ std::vector< int64_t > MemCraftReplica::get_missing_slots(int64_t watermark) { return missing; } -int64_t MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, - uint64_t term) { +std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, + int64_t watermark, + uint64_t client_token, uint64_t term) { auto const peers = replica_manager::instance()->get_volume(vol_uuid); auto const missing_lsns = get_missing_slots(watermark); @@ -584,8 +585,8 @@ int64_t MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, i // Step 2: for each requested lsn, look across every response and decide its fate. int64_t stalled_lsn{-1}; + std::vector< int64_t > empty_slots; for (auto lsn : missing_lsns) { - JournalSlot const* found = nullptr; uint32_t missing = 0; bool is_data{false}; @@ -602,24 +603,28 @@ int64_t MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, i } } - if (missing > peers.size() / 2) { + if (missing >= peers.size() / 2) { cold_mark_empty(lsn); // adopt an already-committed verdict + empty_slots.push_back(lsn); } else if (!is_data) { // unresolved lsn stalled_lsn = (stalled_lsn == -1) ? lsn : std::min(lsn, stalled_lsn); } } - return stalled_lsn; + return {empty_slots, stalled_lsn}; } result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, uint64_t term) { - if (auto const stalled = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); stalled != -1) { + auto const [empty_slots, stalled_lsn] = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); + if (stalled_lsn != -1) { // leader could not resolve all the missing lsns return std::unexpected(make_error_condition(craft_error::INTERNAL)); } - if (auto const r = raft_service::instance()->propose( - vol_uuid, SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, .client_token = client_token}); + if (auto const r = raft_service::instance()->propose(vol_uuid, + SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, + .client_token = client_token, + .empty_slots = std::move(empty_slots)}); !r) { // TODO: any cleanup required? return std::unexpected(r.error()); @@ -739,7 +744,8 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { // create peer raft group and add members to it. result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members) { - auto const commit_cb = [this](uint64_t log_idx, nlohmann::json const& j) { + auto const vol_uuid = craft::to_uuid(volume_id); + auto const commit_cb = [this, vol_uuid](uint64_t log_idx, nlohmann::json const& j) { auto const op_val = j.at("op").get< int >(); switch (static_cast< Operation >(op_val)) { case Operation::SyncRSCommitLSN: { @@ -750,7 +756,7 @@ result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > cons LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); return; } - apply_sync(m.rs_commit_lsn, m.client_token); + apply_sync(vol_uuid, m.rs_commit_lsn, m.client_token, m.empty_slots); break; } case Operation::InternalLogin: { @@ -769,11 +775,59 @@ result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > cons break; } }; - auto const r = raft_service::instance()->srv_create_volume(volume_id, members, commit_cb); - if (r) { replica_manager::instance()->register_volume(volume_id, members); } + auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members, commit_cb); + if (r) { replica_manager::instance()->register_volume(vol_uuid, members); } return r; } -void MemCraftReplica::apply_sync(int64_t rs_commit_lsn, uint64_t client_token) {} +// Follower-side catch-up on SyncRSCommitLSN apply. Verdicts are already decided by the leader (empty_slots) +// -- this never decides Empty itself, only obeys the verdict list or fetches real data. +void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, + std::vector< int64_t > const& empty_slots) { + // Verdicts first -- permanent no-ops, no fetch needed. + for (auto lsn : empty_slots) + cold_mark_empty(lsn); + + uint64_t term; + { + std::lock_guard< std::mutex > g{mu_}; + term = state_.term; + } + + auto missing = get_missing_slots(rs_commit_lsn); + auto const peers = replica_manager::instance()->get_volume(vol_uuid); + for (auto const& peer : peers) { + if (missing.empty()) break; + if (peer.id == geo_.ep.id) continue; // don't ask self + + auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing)); + if (!r) continue; // unreachable, try next peer + + std::erase_if(missing, [&](int64_t lsn) { + auto const it = std::ranges::find_if(*r, [&](auto const& s) { return s.lsn == lsn; }); + if (it == r->end()) return false; // this peer doesn't have it either + if (it->is_empty) { + cold_mark_empty(lsn); // a prior verdict this peer already knows about + } else { + cold_install_slot(lsn, to_mem_journal_slot(*it, term)); + } + return true; + }); + } + + if (!missing.empty()) { + LOGERROR("apply_sync[vol={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " + "stall until the next SyncRSCommitLSN round -- first missing={}", + boost::uuids::to_string(vol_uuid), missing.size(), rs_commit_lsn, missing.front()); + } + + std::lock_guard< std::mutex > g{mu_}; + apply_up_to(rs_commit_lsn); +} + +session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { + std::lock_guard< std::mutex > g{mu_}; + return {state_.term, state_.client_token}; +} } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index bf5bb4e..029875f 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -111,6 +111,12 @@ struct server_geometry { uint32_t max_tx; }; +// for srv helo validation +struct session_info { + uint64_t term; + uint64_t client_token; +}; + // enable_shared_from_this: a write the transport timed out is delivered late, from the transport's timer // thread. That closure must hold a WEAK reference here (a strong one would cycle: replica -> net_ -> closure // -> replica), so the replica must be reachable as a shared_ptr. It always is; make_mem_replica_group is the @@ -213,6 +219,7 @@ class MemCraftReplica final : public craft_replica, return do_get_rs_commit_lsn(term, is_login); } result< std::vector< JournalSlot > > srv_fetch_data(std::vector< int64_t > const& lsns) { return do_fetch(lsns); } + session_info srv_session_info(std::array< uint8_t, 16 > const& volume_id) const; private: friend class MemTransport; // the cold path drives the cold_* / peek helpers below directly, and the IO @@ -281,7 +288,8 @@ class MemCraftReplica final : public craft_replica, void cold_truncate_above(int64_t rs_commit_lsn); // real hooks using raft channel - void apply_sync(int64_t rs_commit_lsn, uint64_t client_token); + void apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, + std::vector< int64_t > const& empty_slots); result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); // void apply_logout(); // void apply_truncate_above(int64_t rs_commit_lsn); @@ -290,8 +298,8 @@ class MemCraftReplica final : public craft_replica, void init_faults(); MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); std::vector< int64_t > get_missing_slots(int64_t watermark); - int64_t resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, - uint64_t term); + std::pair< std::vector< int64_t >, int64_t > + resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, uint64_t term); result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, uint64_t term); diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 594c917..39f93a7 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -158,8 +158,23 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { auto const hr = wire::decode< wire::helo_req >(req.op_header); + + // Fence: HELO must present the term + token of the session the replica already knows about. + // Only binds this connection if it matches. + auto const current = replica_->srv_session_info(hr.volume_id); + wire::status code = wire::status::ok; + + bool is_raft_enabled = raft_service::instance()->is_raft_enabled(); + + if (is_raft_enabled && (hr.term != current.term || hr.client_token != current.client_token)) { + code = wire::status::stale_term; + } else { + session_term_ = hr.term; + session_active_ = true; + } + std::vector< uint8_t > out; - wire::frame_message(out, wire::op::helo_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, {}, {}); + wire::frame_message(out, wire::op::helo_rsp, static_cast< uint8_t >(code), req.hdr.request_id, {}, {}); conn.send_all(out); } diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 1fd80b1..af9cd43 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -47,9 +47,8 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { }); } -result< void > raft_service::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, +result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members, raft_commit_cb_t cb) { - auto const group_id = craft::to_uuid(volume_id); auto consensus = raft_service::instance()->get_consensus(); // Seat THIS replica as leader by creating the group. diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index e268a8c..945394e 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -31,8 +31,8 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab bool is_raft_enabled() { return consensus_ != nullptr; } consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); - result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members, raft_commit_cb_t cb); + result< void > srv_create_volume(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members, + raft_commit_cb_t cb); bool is_leader(nuraft_mesg::group_id_t const& group_id); nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index fdf3d87..8da13ea 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -65,14 +65,14 @@ std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) return it->second; } -void replica_manager::register_volume(std::array< uint8_t, 16 > const& volume_id, +void replica_manager::register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members) { std::lock_guard< std::shared_mutex > g{mu_}; std::vector< replica_info > rinfos; for (auto const& m : members) { rinfos.emplace_back(replicas_[m.id]); } - volumes_[craft::to_uuid(volume_id)] = rinfos; + volumes_[vol_uuid] = rinfos; } std::vector< replica_info > replica_manager::get_volume(boost::uuids::uuid const& volume_id) { diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index b4bc280..8b7a733 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -37,7 +37,7 @@ class replica_manager { // wire-plane peer client: lazily connect-and-cache a CraftTcpPeer for this id. std::shared_ptr< net::CraftTcpPeer > get_peer_client(boost::uuids::uuid const& id); std::optional< replica_info > get(boost::uuids::uuid const& id) const; - void register_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members); + void register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members); std::vector< replica_info > get_volume(boost::uuids::uuid const& volume_id); private: From 93a84665b6e62a057bfecf3eb4c3c5c63d574b26 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 29 Jul 2026 03:25:07 -0700 Subject: [PATCH 07/24] do truncate above rs commit lsn in the internal login commit --- src/mem/replica.cpp | 27 ++++++++++++++++++++++++--- src/mem/replica.hpp | 6 ++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 004c044..5c86c99 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares @@ -604,7 +605,6 @@ std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply( } if (missing >= peers.size() / 2) { - cold_mark_empty(lsn); // adopt an already-committed verdict empty_slots.push_back(lsn); } else if (!is_data) { // unresolved lsn @@ -683,6 +683,10 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con } // Phase 3: InternalLogin(token, term) via RAFT + { + std::lock_guard< std::mutex > lk(login_mu_); + login_done_ = false; + } if (auto const r = raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term}); !r) { @@ -691,7 +695,13 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con } // Phase 4: truncate above rs_commit_lsn - cold_truncate_above(rs_commit_lsn); + // This happens in the internal login commit. Wait until that happens. + { + std::unique_lock< std::mutex > lk(login_mu_); + login_cv_.wait_for(lk, std::chrono::seconds(2), [&] { return login_done_; }); + if (!login_done_) { return std::unexpected(make_error_condition(craft_error::INTERNAL)); } + } + std::vector< replica_endpoint > replicas; for (auto const& m : members) { replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); @@ -767,7 +777,7 @@ result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > cons LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); return; } - cold_apply_login(m.client_token, m.term); + internal_login(m.client_token, m.term); break; } default: @@ -823,6 +833,7 @@ void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_ std::lock_guard< std::mutex > g{mu_}; apply_up_to(rs_commit_lsn); + rs_commit_lsn_.store(rs_commit_lsn, std::memory_order_relaxed); } session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { @@ -830,4 +841,14 @@ session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) return {state_.term, state_.client_token}; } +void MemCraftReplica::internal_login(uint64_t client_token, uint64_t term) { + cold_apply_login(client_token, term); + cold_truncate_above(rs_commit_lsn_.load(std::memory_order_relaxed)); + { + std::lock_guard< std::mutex > lk(login_mu_); + login_done_ = true; + } + login_cv_.notify_one(); +} + } // namespace craft diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 029875f..5353679 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -302,6 +302,7 @@ class MemCraftReplica final : public craft_replica, resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, uint64_t term); result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, uint64_t term); + void internal_login(uint64_t client_token, uint64_t term); // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. @@ -331,6 +332,11 @@ class MemCraftReplica final : public craft_replica, std::map< int64_t, MemJournalSlot > journal_; // dLSN -> slot (out-of-order arrival tolerated) std::map< lba_t, IndexCell > index_; // applied prefix (<= commit_lsn); an absent LBA is a hole mutable std::mutex mu_; + + std::atomic< int64_t > rs_commit_lsn_{-1}; + std::mutex login_mu_; + std::condition_variable login_cv_; + bool login_done_{false}; }; } // namespace craft From 6427c79d4103eccb43a4c2e3162956b516936865 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Tue, 28 Jul 2026 11:35:13 -0700 Subject: [PATCH 08/24] add python scripts to create replicas and craft disk and perform io tests --- .gitignore | 2 + src/raft/raft_service.cpp | 2 +- tools/craft_reference_tcp_srv.cpp | 20 ++--- tools/craft_test_driver/cluster.py | 55 ++++++++++++ tools/craft_test_driver/craft_disk.py | 72 +++++++++++++++ tools/craft_test_driver/craft_wire.py | 66 ++++++++++++++ tools/craft_test_driver/main.py | 125 ++++++++++++++++++++++++++ tools/craft_test_driver/volumes.py | 46 ++++++++++ tools/raft_bootstrap.py | 93 ------------------- 9 files changed, 377 insertions(+), 104 deletions(-) create mode 100644 tools/craft_test_driver/cluster.py create mode 100644 tools/craft_test_driver/craft_disk.py create mode 100644 tools/craft_test_driver/craft_wire.py create mode 100644 tools/craft_test_driver/main.py create mode 100644 tools/craft_test_driver/volumes.py delete mode 100644 tools/raft_bootstrap.py diff --git a/.gitignore b/.gitignore index 7f05ae5..bd98445 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ CMakeUserPresets.json compile_commands.json *.o *.a + +**/__pycache__ diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index af9cd43..167858b 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -38,7 +38,7 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { .mesg_port_ = my_port, .default_group_type_ = default_group_type_, }; - consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), false /*with_data_svc*/); + consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true /*with_data_svc*/); auto raft_params = nuraft::raft_params{}; consensus_->register_mgr_type(default_group_type_, raft_params); server_uuid_ = server_uuid; diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 49799df..c4d5611 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -70,7 +70,16 @@ void on_signal(int) { g_stop.store(true); } int main(int argc, char** argv) { SISL_OPTIONS_LOAD(argc, argv, SRV_OPTIONS); - sisl::logging::SetLogger("craft_reference_tcp_srv"); + auto id = boost::uuids::random_generator()(); + if (SISL_OPTIONS.count("server_uuid")) { + try { + id = boost::uuids::string_generator()(SISL_OPTIONS["server_uuid"].as< std::string >()); + } catch (std::exception const& e) { + std::cerr << "Invalid --server_uuid: " << e.what() << "\n"; + return 2; + } + } + sisl::logging::SetLogger(fmt::format("craft_tcp_srv_{}", SISL_OPTIONS["server_uuid"].as< std::string >())); sisl::logging::SetModuleLogLevel("nuraft_mesg", spdlog::level::info); sisl::logging::SetModuleLogLevel("grpc_server", spdlog::level::info); @@ -101,15 +110,6 @@ int main(int argc, char** argv) { // Advertise this one replica in login_rsp. The id is cosmetic here (the client routes by index, and HELO // fences by term, not id) -- a fresh random id is fine; the client's --craft-tcp supplies its own members. - auto id = boost::uuids::random_generator()(); - if (SISL_OPTIONS.count("server_uuid")) { - try { - id = boost::uuids::string_generator()(SISL_OPTIONS["server_uuid"].as< std::string >()); - } catch (std::exception const& e) { - std::cerr << "Invalid --server_uuid: " << e.what() << "\n"; - return 2; - } - } auto geo = craft::server_geometry{.capacity = capacity, .lba_size = lba_size, diff --git a/tools/craft_test_driver/cluster.py b/tools/craft_test_driver/cluster.py new file mode 100644 index 0000000..03414b2 --- /dev/null +++ b/tools/craft_test_driver/cluster.py @@ -0,0 +1,55 @@ +# cluster.py +import signal +import subprocess +import sys +from pathlib import Path + + +class ClusterManager: + """Owns a set of craft_reference_tcp_srv subprocesses. Guarantees cleanup on exit, even if + the script is interrupted or a later step raises.""" + + def __init__(self, binary: Path, config_path: Path, members: list[dict]): + self.binary = binary + self.config_path = config_path + self.members = members + self.procs: dict[str, subprocess.Popen] = {} + + def start_all(self): + for m in self.members: + self._start_one(m) + + def _start_one(self, member: dict): + uuid_ = member["uuid"] + port = member["tcp_port"] + cmd = [ + str(self.binary), + "--port", str(port), + "--server_config_file", str(self.config_path), + "--server_uuid", uuid_, + ] + print(f"starting: {' '.join(cmd)}") + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, start_new_session=True) + self.procs[uuid_] = proc + + def any_dead(self) -> list[str]: + return [u for u, p in self.procs.items() if p.poll() is not None] + + def shutdown(self): + for proc in self.procs.values(): + if proc.poll() is None: + proc.send_signal(signal.SIGTERM) + for uuid_, proc in self.procs.items(): + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + print(f"server {uuid_} did not exit on SIGTERM, killing", file=sys.stderr) + proc.kill() + proc.wait() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.shutdown() \ No newline at end of file diff --git a/tools/craft_test_driver/craft_disk.py b/tools/craft_test_driver/craft_disk.py new file mode 100644 index 0000000..5521055 --- /dev/null +++ b/tools/craft_test_driver/craft_disk.py @@ -0,0 +1,72 @@ +# craft_disk.py +import re +import signal +import subprocess +import sys +import time +from pathlib import Path + +from volumes import Volume + + +class CraftDisk: + """Owns one ublkpp_disk --craft_tcp subprocess attaching to a Volume. Parses the resulting + /dev/ublkbN path from stdout once ublkpp_tgt::run() reports the device exposed.""" + + _DEVICE_RE = re.compile(r"exposed as UBD device: \[(/dev/ublkb\d+)\]") + + def __init__(self, binary: Path, vol: Volume, ready_timeout: float = 15.0): + self.binary = binary + self.vol = vol + self.ready_timeout = ready_timeout + self.proc: subprocess.Popen | None = None + self.device_path: str | None = None + + def start(self): + endpoints = ",".join(f"{m['host']}:{m['tcp_port']}" for m in self.vol.members) + cmd = [ + str(self.binary), + "--craft_tcp", endpoints, + "--vol_id", str(self.vol.vol_id), + ] + print(f"starting: {' '.join(cmd)}") + self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, start_new_session=True) + self._wait_ready() + + def _wait_ready(self): + deadline = time.monotonic() + self.ready_timeout + while time.monotonic() < deadline: + if self.proc.poll() is not None: + raise RuntimeError(f"ublkpp_disk exited early (code={self.proc.returncode}) " + f"before exposing a device -- check its output above") + line = self.proc.stdout.readline() + if not line: + continue + print(f"[ublkpp_disk] {line.rstrip()}") + m = self._DEVICE_RE.search(line) + if m: + self.device_path = m.group(1) + return + raise TimeoutError(f"ublkpp_disk did not expose a device within {self.ready_timeout}s") + + def is_alive(self) -> bool: + return self.proc is not None and self.proc.poll() is None + + def stop(self): + if self.proc is None or self.proc.poll() is not None: + return + self.proc.send_signal(signal.SIGTERM) + try: + self.proc.wait(timeout=5) + except subprocess.TimeoutExpired: + print("ublkpp_disk did not exit on SIGTERM, killing", file=sys.stderr) + self.proc.kill() + self.proc.wait() + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc, tb): + self.stop() \ No newline at end of file diff --git a/tools/craft_test_driver/craft_wire.py b/tools/craft_test_driver/craft_wire.py new file mode 100644 index 0000000..c63fbeb --- /dev/null +++ b/tools/craft_test_driver/craft_wire.py @@ -0,0 +1,66 @@ +import socket +import struct +import uuid + + +MSG_HDR = " bytes: + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError( + f"peer closed after {len(buf)}/{n} bytes -- likely a server-side parse_message() " + f"rejection (unknown_op / bad_length / bad_digest)" + ) + buf += chunk + return bytes(buf) + + +def put_member(member_id: bytes, addr: str) -> bytes: + if len(member_id) != 16: + raise ValueError(f"member id must be 16 bytes, got {len(member_id)}") + addr_b = addr.encode() + return member_id + struct.pack(" int: + vol_bytes = volume_id.bytes + body = b"".join(put_member(mid.bytes, addr) for mid, addr in members) + op_header = struct.pack(VOLUME_CREATE_REQ, vol_bytes, capacity, lba_size, len(members)) + hdr = struct.pack(MSG_HDR, OP_CREATE_VOLUME, 0, 1, len(body)) + + try: + s = socket.create_connection((host, port), timeout=timeout) + except OSError as e: + raise ConnectionError(f"could not connect to {host}:{port}: {e}") from e + + try: + s.sendall(hdr + op_header + body) + resp_hdr = recv_exact(s, MSG_HDR_SIZE) + print(f"create_volume raw response header: {resp_hdr!r}") + op, status, rid, resp_body_len = struct.unpack(MSG_HDR, resp_hdr) + print(f"create_volume decoded: op={op} status={status} request_id={rid} body_len={resp_body_len}") + if resp_body_len: + resp_body = recv_exact(s, resp_body_len) + print(f"create_volume response body: {resp_body!r}") + return status + finally: + s.close() + +def status_name(status: int) -> str: + return STATUS_NAMES.get(status, f"unknown({status})") \ No newline at end of file diff --git a/tools/craft_test_driver/main.py b/tools/craft_test_driver/main.py new file mode 100644 index 0000000..d99d46f --- /dev/null +++ b/tools/craft_test_driver/main.py @@ -0,0 +1,125 @@ +# main.py +import argparse +import json +import sys +import time +import uuid +from pathlib import Path + +from cluster import ClusterManager +from craft_disk import CraftDisk +from volumes import VolumeRegistry + + +def parse_args(): + p = argparse.ArgumentParser(description="CRAFT reference cluster test harness") + p.add_argument("--config", required=True, type=Path, + help="server_config.json path (members: uuid, host, raft_port, tcp_port)") + p.add_argument("--tcp_srv_binary", default=Path("./craft_reference_tcp_srv"), type=Path, + help="path to the craft_reference_tcp_srv executable") + p.add_argument("--craft_disk_binary", type=Path, + help="path to the ublkpp_disk executable") + p.add_argument("--startup-wait", type=float, default=1.0, + help="seconds to wait after spawning servers before assuming they are up") + p.add_argument("--vol-id", type=str, default=None, + help="volume UUID to create (default: random, printed for reuse)") + p.add_argument("--capacity", type=int, default=1 << 30, help="volume capacity in bytes") + p.add_argument("--lba-size", type=int, default=4096, help="volume block size in bytes") + p.add_argument("--cleanup", action="store_true", + help="kill any craft_reference_tcp_srv / ublkpp_disk processes left over, then exit") + args = p.parse_args() + args.tcp_srv_binary = args.tcp_srv_binary.resolve() + if args.craft_disk_binary: + args.craft_disk_binary = args.craft_disk_binary.resolve() + return args + + +def load_members(config_path: Path) -> list[dict]: + with open(config_path) as f: + cfg = json.load(f) + members = cfg["members"] + if not members: + raise ValueError(f"{config_path}: no members defined") + return members + + +def cleanup_stray_processes(): + # Best-effort kill of leftover craft_reference_tcp_srv / ublkpp_disk processes by name + import subprocess + for name in ("craft_reference_tcp_srv", "ublkpp_disk"): + result = subprocess.run(["pkill", "-9", "-f", name], capture_output=True) + if result.returncode == 0: + print(f"killed process(es) matching '{name}'") + elif result.returncode == 1: + print(f"no process matching '{name}' found") + else: + print(f"pkill for '{name}' failed: {result.stderr.decode().strip()}", file=sys.stderr) + + +def run(args, members): + vol_id = uuid.UUID(args.vol_id) if args.vol_id else None + cluster = ClusterManager(args.tcp_srv_binary, args.config, members) + disk = None + + try: + cluster.start_all() + print(f"waiting {args.startup_wait}s for servers to come up...") + time.sleep(args.startup_wait) + + dead = cluster.any_dead() + if dead: + raise RuntimeError(f"servers failed to start: {dead}") + print("all server processes alive, proceeding to create_volume") + + registry = VolumeRegistry() + vol = registry.create(members, vol_id=vol_id, capacity=args.capacity, lba_size=args.lba_size) + print(f"volume created, waiting {args.startup_wait}s before attaching client...") + time.sleep(args.startup_wait) + + if args.craft_disk_binary: + disk = CraftDisk(args.craft_disk_binary, vol) + disk.start() + print(f"{len(members)} server(s) running, {vol}, disk at {disk.device_path}, " + f"press Ctrl+C to stop") + else: + print(f"{len(members)} server(s) running, {vol}, press Ctrl+C to stop") + + while True: + time.sleep(1) + dead = cluster.any_dead() + if dead: + raise RuntimeError(f"server(s) died unexpectedly: {dead}") + if disk is not None and not disk.is_alive(): + raise RuntimeError("ublkpp_disk exited unexpectedly") + + except KeyboardInterrupt: + print("Ctrl+C -- shutting down...") + if disk is not None: + disk.stop() + cluster.shutdown() + + except Exception as e: + print(f"error: {e}", file=sys.stderr) + print("leaving processes running for inspection. " + "PIDs:", file=sys.stderr) + for uuid_, proc in cluster.procs.items(): + print(f" server {uuid_}: pid={proc.pid}", file=sys.stderr) + if disk is not None and disk.proc is not None: + print(f" ublkpp_disk: pid={disk.proc.pid}", file=sys.stderr) + print("re-run with --cleanup to kill them later.", file=sys.stderr) + sys.exit(1) + + +def main(): + args = parse_args() + + if args.cleanup: + cleanup_stray_processes() + return + + members = load_members(args.config) + run(args, members) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/craft_test_driver/volumes.py b/tools/craft_test_driver/volumes.py new file mode 100644 index 0000000..afdd769 --- /dev/null +++ b/tools/craft_test_driver/volumes.py @@ -0,0 +1,46 @@ +# volumes.py +import uuid + +from craft_wire import create_volume, status_name + + +class Volume: + def __init__(self, vol_id: uuid.UUID, members: list[dict], capacity: int, lba_size: int): + self.vol_id = vol_id + self.members = members # subset of cluster members hosting this volume + self.capacity = capacity + self.lba_size = lba_size + + def __repr__(self): + return f"Volume({self.vol_id}, members={len(self.members)}, capacity={self.capacity})" + + +class VolumeRegistry: + """Test-harness-side bookkeeping of volumes created on the cluster -- not server state, just + what this test run knows it asked for, so later steps (client attach, fault injection) can + look volumes up instead of threading vol_id/members through every function call.""" + + def __init__(self): + self._volumes: dict[uuid.UUID, Volume] = {} + + def create(self, members: list[dict], vol_id: uuid.UUID = None, + capacity: int = 1 << 30, lba_size: int = 4096) -> Volume: + vol_id = vol_id or uuid.uuid4() + leader = members[0] # TODO: revisit once leader isn't assumed to be members[0] + wire_members = [(uuid.UUID(m["uuid"]), f"{m['host']}:{m['tcp_port']}") for m in members] + + status = create_volume(leader["host"], leader["tcp_port"], vol_id, capacity, lba_size, wire_members) + name = status_name(status) + print(f"create_volume({vol_id}) -> status={status} ({name})") + if status != 0: + raise RuntimeError(f"create_volume failed: {name}") + + vol = Volume(vol_id, members, capacity, lba_size) + self._volumes[vol_id] = vol + return vol + + def get(self, vol_id: uuid.UUID) -> Volume: + return self._volumes[vol_id] + + def all(self) -> list[Volume]: + return list(self._volumes.values()) \ No newline at end of file diff --git a/tools/raft_bootstrap.py b/tools/raft_bootstrap.py deleted file mode 100644 index 1762632..0000000 --- a/tools/raft_bootstrap.py +++ /dev/null @@ -1,93 +0,0 @@ -import socket -import struct -import sys -import uuid - -# msg_hdr: op(u8) status(u8) request_id(u16) body_len(u32) -- 8 bytes, little-endian -MSG_HDR = " bytes: - buf = bytearray() - while len(buf) < n: - chunk = sock.recv(n - len(buf)) - if not chunk: - raise ConnectionError( - f"peer closed after {len(buf)}/{n} bytes -- likely a server-side parse_message() " - f"rejection (unknown_op / bad_length / bad_digest);" - ) - buf += chunk - return bytes(buf) - - -def put_member(member_id: bytes, addr: str) -> bytes: - if len(member_id) != 16: - raise ValueError(f"member id must be 16 bytes, got {len(member_id)}") - addr_b = addr.encode() - return member_id + struct.pack(" int: - if len(volume_id) != 16: - raise ValueError(f"volume_id must be 16 bytes, got {len(volume_id)}") - - body = b"".join(put_member(mid, addr) for mid, addr in members) - op_header = struct.pack(VOLUME_CREATE_REQ, volume_id, capacity, lba_size, len(members)) - body_len = len(body) - hdr = struct.pack(MSG_HDR, OP_CREATE_VOLUME, 0, 1, body_len) - - try: - s = socket.create_connection((host, port), timeout=timeout) - except OSError as e: - raise ConnectionError(f"could not connect to {host}:{port}: {e}") from e - - try: - s.sendall(hdr + op_header + body) - resp_hdr = recv_exact(s, MSG_HDR_SIZE) - op, status, request_id, resp_body_len = struct.unpack(MSG_HDR, resp_hdr) - if resp_body_len: - recv_exact(s, resp_body_len) - return status - finally: - s.close() - - -if __name__ == "__main__": - volume_id = (1).to_bytes(16, "little") - - #member_ids = [uuid.uuid4() for _ in range(3)] - # Fixed for debugging -- swap back to uuid.uuid4() once the flow is finalized. - member_ids = [ - uuid.UUID("11111111-1111-1111-1111-111111111111"), - uuid.UUID("22222222-2222-2222-2222-222222222222"), - uuid.UUID("33333333-3333-3333-3333-333333333333"), - ] - addrs = ["127.0.0.1:8001", "127.0.0.1:8002", "127.0.0.1:8003"] - members = [(mid.bytes, addr) for mid, addr in zip(member_ids, addrs)] - - for mid, addr in zip(member_ids, addrs): - print(f"member {mid} @ {addr}") - - try: - status = create_volume("127.0.0.1", 7001, volume_id, capacity=1 << 30, - lba_size=4096, members=members) - except (ConnectionError, ValueError, socket.timeout) as e: - print(f"create_volume failed: {e}", file=sys.stderr) - sys.exit(1) - - name = STATUS_NAMES.get(status, f"unknown({status})") - print(f"status: {status} ({name})") - sys.exit(0 if status == 0 else 1) \ No newline at end of file From bfdeefba5cb8572bb06828b014b53269a19bcbcd Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Thu, 30 Jul 2026 01:52:16 -0700 Subject: [PATCH 09/24] add basic fio test --- src/mem/replica.cpp | 4 ++ src/net/tcp_server.cpp | 2 +- src/replica_mgr.cpp | 32 +++++------- src/replica_mgr.hpp | 5 +- tools/craft_test_driver/craft_disk.py | 62 +++++++++++++++++----- tools/craft_test_driver/craft_wire.py | 13 +++-- tools/craft_test_driver/main.py | 74 +++++++++++++++++++-------- tools/craft_test_driver/volumes.py | 7 ++- 8 files changed, 135 insertions(+), 64 deletions(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 5c86c99..febc7de 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -651,7 +651,10 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con // 1.1: accepted by leader only. // TODO: what happens if the leader changes before the login is complete? auto vol_uuid = craft::to_uuid(volume_id); + LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(vol_uuid), client_token, + term); if (!raft_service_inst->is_leader(vol_uuid)) { + LOGERROR("current replica not a raft leader"); return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; } @@ -666,6 +669,7 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con auto const members = replica_manager::instance()->get_volume(vol_uuid); for (auto const m : members) { + if (m.id == geo_.ep.id) { continue; } if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term, true /* is_login */)); r) { peer_resp.emplace_back(r.value()); } diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 39f93a7..be03780 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -43,7 +43,7 @@ craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& serve // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { - replica_manager::instance()->start_replica_service(server_config_file); + replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id); raft_service::instance()->start_raft_service(geo.ep.id); } replica_ = std::make_shared< MemCraftReplica >(std::move(geo)); diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index 8da13ea..88f3083 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -14,7 +14,8 @@ std::shared_ptr< replica_manager > replica_manager::instance() { return inst; } -void replica_manager::start_replica_service(std::string const& path) { +void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid) { + id_ = my_uuid; std::ifstream istrm(path, std::ios::binary); if (!istrm.is_open()) { LOGERROR("replica_manager: could not open {}", path); @@ -31,12 +32,18 @@ void replica_manager::start_replica_service(std::string const& path) { replicas_.clear(); for (auto const& m : j.at("members")) { - replica_info info; - info.id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); - info.host = m.at("host").get< std::string >(); - info.raft_port = m.at("raft_port").get< uint16_t >(); - info.tcp_port = m.at("tcp_port").get< uint16_t >(); - replicas_[info.id] = std::move(info); + auto const id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); + replicas_.emplace(id, + replica_info{ + .id = id, + .host = m.at("host").get< std::string >(), + .raft_port = m.at("raft_port").get< uint16_t >(), + .tcp_port = m.at("tcp_port").get< uint16_t >(), + .peer_client = (id == my_uuid) + ? nullptr + : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), + m.at("tcp_port").get< uint16_t >(), id), + }); } } @@ -47,17 +54,6 @@ std::string replica_manager::lookup_peer(boost::uuids::uuid const& id) const { return it->second.host + ":" + std::to_string(it->second.raft_port); } -std::shared_ptr< net::CraftTcpPeer > replica_manager::get_peer_client(boost::uuids::uuid const& id) { - std::lock_guard< std::shared_mutex > g{mu_}; - - auto const rit = replicas_.find(id); - if (rit == replicas_.end()) return nullptr; - if (!rit->second.peer_client) { - rit->second.peer_client = std::make_shared< net::CraftTcpPeer >(rit->second.host, rit->second.tcp_port, id); - } - return rit->second.peer_client; -} - std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) const { std::shared_lock< std::shared_mutex > g(mu_); auto const it = replicas_.find(id); diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index 8b7a733..8d31084 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -30,12 +30,10 @@ class replica_manager { public: static std::shared_ptr< replica_manager > instance(); - void start_replica_service(std::string const& path); + void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid); // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". std::string lookup_peer(boost::uuids::uuid const& id) const; - // wire-plane peer client: lazily connect-and-cache a CraftTcpPeer for this id. - std::shared_ptr< net::CraftTcpPeer > get_peer_client(boost::uuids::uuid const& id); std::optional< replica_info > get(boost::uuids::uuid const& id) const; void register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members); std::vector< replica_info > get_volume(boost::uuids::uuid const& volume_id); @@ -43,6 +41,7 @@ class replica_manager { private: replica_manager() = default; + boost::uuids::uuid id_; mutable std::shared_mutex mu_; std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once std::map< boost::uuids::uuid, std::vector< replica_info > > volumes_; diff --git a/tools/craft_test_driver/craft_disk.py b/tools/craft_test_driver/craft_disk.py index 5521055..6ff4ceb 100644 --- a/tools/craft_test_driver/craft_disk.py +++ b/tools/craft_test_driver/craft_disk.py @@ -1,5 +1,6 @@ -# craft_disk.py +import logging import re +import select import signal import subprocess import sys @@ -7,6 +8,9 @@ from pathlib import Path from volumes import Volume +import registry + +logger = logging.getLogger(__name__) class CraftDisk: @@ -25,30 +29,60 @@ def __init__(self, binary: Path, vol: Volume, ready_timeout: float = 15.0): def start(self): endpoints = ",".join(f"{m['host']}:{m['tcp_port']}" for m in self.vol.members) cmd = [ + "stdbuf", + "-oL", str(self.binary), - "--craft_tcp", endpoints, - "--vol_id", str(self.vol.vol_id), + "--craft_tcp", + endpoints, + "--vol_id", + str(self.vol.vol_id), + "--log_mods", + "ublksrv:info,ublk_tgt:info,ublk_raid:info,ublk_drivers:info", ] - print(f"starting: {' '.join(cmd)}") - self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, start_new_session=True) + logger.info("starting: %s", " ".join(cmd)) + self.proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + start_new_session=True, + ) self._wait_ready() def _wait_ready(self): deadline = time.monotonic() + self.ready_timeout - while time.monotonic() < deadline: - if self.proc.poll() is not None: - raise RuntimeError(f"ublkpp_disk exited early (code={self.proc.returncode}) " - f"before exposing a device -- check its output above") + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + f"ublkpp_disk did not expose a device within {self.ready_timeout}s" + ) + + if (rc := self.proc.poll()) is not None: + if rc < 0: + # negative returncode == killed by signal -rc (SIGABRT=6, SIGSEGV=11, etc.) + raise RuntimeError( + f"ublkpp_disk CRASHED (signal {-rc}, likely core dumped) " + f"before exposing a device -- check its output above" + ) + raise RuntimeError( + f"ublkpp_disk exited early (code={rc}) " + f"before exposing a device -- check its output above" + ) + + ready, _, _ = select.select([self.proc.stdout], [], [], min(remaining, 1.0)) + if not ready: + continue + line = self.proc.stdout.readline() if not line: continue - print(f"[ublkpp_disk] {line.rstrip()}") + logger.info("[ublkpp_disk] %s", line.rstrip()) m = self._DEVICE_RE.search(line) if m: self.device_path = m.group(1) return - raise TimeoutError(f"ublkpp_disk did not expose a device within {self.ready_timeout}s") def is_alive(self) -> bool: return self.proc is not None and self.proc.poll() is None @@ -60,7 +94,7 @@ def stop(self): try: self.proc.wait(timeout=5) except subprocess.TimeoutExpired: - print("ublkpp_disk did not exit on SIGTERM, killing", file=sys.stderr) + logger.warning("ublkpp_disk did not exit on SIGTERM, killing") self.proc.kill() self.proc.wait() @@ -69,4 +103,4 @@ def __enter__(self): return self def __exit__(self, exc_type, exc, tb): - self.stop() \ No newline at end of file + self.stop() diff --git a/tools/craft_test_driver/craft_wire.py b/tools/craft_test_driver/craft_wire.py index c63fbeb..4a193ab 100644 --- a/tools/craft_test_driver/craft_wire.py +++ b/tools/craft_test_driver/craft_wire.py @@ -1,6 +1,9 @@ import socket import struct import uuid +import logging + +logger = logging.getLogger(__name__) MSG_HDR = " str: - return STATUS_NAMES.get(status, f"unknown({status})") \ No newline at end of file + return STATUS_NAMES.get(status, f"unknown({status})") diff --git a/tools/craft_test_driver/main.py b/tools/craft_test_driver/main.py index d99d46f..b141c13 100644 --- a/tools/craft_test_driver/main.py +++ b/tools/craft_test_driver/main.py @@ -1,20 +1,33 @@ -# main.py import argparse import json import sys import time import uuid from pathlib import Path +import logging from cluster import ClusterManager -from craft_disk import CraftDisk from volumes import VolumeRegistry +from craft_disk import CraftDisk +import registry +from test_registry import run_test, TestNotFoundError, list_tests + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(filename)s:%(funcName)s:%(lineno)d] %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger(__name__) def parse_args(): p = argparse.ArgumentParser(description="CRAFT reference cluster test harness") - p.add_argument("--config", required=True, type=Path, - help="server_config.json path (members: uuid, host, raft_port, tcp_port)") + p.add_argument( + "--config", + type=Path, + default=Path("server_config.json"), + help="server_config.json path (members: uuid, host, raft_port, tcp_port)", + ) p.add_argument("--tcp_srv_binary", default=Path("./craft_reference_tcp_srv"), type=Path, help="path to the craft_reference_tcp_srv executable") p.add_argument("--craft_disk_binary", type=Path, @@ -27,6 +40,9 @@ def parse_args(): p.add_argument("--lba-size", type=int, default=4096, help="volume block size in bytes") p.add_argument("--cleanup", action="store_true", help="kill any craft_reference_tcp_srv / ublkpp_disk processes left over, then exit") + p.add_argument("--run-test", action="append", default=[], + help="name of a test to run after attaching (repeatable); see --list-tests") + p.add_argument("--list-tests", action="store_true", help="list available tests and exit") args = p.parse_args() args.tcp_srv_binary = args.tcp_srv_binary.resolve() if args.craft_disk_binary: @@ -49,40 +65,50 @@ def cleanup_stray_processes(): for name in ("craft_reference_tcp_srv", "ublkpp_disk"): result = subprocess.run(["pkill", "-9", "-f", name], capture_output=True) if result.returncode == 0: - print(f"killed process(es) matching '{name}'") + logger.info(f"killed process(es) matching '{name}'") elif result.returncode == 1: - print(f"no process matching '{name}' found") + logger.info(f"no process matching '{name}' found") else: - print(f"pkill for '{name}' failed: {result.stderr.decode().strip()}", file=sys.stderr) + logger.error(f"pkill for '{name}' failed: {result.stderr.decode().strip()}") def run(args, members): vol_id = uuid.UUID(args.vol_id) if args.vol_id else None cluster = ClusterManager(args.tcp_srv_binary, args.config, members) + registry.set_cluster(cluster) disk = None try: cluster.start_all() - print(f"waiting {args.startup_wait}s for servers to come up...") + logger.info(f"waiting {args.startup_wait}s for servers to come up...") time.sleep(args.startup_wait) dead = cluster.any_dead() if dead: raise RuntimeError(f"servers failed to start: {dead}") - print("all server processes alive, proceeding to create_volume") + logger.info("all server processes alive, proceeding to create_volume") - registry = VolumeRegistry() - vol = registry.create(members, vol_id=vol_id, capacity=args.capacity, lba_size=args.lba_size) - print(f"volume created, waiting {args.startup_wait}s before attaching client...") + vol = registry.volumes.create(members, vol_id=vol_id, capacity=args.capacity, lba_size=args.lba_size) + logger.info(f"volume created, waiting {args.startup_wait}s before attaching client...") time.sleep(args.startup_wait) if args.craft_disk_binary: disk = CraftDisk(args.craft_disk_binary, vol) disk.start() - print(f"{len(members)} server(s) running, {vol}, disk at {disk.device_path}, " - f"press Ctrl+C to stop") + registry.add_disk(str(vol.vol_id), disk) + logger.info(f"{len(members)} server(s) running, {vol}, disk at {disk.device_path}, " + f"press Ctrl+C to stop") else: - print(f"{len(members)} server(s) running, {vol}, press Ctrl+C to stop") + logger.info(f"{len(members)} server(s) running, {vol}, press Ctrl+C to stop") + + time.sleep(args.startup_wait) + if args.run_test: + for name in args.run_test: + try: + run_test(name, disk.device_path) + except TestNotFoundError as e: + logger.error(str(e)) + raise while True: time.sleep(1) @@ -93,26 +119,30 @@ def run(args, members): raise RuntimeError("ublkpp_disk exited unexpectedly") except KeyboardInterrupt: - print("Ctrl+C -- shutting down...") + logger.info("Ctrl+C -- shutting down...") if disk is not None: disk.stop() cluster.shutdown() except Exception as e: - print(f"error: {e}", file=sys.stderr) - print("leaving processes running for inspection. " - "PIDs:", file=sys.stderr) + logger.error(f"error: {e}") + logger.error("leaving processes running for inspection. PIDs:") for uuid_, proc in cluster.procs.items(): - print(f" server {uuid_}: pid={proc.pid}", file=sys.stderr) + logger.error(f" server {uuid_}: pid={proc.pid}") if disk is not None and disk.proc is not None: - print(f" ublkpp_disk: pid={disk.proc.pid}", file=sys.stderr) - print("re-run with --cleanup to kill them later.", file=sys.stderr) + logger.error(f" ublkpp_disk: pid={disk.proc.pid}") + logger.error("re-run with --cleanup to kill them later.") sys.exit(1) def main(): args = parse_args() + if args.list_tests: + for name in list_tests(): + print(name) + return + if args.cleanup: cleanup_stray_processes() return diff --git a/tools/craft_test_driver/volumes.py b/tools/craft_test_driver/volumes.py index afdd769..4aed579 100644 --- a/tools/craft_test_driver/volumes.py +++ b/tools/craft_test_driver/volumes.py @@ -1,8 +1,11 @@ # volumes.py import uuid +import logging from craft_wire import create_volume, status_name +logger = logging.getLogger(__name__) + class Volume: def __init__(self, vol_id: uuid.UUID, members: list[dict], capacity: int, lba_size: int): @@ -31,7 +34,7 @@ def create(self, members: list[dict], vol_id: uuid.UUID = None, status = create_volume(leader["host"], leader["tcp_port"], vol_id, capacity, lba_size, wire_members) name = status_name(status) - print(f"create_volume({vol_id}) -> status={status} ({name})") + logger.info(f"create_volume({vol_id}) -> status={status} ({name})") if status != 0: raise RuntimeError(f"create_volume failed: {name}") @@ -43,4 +46,4 @@ def get(self, vol_id: uuid.UUID) -> Volume: return self._volumes[vol_id] def all(self) -> list[Volume]: - return list(self._volumes.values()) \ No newline at end of file + return list(self._volumes.values()) From fc9e621899a3eccbfef213c22c95cbc8377dfc27 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sat, 1 Aug 2026 09:50:07 -0700 Subject: [PATCH 10/24] Add more helper scripts for the testing. Bug fixes from running the test --- conanfile.py | 96 ++++++++++--- include/craft/types.hpp | 4 +- src/client.cpp | 3 +- src/client_impl.hpp | 2 + src/craft_peer.hpp | 2 + src/mem/replica.cpp | 174 ++++++++++++++++++----- src/mem/replica.hpp | 2 +- src/net/async_conn.cpp | 6 +- src/net/tcp_peer.cpp | 74 +++++++++- src/net/tcp_peer.hpp | 5 +- src/net/tcp_replica.cpp | 42 +++--- src/net/tcp_replica.hpp | 2 +- src/net/tcp_server.cpp | 56 ++++++-- src/raft/raft_service.cpp | 31 ++-- src/raft/raft_service.hpp | 11 +- src/raft/raft_state_machine.hpp | 32 +++-- src/raft/raft_state_manager.cpp | 2 +- src/replica_mgr.cpp | 12 +- src/replica_mgr.hpp | 3 +- tools/craft_reference_tcp_srv.cpp | 6 +- tools/craft_test_driver/cluster.py | 28 +++- tools/craft_test_driver/craft_disk.py | 96 +++++++------ tools/craft_test_driver/fio_runner.py | 39 +++++ tools/craft_test_driver/main.py | 9 +- tools/craft_test_driver/registry.py | 26 ++++ tools/craft_test_driver/test_registry.py | 25 ++++ tools/craft_test_driver/tests/basic.py | 42 ++++++ 27 files changed, 639 insertions(+), 191 deletions(-) create mode 100644 tools/craft_test_driver/fio_runner.py create mode 100644 tools/craft_test_driver/registry.py create mode 100644 tools/craft_test_driver/test_registry.py create mode 100644 tools/craft_test_driver/tests/basic.py diff --git a/conanfile.py b/conanfile.py index 05d60e2..e764e96 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,9 +10,11 @@ class CraftClientConan(ConanFile): name = "craft_client" - version = "0.3.0" + version = "0.4.3" - description = "CRAFT reference client + wire protocol -- transport-agnostic, HomeStore-free" + description = ( + "CRAFT reference client + wire protocol -- transport-agnostic, HomeStore-free" + ) topics = ("ebay", "craft") license = "Apache-2.0" @@ -21,7 +23,7 @@ class CraftClientConan(ConanFile): options = { "shared": [True, False], "fPIC": [True, False], - "coverage": ['True', 'False'], + "coverage": ["True", "False"], "sanitize": ["address", "thread", "False"], } default_options = { @@ -31,7 +33,16 @@ class CraftClientConan(ConanFile): "sanitize": "False", } - exports_sources = ("CMakeLists.txt", "cmake/*", "include/*", "src/*", "test/*", "tools/*", "tsan.supp", "LICENSE") + exports_sources = ( + "CMakeLists.txt", + "cmake/*", + "include/*", + "src/*", + "test/*", + "tools/*", + "tsan.supp", + "LICENSE", + ) def configure(self): if self.options.shared: @@ -40,10 +51,14 @@ def configure(self): def config_options(self): if self.settings.build_type == "Debug": if self.options.coverage and self.options.sanitize: - raise ConanInvalidConfiguration("Sanitizer does not work with Code Coverage!") + raise ConanInvalidConfiguration( + "Sanitizer does not work with Code Coverage!" + ) if self.conf.get("tools.build:skip_test", default=False): if self.options.coverage or self.options.sanitize: - raise ConanInvalidConfiguration("Coverage/Sanitizer requires Testing!") + raise ConanInvalidConfiguration( + "Coverage/Sanitizer requires Testing!" + ) def build_requirements(self): self.test_requires("gtest/[^1.17]") @@ -81,7 +96,11 @@ def layout(self): # consumer falls back to /lib and cannot find the libs. for comp in ("craft_wire", "craft_types", "craft_client", "craft_reference"): self.cpp.source.components[comp].includedirs = ["include"] - for comp in ("craft_wire", "craft_client", "craft_reference"): # craft_types is header-only + for comp in ( + "craft_wire", + "craft_client", + "craft_reference", + ): # craft_types is header-only self.cpp.build.components[comp].libdirs = ["."] def generate(self): @@ -90,14 +109,14 @@ def generate(self): tc.variables["CTEST_OUTPUT_ON_FAILURE"] = "ON" if self.settings.build_type == "Debug": if self.options.get_safe("coverage"): - tc.variables['BUILD_COVERAGE'] = 'ON' + tc.variables["BUILD_COVERAGE"] = "ON" elif self.options.get_safe("sanitize") and self.options.sanitize != "False": if self.options.sanitize == "thread": - tc.variables['THREAD_SANITIZER_ON'] = 'ON' + tc.variables["THREAD_SANITIZER_ON"] = "ON" else: # address - tc.variables['ADDRESS_SANITIZER_ON'] = 'ON' + tc.variables["ADDRESS_SANITIZER_ON"] = "ON" if self.settings.build_type != "Debug": - tc.variables['TCMALLOC_ON'] = 'ON' + tc.variables["TCMALLOC_ON"] = "ON" tc.generate() CMakeDeps(self).generate() @@ -109,10 +128,28 @@ def build(self): cmake.test() def package(self): - copy(self, "LICENSE", self.source_folder, join(self.package_folder, "licenses"), keep_path=False) - copy(self, "*.h*", join(self.source_folder, "include"), join(self.package_folder, "include"), keep_path=True) + copy( + self, + "LICENSE", + self.source_folder, + join(self.package_folder, "licenses"), + keep_path=False, + ) + copy( + self, + "*.h*", + join(self.source_folder, "include"), + join(self.package_folder, "include"), + keep_path=True, + ) for pat in ("*.a", "*.lib", "*.so*", "*.dylib*"): - copy(self, pat, self.build_folder, join(self.package_folder, "lib"), keep_path=False) + copy( + self, + pat, + self.build_folder, + join(self.package_folder, "lib"), + keep_path=False, + ) def package_info(self): # Components, so a consumer links ONLY what it uses and the one-way dependency graph is enforced: @@ -128,8 +165,33 @@ def package_info(self): self.cpp_info.components["craft_types"].requires = ["sisl::sisl"] self.cpp_info.components["craft_client"].libs = ["craft_client"] - self.cpp_info.components["craft_client"].requires = ["craft_wire", "craft_types", "sisl::sisl", - "liburing::liburing"] + self.cpp_info.components["craft_client"].requires = [ + "craft_wire", + "craft_types", + "sisl::sisl", + "liburing::liburing", + ] + + self.cpp_info.components["craft_replica_mgr"].libs = ["craft_replica_mgr"] + self.cpp_info.components["craft_replica_mgr"].requires = [ + "craft_types", + "craft_wire", + "nuraft_mesg::nuraft_mesg", + ] + + self.cpp_info.components["craft_raft_service"].libs = [ + "raft_service" + ] # actual CMake target name + self.cpp_info.components["craft_raft_service"].requires = [ + "craft_types", + "sisl::sisl", + "nuraft_mesg::nuraft_mesg", + "craft_replica_mgr", + ] self.cpp_info.components["craft_reference"].libs = ["craft_reference"] - self.cpp_info.components["craft_reference"].requires = ["craft_client"] + self.cpp_info.components["craft_reference"].requires = [ + "craft_client", + "craft_raft_service", + "craft_replica_mgr", + ] diff --git a/include/craft/types.hpp b/include/craft/types.hpp index 356d567..fcda555 100644 --- a/include/craft/types.hpp +++ b/include/craft/types.hpp @@ -64,6 +64,7 @@ struct client_hdr { uint64_t term{0}; int64_t commit_lsn{-1}; int64_t all_committed_lsn{-1}; + uint64_t client_token{0}; }; // Returned by login(): the replica set, the starting dLSN for new I/O, the session term, and the volume @@ -129,7 +130,8 @@ ENUM(craft_error, uint16_t, WRONG_TOKEN, // client_token is not the current owner NOT_ELIGIBLE, // replica cannot serve this read (Missing overlap / below login-dLSN L) REPLICA_DOWN, // addressed replica is down (fault injection / unreachable) - INTERNAL); // unexpected internal failure + INTERNAL, // unexpected internal failure + NOT_IMPLEMENTED) class craft_error_category : public std::error_category { public: diff --git a/src/client.cpp b/src/client.cpp index d49ffed..7aa6d0a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -52,7 +52,7 @@ client_hdr craft_client::make_hdr() const { // Every IO piggybacks the commit frontier (CRAFT has no standalone commit verb) and the set-wide reclaim // floor -- min commit_lsn across members, which the broadcast keep_alive maintains (the login baseline // until the first sweep). A replica reclaims journal below min(all_committed_lsn, its own apply frontier). - return client_hdr{term_, tracker_->frontier(), route_->all_committed()}; + return client_hdr{term_, tracker_->frontier(), route_->all_committed(), client_token_}; } // Fail fast rather than burn a dLSN on an IO the replicas will reject anyway. They enforce alignment too. @@ -81,6 +81,7 @@ async_status craft_client::login(uint64_t client_token) { leader_ = target; term_ = lr->term; + client_token_ = client_token; lba_size_ = lr->lba_size; capacity_ = lr->capacity; // max_tx is the volume's max DATA transfer (like iSCSI's 512 KiB payload, header excluded). A driver caps diff --git a/src/client_impl.hpp b/src/client_impl.hpp index 578ba15..861353a 100644 --- a/src/client_impl.hpp +++ b/src/client_impl.hpp @@ -56,6 +56,7 @@ class craft_client { uint64_t capacity() const { return capacity_; } uint32_t max_tx() const { return max_tx_; } uint64_t term() const { return term_; } + uint64_t client_token() const { return client_token_; } int64_t commit_lsn() const { return tracker_->frontier(); } int64_t read_horizon() const { return tracker_->read_horizon(); } uint64_t winner_scans() const { return tracker_->winner_scans(); } @@ -89,6 +90,7 @@ class craft_client { std::vector< std::shared_ptr< craft_replica > > replicas_; uint32_t leader_{0}; uint64_t term_{0}; + uint64_t client_token_{0}; uint32_t lba_size_{0}; uint64_t capacity_{0}; uint32_t max_tx_{0}; diff --git a/src/craft_peer.hpp b/src/craft_peer.hpp index 29db72f..dd82590 100644 --- a/src/craft_peer.hpp +++ b/src/craft_peer.hpp @@ -67,6 +67,8 @@ struct JournalSlot { lba_t lba{0}; lba_count_t len{0}; sisl::sg_list data{}; + std::shared_ptr< std::vector< uint8_t > > + owned_data{}; // optional: null when data is borrowed (do_fetch), set when owned (fetch_data) }; // The peer-facing surface of one replica: what a PEER (not a client) may ask of it. Driven by the leader during diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index febc7de..4dc461d 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -26,9 +26,11 @@ #include #include #include +#include #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares +#include namespace craft { @@ -54,19 +56,64 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } } // namespace -void MemCraftReplica::init_faults() { +void MemCraftReplica::init() { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); fault_retired_.push_back(std::move(initial)); + + // register raft callbacks + auto raft_inst = raft_service::instance(); + if (!raft_inst->is_raft_enabled()) { return; } + auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& vol_uuid_str) { + auto const vol_uuid = boost::uuids::string_generator()(vol_uuid_str); + auto const op_val = j.at("op").get< int >(); + switch (static_cast< Operation >(op_val)) { + case Operation::SyncRSCommitLSN: { + SyncRSCommitLSNMsg m; + try { + m = j.get< SyncRSCommitLSNMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); + return; + } + LOGDEBUG("commit[{}][vol={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, + boost::uuids::to_string(vol_uuid), m.rs_commit_lsn, m.empty_slots.size()); + apply_sync(vol_uuid, m.rs_commit_lsn, m.client_token, m.empty_slots); + break; + } + case Operation::InternalLogin: { + InternalLoginMsg m; + try { + m = j.get< InternalLoginMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); + return; + } + LOGDEBUG("commit[{}][vol={}]: applying InternalLogin term={} client_token={}", log_idx, + boost::uuids::to_string(vol_uuid), m.term, m.client_token); + internal_login(m.client_token, m.term); + break; + } + default: + LOGERROR("commit[{}]: unknown op={}", log_idx, op_val); + break; + } + }; + raft_inst->add_commit_cb(std::move(commit_cb)); } MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : geo_{.lba_size = page_size, .ep = std::move(ep)}, net_{std::move(net)} { - init_faults(); + init(); + LOGDEBUG("MemCraftReplica constructed [id={}] page_size={}", boost::uuids::to_string(geo_.ep.id), page_size); } -MemCraftReplica::MemCraftReplica(server_geometry geo) : geo_{std::move(geo)} { init_faults(); } +MemCraftReplica::MemCraftReplica(server_geometry geo) : geo_{std::move(geo)} { + init(); + LOGDEBUG("MemCraftReplica constructed [id={}] lba_size={} capacity={}", boost::uuids::to_string(geo_.ep.id), + geo_.lba_size, geo_.capacity); +} // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── @@ -81,15 +128,19 @@ void MemCraftReplica::mutate_faults(Fn&& fn) { } void MemCraftReplica::set_up(bool up) { + LOGINFO("fault injection: set_up({}) [id={}]", up, boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.up = up; }); } void MemCraftReplica::set_delay(std::chrono::milliseconds d) { + LOGINFO("fault injection: set_delay({}ms) [id={}]", d.count(), boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.delay = (d.count() > 0) ? d : std::chrono::milliseconds{0}; }); } void MemCraftReplica::drop_writes(bool drop) { + LOGINFO("fault injection: drop_writes({}) [id={}]", drop, boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.write_ok = !drop; }); } void MemCraftReplica::clear_faults() { + LOGINFO("fault injection: clear_faults [id={}]", boost::uuids::to_string(geo_.ep.id)); mutate_faults([](replica_faults& s) { s = replica_faults{}; }); } bool MemCraftReplica::is_up() const { return fault_snapshot()->up; } @@ -109,6 +160,7 @@ async_status MemCraftReplica::logout(client_hdr hdr) { std::lock_guard< std::mutex > g{mu_}; if (hdr.term != state_.term) co_return fail(craft_error::STALE_TERM); } + LOGINFO("logout [id={}] term={}", boost::uuids::to_string(geo_.ep.id), hdr.term); co_return net_ ? net_->run_logout(this, hdr.term) : ok(); } // Two data paths behind one interface, chosen by the verb's leading `q`. Null q: every op crosses the wire @@ -222,6 +274,7 @@ async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring std::lock_guard< std::mutex > g{mu_}; if (hdr.term != state_.term) co_return fail(craft_error::STALE_TERM); } + LOGDEBUG("request_resolution [id={}] term={} upto={}", boost::uuids::to_string(geo_.ep.id), hdr.term, upto); co_return net_->run_resolution(this, hdr.term, upto); } @@ -306,6 +359,8 @@ result< lsn_pair > MemCraftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_ status MemCraftReplica::do_truncate(int64_t lsn) { std::lock_guard< std::mutex > g{mu_}; + LOGDEBUG("do_truncate [id={}] above lsn={} (last_append_lsn was {})", boost::uuids::to_string(geo_.ep.id), lsn, + state_.last_append_lsn); journal_.erase(journal_.upper_bound(lsn), journal_.end()); state_.last_append_lsn = std::min(state_.last_append_lsn, lsn); return ok(); @@ -331,6 +386,7 @@ result< std::vector< JournalSlot > > MemCraftReplica::do_fetch(std::vector< int6 } out.push_back(std::move(js)); } + LOGDEBUG("do_fetch [id={}] requested={} returned={}", boost::uuids::to_string(geo_.ep.id), lsns.size(), out.size()); return out; } @@ -355,6 +411,8 @@ result< resolution_result > MemCraftReplica::do_resolve_local(client_hdr hdr, in } state_.last_append_lsn = std::max(state_.last_append_lsn, upto); apply_up_to(upto); + LOGDEBUG("do_resolve_local [id={}] upto={} empty_slots={} commit_lsn now {}", boost::uuids::to_string(geo_.ep.id), + upto, out.empty_slots.size(), state_.commit_lsn); return out; } @@ -526,16 +584,22 @@ void MemCraftReplica::cold_apply_sync(int64_t rs_commit_lsn, uint64_t /*client_t } void MemCraftReplica::cold_apply_login(uint64_t client_token, uint64_t term) { std::lock_guard< std::mutex > g{mu_}; + LOGDEBUG("cold_apply_login [id={}] client_token={} term={} (was term={})", boost::uuids::to_string(geo_.ep.id), + client_token, term, state_.term); state_.client_token = client_token; state_.term = term; } void MemCraftReplica::cold_apply_logout() { std::lock_guard< std::mutex > g{mu_}; + LOGINFO("cold_apply_logout [id={}] clearing term={} client_token={}", boost::uuids::to_string(geo_.ep.id), + state_.term, state_.client_token); state_.client_token = 0; state_.term = 0; // no active session; subsequent IOs with old term fail STALE_TERM } void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { std::lock_guard< std::mutex > g{mu_}; + LOGDEBUG("cold_truncate_above [id={}] rs_commit_lsn={} (last_append_lsn was {})", + boost::uuids::to_string(geo_.ep.id), rs_commit_lsn, state_.last_append_lsn); journal_.erase(journal_.upper_bound(rs_commit_lsn), journal_.end()); state_.last_append_lsn = std::min(state_.last_append_lsn, rs_commit_lsn); } @@ -543,13 +607,19 @@ void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { // ── peer comm hooks (driven by raft) ── MemCraftReplica::MemJournalSlot MemCraftReplica::to_mem_journal_slot(JournalSlot const& j, uint64_t term) { + std::shared_ptr< std::vector< uint8_t > > bytes; + if (j.owned_data) { + bytes = j.owned_data; + } else if (j.data.size > 0) { + bytes = take_payload(j.data); + } return MemJournalSlot{ .term = term, .lba = j.lba, .len = j.len, .all_zeros = j.all_zeros, .is_empty = j.is_empty, - .bytes = (j.data.size == 0) ? nullptr : take_payload(j.data), + .bytes = std::move(bytes), }; } @@ -565,6 +635,8 @@ std::vector< int64_t > MemCraftReplica::get_missing_slots(int64_t watermark) { } for (; expect <= watermark; ++expect) missing.push_back(expect); // trailing gap after the last present entry + LOGDEBUG("get_missing_slots [id={}] watermark={} missing_count={}", boost::uuids::to_string(geo_.ep.id), watermark, + missing.size()); return missing; } @@ -573,6 +645,9 @@ std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply( uint64_t client_token, uint64_t term) { auto const peers = replica_manager::instance()->get_volume(vol_uuid); auto const missing_lsns = get_missing_slots(watermark); + LOGDEBUG("resolve_and_apply[vol={}] watermark={} missing={} peers={}", boost::uuids::to_string(vol_uuid), watermark, + missing_lsns.size(), peers.size()); + if (missing_lsns.empty()) { return {{}, -1}; } // Brute force, no optimizations for now // Step 1: ask every peer for the full missing list, collect ALL responses first. @@ -581,6 +656,9 @@ std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply( if (peer.id == geo_.ep.id) { continue; } if (auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing_lsns)); r) { all_responses.emplace_back(std::move(r.value())); + } else { + LOGWARN("resolve_and_apply[vol={}]: fetch_data to peer {} failed/unreachable, error: {}", + boost::uuids::to_string(vol_uuid), boost::uuids::to_string(peer.id), r.error().message()); } } @@ -611,6 +689,13 @@ std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply( stalled_lsn = (stalled_lsn == -1) ? lsn : std::min(lsn, stalled_lsn); } } + if (stalled_lsn != -1) { + LOGWARN("resolve_and_apply[vol={}]: could not resolve past lsn={} (quorum-lacks evidence insufficient)", + boost::uuids::to_string(vol_uuid), stalled_lsn); + } else { + LOGDEBUG("resolve_and_apply[vol={}]: fully resolved up to watermark={}, empty_slots={}", + boost::uuids::to_string(vol_uuid), watermark, empty_slots.size()); + } return {empty_slots, stalled_lsn}; } @@ -619,6 +704,8 @@ result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol auto const [empty_slots, stalled_lsn] = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); if (stalled_lsn != -1) { // leader could not resolve all the missing lsns + LOGERROR("sync_rs_commit_lsn[vol={}]: leader could not resolve all missing lsns, stalled at {}", + boost::uuids::to_string(vol_uuid), stalled_lsn); return std::unexpected(make_error_condition(craft_error::INTERNAL)); } if (auto const r = raft_service::instance()->propose(vol_uuid, @@ -627,8 +714,12 @@ result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol .empty_slots = std::move(empty_slots)}); !r) { // TODO: any cleanup required? + LOGERROR("sync_rs_commit_lsn[vol={}]: propose(SyncRSCommitLSN={}) failed: {}", + boost::uuids::to_string(vol_uuid), rs_commit_lsn, r.error().message()); return std::unexpected(r.error()); } + LOGINFO("sync_rs_commit_lsn[vol={}]: proposed rs_commit_lsn={} OK", boost::uuids::to_string(vol_uuid), + rs_commit_lsn); return {}; } @@ -640,6 +731,8 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con std::lock_guard< std::mutex > g{mu_}; state_.term = term; state_.client_token = client_token; + LOGINFO("apply_login [id={}]: raft disabled, cold-path login OK, term={} token={}", + boost::uuids::to_string(geo_.ep.id), term, client_token); return LoginResult{.members = {geo_.ep}, .dLSN = state_.last_append_lsn, .term = state_.term, @@ -668,21 +761,35 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con } auto const members = replica_manager::instance()->get_volume(vol_uuid); + LOGDEBUG("apply_login[vol={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(vol_uuid), + members.size()); for (auto const m : members) { if (m.id == geo_.ep.id) { continue; } if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term, true /* is_login */)); r) { + LOGDEBUG("apply_login[vol={}]: peer {} reported commit_lsn={} last_append_lsn={}", + boost::uuids::to_string(vol_uuid), boost::uuids::to_string(m.id), r->commit_lsn, + r->last_append_lsn); peer_resp.emplace_back(r.value()); + } else { + LOGWARN("apply_login[vol={}]: peer {} did not respond to GetRSCommitLSN", boost::uuids::to_string(vol_uuid), + boost::uuids::to_string(m.id)); } } // compute watermark as max(quorum.last_append) if (peer_resp.size() <= members.size() / 2) { + LOGERROR("apply_login[vol={}]: quorum not reached ({} of {} responded)", boost::uuids::to_string(vol_uuid), + peer_resp.size(), members.size()); return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); } auto const rs_commit_lsn = std::ranges::max_element(peer_resp, {}, &lsn_pair::last_append_lsn)->last_append_lsn; + LOGINFO("apply_login[vol={}]: computed rs_commit_lsn={} from {} responder(s)", boost::uuids::to_string(vol_uuid), + rs_commit_lsn, peer_resp.size()); // Phase 1b: Leader behind - resolve all the missing lsns and // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, current_term); !r) { + LOGERROR("apply_login[vol={}]: sync_rs_commit_lsn failed: {}", boost::uuids::to_string(vol_uuid), + r.error().message()); return std::unexpected(r.error()); } @@ -695,21 +802,31 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term}); !r) { // TODO: any cleanup required? + LOGERROR("apply_login[vol={}]: propose(InternalLogin term={}) failed: {}", boost::uuids::to_string(vol_uuid), + term, r.error().message()); return std::unexpected(r.error()); } + LOGDEBUG("apply_login[vol={}]: InternalLogin(term={}) proposed, waiting for commit", + boost::uuids::to_string(vol_uuid), term); // Phase 4: truncate above rs_commit_lsn // This happens in the internal login commit. Wait until that happens. { std::unique_lock< std::mutex > lk(login_mu_); login_cv_.wait_for(lk, std::chrono::seconds(2), [&] { return login_done_; }); - if (!login_done_) { return std::unexpected(make_error_condition(craft_error::INTERNAL)); } + if (!login_done_) { + LOGERROR("apply_login[vol={}]: timed out waiting for InternalLogin(term={}) commit callback", + boost::uuids::to_string(vol_uuid), term); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } } std::vector< replica_endpoint > replicas; for (auto const& m : members) { replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); } + LOGINFO("apply_login[vol={}]: LOGIN SUCCESS term={} dLSN={} members={}", boost::uuids::to_string(vol_uuid), term, + rs_commit_lsn, replicas.size()); return LoginResult{.members = replicas, .dLSN = rs_commit_lsn, .term = term, @@ -759,38 +876,15 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members) { auto const vol_uuid = craft::to_uuid(volume_id); - auto const commit_cb = [this, vol_uuid](uint64_t log_idx, nlohmann::json const& j) { - auto const op_val = j.at("op").get< int >(); - switch (static_cast< Operation >(op_val)) { - case Operation::SyncRSCommitLSN: { - SyncRSCommitLSNMsg m; - try { - m = j.get< SyncRSCommitLSNMsg >(); - } catch (nlohmann::json::exception const& e) { - LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); - return; - } - apply_sync(vol_uuid, m.rs_commit_lsn, m.client_token, m.empty_slots); - break; - } - case Operation::InternalLogin: { - InternalLoginMsg m; - try { - m = j.get< InternalLoginMsg >(); - } catch (nlohmann::json::exception const& e) { - LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); - return; - } - internal_login(m.client_token, m.term); - break; - } - default: - LOGERROR("commit[{}]: unknown op={}", log_idx, op_val); - break; - } - }; - auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members, commit_cb); - if (r) { replica_manager::instance()->register_volume(vol_uuid, members); } + LOGINFO("srv_create_volume[vol={}]: creating with {} member(s)", boost::uuids::to_string(vol_uuid), members.size()); + + auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members); + if (r) { + replica_manager::instance()->register_volume(vol_uuid, members); + LOGINFO("srv_create_volume[vol={}]: SUCCESS", boost::uuids::to_string(vol_uuid)); + } else { + LOGERROR("srv_create_volume[vol={}]: FAILED: {}", boost::uuids::to_string(vol_uuid), r.error().message()); + } return r; } @@ -838,6 +932,8 @@ void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_ std::lock_guard< std::mutex > g{mu_}; apply_up_to(rs_commit_lsn); rs_commit_lsn_.store(rs_commit_lsn, std::memory_order_relaxed); + LOGDEBUG("apply_sync[vol={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(vol_uuid), + state_.commit_lsn, rs_commit_lsn); } session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { @@ -853,6 +949,8 @@ void MemCraftReplica::internal_login(uint64_t client_token, uint64_t term) { login_done_ = true; } login_cv_.notify_one(); + LOGINFO("internal_login [id={}]: InternalLogin COMMITTED term={} client_token={}", + boost::uuids::to_string(geo_.ep.id), term, client_token); } -} // namespace craft +} // namespace craft \ No newline at end of file diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 5353679..a611313 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -295,7 +295,7 @@ class MemCraftReplica final : public craft_replica, // void apply_truncate_above(int64_t rs_commit_lsn); // Misc helpers - void init_faults(); + void init(); MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); std::vector< int64_t > get_missing_slots(int64_t watermark); std::pair< std::vector< int64_t >, int64_t > diff --git a/src/net/async_conn.cpp b/src/net/async_conn.cpp index aa73909..bfbb8d0 100644 --- a/src/net/async_conn.cpp +++ b/src/net/async_conn.cpp @@ -280,13 +280,13 @@ craft_async_conn::ensure_ready(std::array< uint8_t, 16 > const& vol, uint64_t to co_return finish(std::unexpected(net_error::malformed)); } if (static_cast< wire::status >(parsed->hdr.status) != wire::status::ok) { - LOGDEBUG("craft_async_conn[{}:{}] HELO rejected (status={}) at term={}", host_, port_, - static_cast< int >(parsed->hdr.status), term); + LOGDEBUG("craft_async_conn[{}:{}] HELO rejected (status={}) at term={}, client_token={}", host_, port_, + static_cast< int >(parsed->hdr.status), term, token); drop_fd(); // HELO rejected: treat as a connection-level fault so the client re-establishes the session co_return finish(std::unexpected(net_error::closed)); } ready_ = true; - LOGTRACE("craft_async_conn[{}:{}] ready (HELO'd at term={})", host_, port_, term); + LOGTRACE("craft_async_conn[{}:{}] ready (HELO'd at term={}, client_token=)", host_, port_, term, token); co_return finish(std::expected< void, net_error >{}); } diff --git a/src/net/tcp_peer.cpp b/src/net/tcp_peer.cpp index ae12331..81c338e 100644 --- a/src/net/tcp_peer.cpp +++ b/src/net/tcp_peer.cpp @@ -27,8 +27,9 @@ std::error_condition net_to_error(net_error e) { } } // namespace -CraftTcpPeer::CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, std::chrono::milliseconds op_timeout) : - host_{std::move(host)}, port_{port}, id_{id}, op_timeout_{op_timeout} {} +CraftTcpPeer::CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, uint32_t page_size, + std::chrono::milliseconds op_timeout) : + host_{std::move(host)}, port_{port}, id_{id}, page_size_{page_size}, op_timeout_{op_timeout} {} CraftTcpPeer::~CraftTcpPeer() = default; @@ -74,11 +75,72 @@ async_result< lsn_pair > CraftTcpPeer::get_rs_commit_lsn(uint64_t term, bool is_ co_return lsn_pair{rsp.commit_lsn, rsp.last_append_lsn}; } -// ── existing craft_peer methods: not yet implemented over the wire (no opcodes allocated for these yet) ── -async_result< lsn_pair > CraftTcpPeer::get_lsns() { co_return fail(craft_error::INTERNAL); } async_result< std::vector< JournalSlot > > CraftTcpPeer::fetch_data(std::vector< int64_t > lsns) { - co_return fail(craft_error::INTERNAL); + if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); + + wire::fetch_data_req req{}; + req.lsn_count = static_cast< uint32_t >(lsns.size()); + std::vector< uint8_t > body; + for (auto lsn : lsns) + wire::put(body, lsn); + + std::vector< uint8_t > out; + wire::frame_message(out, wire::op::fetch_data, 0, next_request_id(), + {reinterpret_cast< uint8_t const* >(&req), sizeof(req)}, body); + if (!conn_.send_all(out)) { + connected_ = false; + co_return fail(craft_error::REPLICA_DOWN); + } + + auto msg = conn_.recv_message(wire::k_default_max_tx); + if (!msg) { + connected_ = false; + co_return std::unexpected(net_to_error(msg.error())); + } + auto parsed = wire::parse_message(*msg, wire::k_default_max_tx); + if (!parsed) { + connected_ = false; + co_return fail(craft_error::INTERNAL); + } + if (static_cast< wire::status >(parsed->hdr.status) != wire::status::ok) { co_return fail(craft_error::INTERNAL); } + + auto const rsp = wire::decode< wire::fetch_data_rsp >(parsed->op_header); + std::size_t const desc_bytes = static_cast< std::size_t >(rsp.slot_count) * sizeof(wire::fetch_slot_desc); + if (parsed->body.size() < desc_bytes) co_return fail(craft_error::INTERNAL); + + std::vector< JournalSlot > out_slots; + out_slots.reserve(rsp.slot_count); + std::size_t data_off = desc_bytes; + + for (std::size_t i = 0; i < rsp.slot_count; ++i) { + auto const sd = wire::decode< wire::fetch_slot_desc >(parsed->body.subspan(i * sizeof(wire::fetch_slot_desc))); + + JournalSlot js; + js.lsn = sd.lsn; + js.lba = sd.lba; + js.len = sd.len; + js.is_empty = sd.is_empty != 0; + js.all_zeros = sd.all_zeros != 0; + + if (!js.is_empty && !js.all_zeros) { + std::size_t const nbytes = static_cast< std::size_t >(sd.len) * page_size_; + if (data_off + nbytes > parsed->body.size()) co_return fail(craft_error::INTERNAL); + + js.owned_data = std::make_shared< std::vector< uint8_t > >(parsed->body.begin() + data_off, + parsed->body.begin() + data_off + nbytes); + js.data.size = nbytes; + js.data.iovs.push_back(iovec{js.owned_data->data(), nbytes}); + + data_off += nbytes; + } + out_slots.push_back(std::move(js)); + } + + co_return out_slots; } -async_status CraftTcpPeer::truncate(int64_t lsn) { co_return fail(craft_error::INTERNAL); } + +// ── existing craft_peer methods: not yet implemented over the wire (no opcodes allocated for these yet) ── +async_result< lsn_pair > CraftTcpPeer::get_lsns() { co_return fail(craft_error::NOT_IMPLEMENTED); } +async_status CraftTcpPeer::truncate(int64_t lsn) { co_return fail(craft_error::NOT_IMPLEMENTED); } } // namespace craft::net \ No newline at end of file diff --git a/src/net/tcp_peer.hpp b/src/net/tcp_peer.hpp index c4c67e3..e150d55 100644 --- a/src/net/tcp_peer.hpp +++ b/src/net/tcp_peer.hpp @@ -14,8 +14,8 @@ namespace craft::net { class CraftTcpPeer final : public craft_peer { public: - CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, - std::chrono::milliseconds op_timeout = std::chrono::milliseconds{0}); + CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, uint32_t page_size, + std::chrono::milliseconds op_timeout = std::chrono::milliseconds{0}); ~CraftTcpPeer() override; CraftTcpPeer(CraftTcpPeer const&) = delete; @@ -39,6 +39,7 @@ class CraftTcpPeer final : public craft_peer { std::string host_; uint16_t port_; peer_id_t id_; + uint32_t page_size_; std::chrono::milliseconds op_timeout_{0}; craft_conn conn_; diff --git a/src/net/tcp_replica.cpp b/src/net/tcp_replica.cpp index a4035d4..f50c2a8 100644 --- a/src/net/tcp_replica.cpp +++ b/src/net/tcp_replica.cpp @@ -19,8 +19,9 @@ #include #include #include - +#include #include +#include #include // the round-trip summary at shutdown (rt_stat) @@ -128,7 +129,8 @@ void CraftTcpReplica::shutdown() { if (n == 0) return; double const avg_us = static_cast< double >(s.total_ns.load(std::memory_order_relaxed)) / n / 1000.0; double const max_us = static_cast< double >(s.max_ns.load(std::memory_order_relaxed)) / 1000.0; - LOGINFO("craft_rt {}:{} {:<9} n={:<8} avg={:8.2f}us max={:9.2f}us", host_, port_, what, n, avg_us, max_us); + LOGINFO("[peer_id: {}] craft_rt {}:{} {:<9} n={:<8} avg={:8.2f}us max={:9.2f}us", boost::uuids::to_string(id_), + host_, port_, what, n, avg_us, max_us); }; dump("read", rt_read_); dump("write", rt_write_); @@ -144,10 +146,11 @@ void CraftTcpReplica::shutdown() { recv_bytes += c->n_recv_bytes(); } if (replies > 0 && recvs > 0) { - LOGINFO("craft_rt {}:{} pump recvs={} replies={} recvs/reply={:.2f} avg_recv={:.0f}B queues={}", host_, - port_, recvs, replies, static_cast< double >(recvs) / static_cast< double >(replies), - static_cast< double >(recv_bytes) / static_cast< double >(recvs), - n_slots_.load(std::memory_order_relaxed)); + LOGINFO( + "[peer_id: {}] craft_rt {}:{} pump recvs={} replies={} recvs/reply={:.2f} avg_recv={:.0f}B queues={}", + boost::uuids::to_string(id_), host_, port_, recvs, replies, + static_cast< double >(recvs) / static_cast< double >(replies), + static_cast< double >(recv_bytes) / static_cast< double >(recvs), n_slots_.load(std::memory_order_relaxed)); } #endif { @@ -210,7 +213,7 @@ std::error_condition CraftTcpReplica::on_net_fault(net::net_error e) { return net_to_error(e); } -std::optional< std::error_condition > CraftTcpReplica::ensure_bound(uint64_t term) { +std::optional< std::error_condition > CraftTcpReplica::ensure_bound(uint64_t term, uint64_t client_token) { if (!ensure_connected()) return make_error_condition(craft_error::REPLICA_DOWN); // Re-HELO whenever the requested term differs from what this connection is bound at -- not just on the first // IO. A logout+relogin bumps the session term (leader run_login: ++term_), and the wire carries no per-op @@ -218,7 +221,9 @@ std::optional< std::error_condition > CraftTcpReplica::ensure_bound(uint64_t ter // stamp that stale term on every IO and fence it. Binding is idempotent server-side, so re-HELO rebinds // this same connection to the new term. (A post-timeout reset drops bound_, so that path re-HELOs too.) if (bound_ && term == bound_term_) return std::nullopt; - auto h = conn_.helo(vol_id_, /*client_token=*/0, term); // token unused by the server's HELO (auth is P6) + LOGDEBUG("[peer_id: {}] sending helo, vol id {}, client token {}, term {}", boost::uuids::to_string(id_), + fmt::format("{:02x}", fmt::join(vol_id_, "")), client_token, term); + auto h = conn_.helo(vol_id_, client_token, term); if (!h) return on_net_fault(h.error()); if (*h != wire::status::ok) return status_to_error(*h); bound_ = true; @@ -241,6 +246,8 @@ async_result< LoginResult > CraftTcpReplica::login(uint64_t client_token) { bound_ = true; bound_term_ = r->term; } + LOGINFO("[peer_id: {}] received login response from the server, client_token {}, max_tx {}, lba_size {}, term {}", + boost::uuids::to_string(id_), client_token, max_tx_, lba_, bound_term_); LoginResult out; out.dLSN = r->dlsn; out.term = r->term; @@ -286,7 +293,7 @@ async_result< lsn_pair > CraftTcpReplica::write(::io_uring* q, client_hdr hdr, i } // ── on-ring data path: this queue's conn, lazily HELO'd at hdr.term, sends over the caller's ring ── if (auto* conn = conn_for(q)) { - if (auto e = co_await conn->ensure_ready(vol_id_, /*token=*/0, hdr.term); !e) + if (auto e = co_await conn->ensure_ready(vol_id_, hdr.client_token, hdr.term); !e) co_return std::unexpected(net_to_error(e.error())); auto const t0 = std::chrono::steady_clock::now(); auto r = co_await conn->write(dlsn, addr, len, payload, hdr.commit_lsn, hdr.all_committed_lsn); @@ -298,7 +305,7 @@ async_result< lsn_pair > CraftTcpReplica::write(::io_uring* q, client_hdr hdr, i auto ev = hop(); co_await *ev; - if (auto e = ensure_bound(hdr.term)) co_return std::unexpected(*e); + if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.write(dlsn, addr, len, payload, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); if (r->status != wire::status::ok) co_return std::unexpected(status_to_error(r->status)); @@ -321,7 +328,7 @@ async_result< read_result > CraftTcpReplica::read(::io_uring* q, client_hdr hdr, net::read_reply reply; if (auto* conn = conn_for(q)) { // ── on-ring data path ── - if (auto e = co_await conn->ensure_ready(vol_id_, /*token=*/0, hdr.term); !e) + if (auto e = co_await conn->ensure_ready(vol_id_, hdr.client_token, hdr.term); !e) co_return std::unexpected(net_to_error(e.error())); auto const t0 = std::chrono::steady_clock::now(); auto r = co_await conn->read(read_lsn, addr, len, d, hdr.commit_lsn, hdr.all_committed_lsn); @@ -331,7 +338,7 @@ async_result< read_result > CraftTcpReplica::read(::io_uring* q, client_hdr hdr, } else { auto ev = hop(); co_await *ev; - if (auto e = ensure_bound(hdr.term)) co_return std::unexpected(*e); + if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.read(read_lsn, addr, len, d, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); reply = std::move(*r); @@ -352,7 +359,10 @@ async_result< read_result > CraftTcpReplica::read(::io_uring* q, client_hdr hdr, async_result< lsn_pair > CraftTcpReplica::keep_alive(::io_uring* q, client_hdr hdr) { if (auto* conn = conn_for(q)) { // ── on-ring data path ── - if (auto e = co_await conn->ensure_ready(vol_id_, /*token=*/0, hdr.term); !e) + LOGDEBUG("[PEER_ID: {}] sending ensure ready, vol id {}, client token {}, term {}", + boost::uuids::to_string(id_), fmt::format("{:02x}", fmt::join(vol_id_, "")), hdr.client_token, + hdr.term); + if (auto e = co_await conn->ensure_ready(vol_id_, hdr.client_token, hdr.term); !e) co_return std::unexpected(net_to_error(e.error())); auto const t0 = std::chrono::steady_clock::now(); auto r = co_await conn->keep_alive(hdr.commit_lsn, hdr.all_committed_lsn); @@ -364,7 +374,7 @@ async_result< lsn_pair > CraftTcpReplica::keep_alive(::io_uring* q, client_hdr h auto ev = hop(); co_await *ev; - if (auto e = ensure_bound(hdr.term)) co_return std::unexpected(*e); + if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.keep_alive(hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); if (r->status != wire::status::ok) co_return std::unexpected(status_to_error(r->status)); @@ -376,7 +386,7 @@ async_result< resolution_result > CraftTcpReplica::request_resolution(::io_uring // must not hop to a blocking round-trip. A round is slow leader work, but the pump demuxes replies by // request_id, so the parked leg costs the data ops in flight nothing. if (auto* conn = conn_for(q)) { - if (auto e = co_await conn->ensure_ready(vol_id_, /*token=*/0, hdr.term); !e) + if (auto e = co_await conn->ensure_ready(vol_id_, hdr.client_token, hdr.term); !e) co_return std::unexpected(net_to_error(e.error())); auto r = co_await conn->resolve(upto, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(net_to_error(r.error())); @@ -387,7 +397,7 @@ async_result< resolution_result > CraftTcpReplica::request_resolution(::io_uring // No-ring tier: the blocking session-mgr path, like login/logout. auto ev = hop(); co_await *ev; - if (auto e = ensure_bound(hdr.term)) co_return std::unexpected(*e); + if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.resolve(upto, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); if (r->status != wire::status::ok) co_return std::unexpected(status_to_error(r->status)); diff --git a/src/net/tcp_replica.hpp b/src/net/tcp_replica.hpp index f01abb4..7f11eb1 100644 --- a/src/net/tcp_replica.hpp +++ b/src/net/tcp_replica.hpp @@ -129,7 +129,7 @@ class CraftTcpReplica final : public craft_replica { // Session-mgr-thread-only state (this proxy's blocking ops all run serially there, so no lock guards these). bool ensure_connected(); // lazy connect on first use // Lazy HELO for a follower connection (login binds the leader's). nullopt = bound; else why it could not be. - std::optional< std::error_condition > ensure_bound(uint64_t term); + std::optional< std::error_condition > ensure_bound(uint64_t term, uint64_t client_token); // Map a transport fault to the domain error AND, for any connection-level fault (timed out / closed / send // / recv), poison this connection so the next op reconnects + re-HELOs. A timed-out recv is still pending // in the ring, so the socket cannot be reused as-is; the straggler ack-at-quorum path lands right here. diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index be03780..6a588ea 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -40,11 +40,17 @@ std::span< uint8_t const > as_bytes(T const& v) { } // namespace craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : max_tx_{geo.max_tx} { + LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(geo.ep.id), + server_config_file); // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { - replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id); + replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id, geo.lba_size); raft_service::instance()->start_raft_service(geo.ep.id); + LOGINFO("craft_tcp_server: replica_manager + raft_service started [id={}]", boost::uuids::to_string(geo.ep.id)); + } else { + LOGINFO("craft_tcp_server: no server_config_file given -- running in standalone/cold-path mode [id={}]", + boost::uuids::to_string(geo.ep.id)); } replica_ = std::make_shared< MemCraftReplica >(std::move(geo)); } @@ -67,11 +73,18 @@ void craft_tcp_server::log_stats() const { } void craft_tcp_server::serve(craft_conn conn) { + LOGDEBUG("craft_srv: connection accepted, serving"); for (;;) { auto msg = conn.recv_message(max_tx_); - if (!msg) return; // peer closed, or a framing error -- done with this connection + if (!msg) { + LOGDEBUG("craft_srv: connection closed (peer closed, or framing error)"); + return; // peer closed, or a framing error -- done with this connection + } auto parsed = wire::parse_message(*msg, max_tx_); - if (!parsed) return; + if (!parsed) { + LOGWARN("craft_srv: malformed message, resetting connection"); + return; + } switch (static_cast< wire::op >(parsed->hdr.op)) { case wire::op::login: on_login(conn, *parsed); @@ -104,6 +117,7 @@ void craft_tcp_server::serve(craft_conn conn) { on_fetch_data(conn, *parsed); break; default: + LOGWARN("craft_srv: unknown op={}, resetting connection", static_cast< int >(parsed->hdr.op)); return; // a client sends only request ops we serve; anything else resets the connection } } @@ -113,6 +127,8 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { // session_active_ is stoll maintained here, change it once we support multi volume std::vector< uint8_t > out; if (session_active_) { + LOGWARN("craft_srv LOGIN [rid:{}]: rejected, a session is already active (term={})", req.hdr.request_id, + session_term_); wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_eligible), req.hdr.request_id, {}, {}); conn.send_all(out); @@ -121,8 +137,12 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection session_active_ = true; auto const lr = wire::decode< wire::login_req >(req.op_header); + LOGINFO("craft_srv LOGIN [rid:{}]: client_token={} new_term={}", req.hdr.request_id, lr.client_token, + session_term_); auto result = replica_->srv_establish(lr.volume_id, lr.client_token, session_term_); if (!result) { + LOGERROR("craft_srv LOGIN [rid:{}]: srv_establish failed: {}", req.hdr.request_id, result.error().message()); + session_active_ = false; // establish never happened -- don't hold the slot open wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(to_wire_status(result.error())), req.hdr.request_id, {}, {}); conn.send_all(out); @@ -130,6 +150,9 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { } auto const srv_rsp = result.value(); if (!srv_rsp.leader_hint.is_nil()) { + LOGINFO("craft_srv LOGIN [rid:{}]: NOT_LEADER, redirecting to {}", req.hdr.request_id, + boost::uuids::to_string(srv_rsp.leader_hint)); + session_active_ = false; wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_leader), req.hdr.request_id, {}, {}); conn.send_all(out); @@ -151,6 +174,8 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { wire::put_member(body, wm); } + LOGINFO("craft_srv LOGIN [rid:{}]: SUCCESS term={} dlsn={} members={}", req.hdr.request_id, rsp.term, rsp.dlsn, + rsp.member_count); wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, as_bytes(rsp), body); conn.send_all(out); @@ -167,10 +192,13 @@ void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { bool is_raft_enabled = raft_service::instance()->is_raft_enabled(); if (is_raft_enabled && (hr.term != current.term || hr.client_token != current.client_token)) { + LOGWARN("craft_srv HELO [rid:{}]: FENCED -- presented term={} token={}, current term={} token={}", + req.hdr.request_id, hr.term, hr.client_token, current.term, current.client_token); code = wire::status::stale_term; } else { session_term_ = hr.term; session_active_ = true; + LOGDEBUG("craft_srv HELO [rid:{}]: bound connection at term={}", req.hdr.request_id, hr.term); } std::vector< uint8_t > out; @@ -180,9 +208,11 @@ void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { void craft_tcp_server::on_logout(craft_conn& conn, wire::message const& req) { auto st = wire::status::ok; - if (!session_active_) + if (!session_active_) { + LOGWARN("craft_srv LOGOUT [rid:{}]: no active session (already fenced)", req.hdr.request_id); st = wire::status::stale_term; // no active session to tear down - else { + } else { + LOGINFO("craft_srv LOGOUT [rid:{}]: term={}", req.hdr.request_id, session_term_); session_active_ = false; replica_->srv_end(); // clear the replica's term; later IO with the old term now fences STALE_TERM } @@ -289,7 +319,7 @@ void craft_tcp_server::on_resolve(craft_conn& conn, wire::message const& req) { wire::put(body, d); } } - LOGTRACE("craft_srv RS [rid:{}] upto={} status={} empties={}", req.hdr.request_id, rr.upto, + LOGDEBUG("craft_srv RS [rid:{}] upto={} status={} empties={}", req.hdr.request_id, rr.upto, static_cast< int >(code), rsp.empty_count); std::vector< uint8_t > out; wire::frame_message(out, wire::op::resolve_rsp, static_cast< uint8_t >(code), req.hdr.request_id, as_bytes(rsp), @@ -326,6 +356,7 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r LOGINFO("craft_srv CREATE_VOLUME [rid:{}] member_count={}", req.hdr.request_id, cr.member_count); wire::status code = wire::status::ok; if (!members) { + LOGERROR("craft_srv CREATE_VOLUME [rid:{}]: malformed body (member list truncated)", req.hdr.request_id); code = wire::status::invalid_argument; // body shorter than member_count implies -- malformed request } else { std::vector< replica_endpoint > replica_members; @@ -333,7 +364,11 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r replica_members.emplace_back(replica_endpoint{.id = craft::to_uuid(m.id), .addr = m.addr}); } auto const r = replica_->srv_create_volume(cr.volume_id, replica_members); - if (!r) code = to_wire_status(r.error()); + if (!r) { + LOGERROR("craft_srv CREATE_VOLUME [rid:{}]: srv_create_volume failed: {}", req.hdr.request_id, + r.error().message()); + code = to_wire_status(r.error()); + } } LOGINFO("craft_srv CREATE_VOLUME [rid:{}] members={} status={}", req.hdr.request_id, cr.member_count, @@ -351,6 +386,7 @@ void craft_tcp_server::on_get_rs_commit_lsn(craft_conn& conn, wire::message cons auto const r = replica_->srv_get_rs_commit_lsn(gr.term, gr.is_login != 0); if (!r) { + LOGWARN("craft_srv GET_RS_COMMIT_LSN [rid:{}]: failed: {}", req.hdr.request_id, r.error().message()); code = to_wire_status(r.error()); } else { rsp.commit_lsn = r->commit_lsn; @@ -375,10 +411,12 @@ void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) wire::status code = wire::status::ok; if (!lsns) { + LOGERROR("craft_srv FETCH_DATA [rid:{}]: malformed body (lsn list truncated)", req.hdr.request_id); code = wire::status::invalid_argument; // body shorter than lsn_count implies -- malformed request } else { auto const r = replica_->srv_fetch_data(*lsns); if (!r) { + LOGWARN("craft_srv FETCH_DATA [rid:{}]: failed: {}", req.hdr.request_id, r.error().message()); code = to_wire_status(r.error()); } else { rsp.slot_count = static_cast< uint32_t >(r->size()); @@ -402,7 +440,7 @@ void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) } } - LOGTRACE("craft_srv FETCH_DATA [rid:{}] requested={} status={} returned={}", req.hdr.request_id, fr.lsn_count, + LOGDEBUG("craft_srv FETCH_DATA [rid:{}] requested={} status={} returned={}", req.hdr.request_id, fr.lsn_count, static_cast< int >(code), rsp.slot_count); std::vector< uint8_t > out; @@ -411,4 +449,4 @@ void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) conn.send_all(out); } -} // namespace craft::net +} // namespace craft::net \ No newline at end of file diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 167858b..a28e29b 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -31,6 +31,8 @@ std::shared_ptr< raft_service > raft_service::instance() { consensus_handle raft_service::get_consensus() { return consensus_; } void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { + // raft global manager for commits + nuraft::nuraft_global_mgr::init(); std::call_once(raft_started_, [&] { auto const my_port = replica_manager::instance()->get(server_uuid)->raft_port; auto params = nuraft_mesg::manager::params{ @@ -40,6 +42,10 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { }; consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true /*with_data_svc*/); auto raft_params = nuraft::raft_params{}; + raft_params.with_election_timeout_upper(1800) + .with_election_timeout_lower(900) + .with_hb_interval(250) + .with_rpc_failure_backoff(250); consensus_->register_mgr_type(default_group_type_, raft_params); server_uuid_ = server_uuid; LOGINFO("Initialized raft_service for {} with raft consensus manager, port {}", params.server_uuid_, @@ -47,8 +53,13 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { }); } +raft_service::~raft_service() { + consensus_.reset(); + nuraft::nuraft_global_mgr::shutdown(); +} + result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_id, - std::vector< replica_endpoint > const& members, raft_commit_cb_t cb) { + std::vector< replica_endpoint > const& members) { auto consensus = raft_service::instance()->get_consensus(); // Seat THIS replica as leader by creating the group. @@ -70,7 +81,7 @@ result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_i return fail(craft_error::INTERNAL); } } - add_commit_cb(group_id, cb); + return {}; } @@ -90,8 +101,7 @@ std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(in return result.value(); } LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); - auto const r = get_commit_cb(group_id); - auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, (r ? r.value() : nullptr)); + auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_); add_state_mgr(group_id, mgr); return mgr; } @@ -108,16 +118,9 @@ void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::s state_mgrs_[group_id] = std::move(mgr); } -result< raft_commit_cb_t > raft_service::get_commit_cb(nuraft_mesg::group_id_t const& group_id) { - std::shared_lock< std::shared_mutex > g{mu_}; - auto const it = commit_cbs_.find(group_id); - if (it == commit_cbs_.end()) return fail(craft_error::INTERNAL); - return it->second; -} - -void raft_service::add_commit_cb(nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb) { - std::lock_guard< std::shared_mutex > g{mu_}; - commit_cbs_.emplace(group_id, std::move(cb)); +void raft_service::add_commit_cb(raft_commit_cb_t cb) { + // we expect that this is called only once + commit_cb_ = std::move(cb); } bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 945394e..1ee63b6 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -26,13 +26,14 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab public: inline static const std::string default_group_type_{"raft_service_raft"}; - virtual ~raft_service() = default; + virtual ~raft_service(); static std::shared_ptr< raft_service > instance(); bool is_raft_enabled() { return consensus_ != nullptr; } consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); - result< void > srv_create_volume(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members, - raft_commit_cb_t cb); + result< void > srv_create_volume(boost::uuids::uuid const& group_id, + std::vector< replica_endpoint > const& members); + void add_commit_cb(raft_commit_cb_t cb); bool is_leader(nuraft_mesg::group_id_t const& group_id); nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); @@ -53,12 +54,10 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab nlohmann::json server_config_; std::shared_mutex mu_; std::map< nuraft_mesg::group_id_t, std::shared_ptr< raft_state_mgr > > state_mgrs_; - std::map< nuraft_mesg::group_id_t, raft_commit_cb_t > commit_cbs_; + raft_commit_cb_t commit_cb_; result< std::shared_ptr< raft_state_mgr > > get_state_mgr(nuraft_mesg::group_id_t const& group_id); void add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr); - result< raft_commit_cb_t > get_commit_cb(nuraft_mesg::group_id_t const& group_id); - void add_commit_cb(nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb); }; } // namespace craft diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index f70dbed..3be77b7 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -54,13 +54,14 @@ inline void from_json(nlohmann::json const& j, InternalLoginMsg& m) { j.at("term").get_to(m.term); } -using raft_commit_cb_t = std::function< void(uint64_t log_idx, nlohmann::json const& j) >; +using raft_commit_cb_t = + std::function< void(uint64_t log_idx, nlohmann::json const& j, std::string const& group_id) >; class echo_state_machine : public nuraft::state_machine { public: - echo_state_machine(raft_commit_cb_t cb) : commit_cb_(std::move(cb)), last_commit_idx_(0) {} + echo_state_machine(raft_commit_cb_t cb, std::string const& group_id) : commit_cb_(std::move(cb)), group_id_{group_id}, last_commit_idx_(0) {} - virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) { + virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) override { nlohmann::json j; try { j = unwrap_buffer(data); @@ -68,33 +69,34 @@ class echo_state_machine : public nuraft::state_machine { LOGERROR("commit[{}]: msgpack decode failed: {}", log_idx, e.what()); return nullptr; } - if (commit_cb_) { commit_cb_(log_idx, j); } + LOGDEBUG("commit[{}]: decoded json={}, cb_set={}", log_idx, j.dump(), commit_cb_ ? "yes" : "no"); + if (commit_cb_) { commit_cb_(log_idx, j, group_id_); } last_commit_idx_ = log_idx; return nullptr; } - virtual nuraft::ptr< nuraft::buffer > pre_commit(const nuraft::ulong log_idx, nuraft::buffer& data) { - LOGINFO("Pre-Commit message [{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); + virtual nuraft::ptr< nuraft::buffer > pre_commit(const nuraft::ulong log_idx, nuraft::buffer& data) override { return nullptr; } - virtual void rollback(const nuraft::ulong log_idx, nuraft::buffer& data) { - LOGINFO("Rollback[{}] : {}", log_idx, reinterpret_cast< const char* >(data.data())); - } + virtual void rollback(const nuraft::ulong log_idx, nuraft::buffer& data) override {} - virtual void save_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) {} - virtual bool apply_snapshot(nuraft::snapshot& s) { return true; } + virtual void save_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) override {} + virtual bool apply_snapshot(nuraft::snapshot& s) override { return true; } - virtual int read_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) { return 0; } + virtual int read_snapshot_data(nuraft::snapshot& s, const nuraft::ulong offset, nuraft::buffer& data) override { + return 0; + } - virtual nuraft::ptr< nuraft::snapshot > last_snapshot() { return nuraft::ptr< nuraft::snapshot >(); } + virtual nuraft::ptr< nuraft::snapshot > last_snapshot() override { return nuraft::ptr< nuraft::snapshot >(); } - virtual void create_snapshot(nuraft::snapshot& s, nuraft::async_result< bool >::handler_type& when_done) {} + virtual void create_snapshot(nuraft::snapshot& s, nuraft::async_result< bool >::handler_type& when_done) override {} - virtual nuraft::ulong last_commit_index() { return last_commit_idx_; } + virtual nuraft::ulong last_commit_index() override { return last_commit_idx_; } private: nuraft::ulong last_commit_idx_; raft_commit_cb_t commit_cb_; + std::string group_id_; }; } \ No newline at end of file diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index 50c35d7..9e71c26 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -118,7 +118,7 @@ void raft_state_mgr::save_state(const nuraft::srv_state& state) { uint32_t raft_state_mgr::get_logstore_id() const { return 0; } std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { - return std::make_shared< echo_state_machine >(std::move(_commit_cb)); + return std::make_shared< echo_state_machine >(std::move(_commit_cb), _group_id); } void raft_state_mgr::permanent_destroy() {} diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index 88f3083..1ec3599 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -14,8 +14,10 @@ std::shared_ptr< replica_manager > replica_manager::instance() { return inst; } -void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid) { +void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid, + uint32_t page_size) { id_ = my_uuid; + page_size_ = page_size; std::ifstream istrm(path, std::ios::binary); if (!istrm.is_open()) { LOGERROR("replica_manager: could not open {}", path); @@ -39,10 +41,10 @@ void replica_manager::start_replica_service(std::string const& path, boost::uuid .host = m.at("host").get< std::string >(), .raft_port = m.at("raft_port").get< uint16_t >(), .tcp_port = m.at("tcp_port").get< uint16_t >(), - .peer_client = (id == my_uuid) - ? nullptr - : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), - m.at("tcp_port").get< uint16_t >(), id), + .peer_client = (id == my_uuid) ? nullptr + : std::make_shared< net::CraftTcpPeer >( + m.at("host").get< std::string >(), + m.at("tcp_port").get< uint16_t >(), id, page_size_), }); } } diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index 8d31084..3451be9 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -30,7 +30,7 @@ class replica_manager { public: static std::shared_ptr< replica_manager > instance(); - void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid); + void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid, uint32_t page_size); // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". std::string lookup_peer(boost::uuids::uuid const& id) const; @@ -42,6 +42,7 @@ class replica_manager { replica_manager() = default; boost::uuids::uuid id_; + uint32_t page_size_; mutable std::shared_mutex mu_; std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once std::map< boost::uuids::uuid, std::vector< replica_info > > volumes_; diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index c4d5611..79c9634 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -80,8 +80,10 @@ int main(int argc, char** argv) { } } sisl::logging::SetLogger(fmt::format("craft_tcp_srv_{}", SISL_OPTIONS["server_uuid"].as< std::string >())); - sisl::logging::SetModuleLogLevel("nuraft_mesg", spdlog::level::info); - sisl::logging::SetModuleLogLevel("grpc_server", spdlog::level::info); + std::string const s = SISL_OPTIONS.count("log_mods") ? SISL_OPTIONS["log_mods"].as< std::string >() : ""; + for (auto const* mod : {"nuraft_mesg", "grpc_server"}) { + if (!s.contains(mod)) { sisl::logging::SetModuleLogLevel(mod, spdlog::level::info); } + } auto const port = SISL_OPTIONS["port"].as< uint16_t >(); if (port == 0) { diff --git a/tools/craft_test_driver/cluster.py b/tools/craft_test_driver/cluster.py index 03414b2..fc0631c 100644 --- a/tools/craft_test_driver/cluster.py +++ b/tools/craft_test_driver/cluster.py @@ -4,15 +4,20 @@ import sys from pathlib import Path +log_level_dict: dict[int, str] = {0: "trace", 1: "debug", 2: "info"} class ClusterManager: """Owns a set of craft_reference_tcp_srv subprocesses. Guarantees cleanup on exit, even if the script is interrupted or a later step raises.""" - def __init__(self, binary: Path, config_path: Path, members: list[dict]): + def __init__( + self, binary: Path, config_path: Path, members: list[dict], verbose: int + ): self.binary = binary self.config_path = config_path self.members = members + lvl = log_level_dict[verbose] + self.log_args = f"base:{lvl},nuraft_mesg:info" self.procs: dict[str, subprocess.Popen] = {} def start_all(self): @@ -24,13 +29,22 @@ def _start_one(self, member: dict): port = member["tcp_port"] cmd = [ str(self.binary), - "--port", str(port), - "--server_config_file", str(self.config_path), - "--server_uuid", uuid_, + "--port", + str(port), + "--server_config_file", + str(self.config_path), + "--server_uuid", + uuid_, + "--log_mods", + str(self.log_args), ] print(f"starting: {' '.join(cmd)}") - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, start_new_session=True) + proc = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) self.procs[uuid_] = proc def any_dead(self) -> list[str]: @@ -52,4 +66,4 @@ def __enter__(self): return self def __exit__(self, exc_type, exc, tb): - self.shutdown() \ No newline at end of file + self.shutdown() diff --git a/tools/craft_test_driver/craft_disk.py b/tools/craft_test_driver/craft_disk.py index 6ff4ceb..4613f49 100644 --- a/tools/craft_test_driver/craft_disk.py +++ b/tools/craft_test_driver/craft_disk.py @@ -11,79 +11,91 @@ import registry logger = logging.getLogger(__name__) - +log_level_dict: dict[int, str] = {0: "trace", 1: "debug", 2: "info"} class CraftDisk: """Owns one ublkpp_disk --craft_tcp subprocess attaching to a Volume. Parses the resulting /dev/ublkbN path from stdout once ublkpp_tgt::run() reports the device exposed.""" _DEVICE_RE = re.compile(r"exposed as UBD device: \[(/dev/ublkb\d+)\]") + _LOG_PATH = Path("logs/latest/ublkpp_disk_log") - def __init__(self, binary: Path, vol: Volume, ready_timeout: float = 15.0): + def __init__( + self, + binary: Path, + config_path: Path, + vol: Volume, + verbose: int, + ready_timeout: float = 15.0, + ): self.binary = binary + self.config_path = config_path self.vol = vol self.ready_timeout = ready_timeout + lvl = log_level_dict[verbose] + self.log_args = f"base:{lvl},ublksrv:{lvl},ublk_tgt:{lvl},ublk_raid:{lvl},ublk_drivers:{lvl}" self.proc: subprocess.Popen | None = None self.device_path: str | None = None def start(self): - endpoints = ",".join(f"{m['host']}:{m['tcp_port']}" for m in self.vol.members) cmd = [ "stdbuf", "-oL", str(self.binary), "--craft_tcp", - endpoints, + "--server_config_file", + str(self.config_path), "--vol_id", str(self.vol.vol_id), "--log_mods", - "ublksrv:info,ublk_tgt:info,ublk_raid:info,ublk_drivers:info", + str(self.log_args), ] logger.info("starting: %s", " ".join(cmd)) + # stdout/stderr -> DEVNULL: ublkpp_disk's async (spdlog) logger writes to its own log file + # regardless; leaving stdout as an unread PIPE risks that logger blocking forever once the + # OS pipe buffer fills. self.proc = subprocess.Popen( cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, start_new_session=True, ) self._wait_ready() def _wait_ready(self): - deadline = time.monotonic() + self.ready_timeout - while True: - remaining = deadline - time.monotonic() - if remaining <= 0: - raise TimeoutError( - f"ublkpp_disk did not expose a device within {self.ready_timeout}s" - ) - - if (rc := self.proc.poll()) is not None: - if rc < 0: - # negative returncode == killed by signal -rc (SIGABRT=6, SIGSEGV=11, etc.) - raise RuntimeError( - f"ublkpp_disk CRASHED (signal {-rc}, likely core dumped) " - f"before exposing a device -- check its output above" - ) - raise RuntimeError( - f"ublkpp_disk exited early (code={rc}) " - f"before exposing a device -- check its output above" - ) - - ready, _, _ = select.select([self.proc.stdout], [], [], min(remaining, 1.0)) - if not ready: - continue - - line = self.proc.stdout.readline() - if not line: - continue - logger.info("[ublkpp_disk] %s", line.rstrip()) - m = self._DEVICE_RE.search(line) - if m: - self.device_path = m.group(1) - return - + deadline = time.monotonic() + self.ready_timeout + # The log file may not exist yet the instant the process starts -- wait for it too. + while not self._LOG_PATH.exists(): + if time.monotonic() >= deadline: + raise TimeoutError(f"ublkpp_disk log file never appeared within {self.ready_timeout}s") + if (rc := self.proc.poll()) is not None: + raise RuntimeError(f"ublkpp_disk exited early (code={rc}) before creating a log file") + time.sleep(0.05) + + with open(self._LOG_PATH) as f: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError(f"ublkpp_disk did not expose a device within {self.ready_timeout}s") + if (rc := self.proc.poll()) is not None: + if rc < 0: + raise RuntimeError(f"ublkpp_disk CRASHED (signal {-rc})") + raise RuntimeError(f"ublkpp_disk exited early (code={rc}) before exposing a device") + if registry.cluster is not None: + dead = registry.cluster.any_dead() + if dead: + raise RuntimeError(f"server(s) died while waiting for disk attach: {dead}") + + line = f.readline() + if not line: + time.sleep(0.1) + continue + m = self._DEVICE_RE.search(line) + if m: + self.device_path = m.group(1) + logger.info("ublkpp_disk exposed device: %s", self.device_path) + return + def is_alive(self) -> bool: return self.proc is not None and self.proc.poll() is None diff --git a/tools/craft_test_driver/fio_runner.py b/tools/craft_test_driver/fio_runner.py new file mode 100644 index 0000000..bcd4d26 --- /dev/null +++ b/tools/craft_test_driver/fio_runner.py @@ -0,0 +1,39 @@ +import logging +import subprocess + +logger = logging.getLogger(__name__) + +DEFAULT_SIZE = "64m" +DEFAULT_BS = "4k" +DEFAULT_IOENGINE = "io_uring" + + +class FioError(RuntimeError): + pass + + +def run_fio(device: str, job_name: str, rw: str, size: str = DEFAULT_SIZE, bs: str = DEFAULT_BS, + ioengine: str = DEFAULT_IOENGINE, direct: bool = True, runtime: int = None, + extra_args: dict[str, str] = None) -> int: + """Run one fio job against `device`, streaming its normal terminal output live. Returns fio's + exit code (0 = success).""" + args = [ + "fio", + f"--name={job_name}", + f"--filename={device}", + f"--rw={rw}", + f"--bs={bs}", + f"--size={size}", + f"--ioengine={ioengine}", + f"--direct={1 if direct else 0}", + "--status-interval=1", + ] + if runtime is not None: + args.append(f"--runtime={runtime}") + args.append("--time_based") + for k, v in (extra_args or {}).items(): + args.append(f"--{k}={v}") + + logger.info("running: %s", " ".join(args)) + proc = subprocess.run(args) + return proc.returncode \ No newline at end of file diff --git a/tools/craft_test_driver/main.py b/tools/craft_test_driver/main.py index b141c13..8fc4ea4 100644 --- a/tools/craft_test_driver/main.py +++ b/tools/craft_test_driver/main.py @@ -43,6 +43,9 @@ def parse_args(): p.add_argument("--run-test", action="append", default=[], help="name of a test to run after attaching (repeatable); see --list-tests") p.add_argument("--list-tests", action="store_true", help="list available tests and exit") + p.add_argument( + "--verbose", type=int, default=2, help="2 = info, 1 = debug, 0 = trace" + ) args = p.parse_args() args.tcp_srv_binary = args.tcp_srv_binary.resolve() if args.craft_disk_binary: @@ -74,7 +77,7 @@ def cleanup_stray_processes(): def run(args, members): vol_id = uuid.UUID(args.vol_id) if args.vol_id else None - cluster = ClusterManager(args.tcp_srv_binary, args.config, members) + cluster = ClusterManager(args.tcp_srv_binary, args.config, members, args.verbose) registry.set_cluster(cluster) disk = None @@ -93,7 +96,7 @@ def run(args, members): time.sleep(args.startup_wait) if args.craft_disk_binary: - disk = CraftDisk(args.craft_disk_binary, vol) + disk = CraftDisk(args.craft_disk_binary, args.config, vol, args.verbose) disk.start() registry.add_disk(str(vol.vol_id), disk) logger.info(f"{len(members)} server(s) running, {vol}, disk at {disk.device_path}, " @@ -152,4 +155,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tools/craft_test_driver/registry.py b/tools/craft_test_driver/registry.py new file mode 100644 index 0000000..77a6cf4 --- /dev/null +++ b/tools/craft_test_driver/registry.py @@ -0,0 +1,26 @@ +"""Process-global registry of everything the harness has created: cluster, volumes, disks. +Tests import this module directly instead of receiving these as parameters.""" + +from cluster import ClusterManager +from volumes import VolumeRegistry + +cluster: ClusterManager | None = None +volumes: VolumeRegistry = VolumeRegistry() +disks: dict = {} # vol_id (str) -> CraftDisk + + +def set_cluster(c: ClusterManager) -> None: + global cluster + cluster = c + + +def add_disk(vol_id: str, disk) -> None: + disks[vol_id] = disk + + +def get_disk(vol_id: str): + return disks[vol_id] + + +def all_disks() -> list: + return list(disks.values()) \ No newline at end of file diff --git a/tools/craft_test_driver/test_registry.py b/tools/craft_test_driver/test_registry.py new file mode 100644 index 0000000..6b0fbf0 --- /dev/null +++ b/tools/craft_test_driver/test_registry.py @@ -0,0 +1,25 @@ +import importlib +import logging + +logger = logging.getLogger(__name__) + +TESTS = { + "basic": "tests.basic", +} + + +class TestNotFoundError(RuntimeError): + pass + + +def list_tests() -> list[str]: + return list(TESTS.keys()) + + +def run_test(name: str, device: str) -> None: + if name not in TESTS: + raise TestNotFoundError(f"unknown test '{name}'; available: {list_tests()}") + module = importlib.import_module(TESTS[name]) + logger.info("=== running test: %s ===", name) + module.run_all(device) + logger.info("=== test PASSED: %s ===", name) \ No newline at end of file diff --git a/tools/craft_test_driver/tests/basic.py b/tools/craft_test_driver/tests/basic.py new file mode 100644 index 0000000..47b49b1 --- /dev/null +++ b/tools/craft_test_driver/tests/basic.py @@ -0,0 +1,42 @@ +import logging +import time + +from fio_runner import run_fio + +logger = logging.getLogger(__name__) + + +class FioError(RuntimeError): + pass + + +def _timed(label: str, fn) -> None: + t0 = time.monotonic() + rc = fn() + logger.info("%s took %.1fs", label, time.monotonic() - t0) + if rc != 0: + raise FioError(f"{label} failed, fio exit code={rc}") + + +def write_read_test(device: str) -> None: + logger.info("=== basic write/read test on %s ===", device) + _timed("write phase", lambda: run_fio(device, job_name="basic_write", rw="write", + extra_args={"verify": "crc32c", "do_verify": "0"})) + _timed("read/verify phase", lambda: run_fio(device, job_name="basic_verify", rw="read", + extra_args={"verify": "crc32c", "do_verify": "1"})) + logger.info("=== basic write/read test PASSED ===") + + +def mixed_readwrite_test(device: str, runtime: int = 30) -> None: + logger.info("=== mixed read/write test on %s (runtime=%ds) ===", device, runtime) + _timed("mixed read/write phase", lambda: run_fio(device, job_name="mixed_readwrite", size="500m", rw="randrw", + runtime=runtime, + extra_args={"verify": "crc32c", "do_verify": "1", + "rwmixread": "50"})) + logger.info("=== mixed read/write test PASSED ===") + + +def run_all(device: str) -> None: + logger.info("starting basic tests") + write_read_test(device) + mixed_readwrite_test(device) \ No newline at end of file From f3d573dce46a58ad3ab73058947ab386e72d32a1 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 5 Aug 2026 05:06:34 -0700 Subject: [PATCH 11/24] review_comments --- src/mem/replica.cpp | 40 +++++++++++++++++++++++++++++++++ src/mem/replica.hpp | 4 ++++ src/net/tcp_server.cpp | 10 +-------- src/raft/raft_service.cpp | 16 +++++++++---- src/raft/raft_state_machine.hpp | 11 ++++++--- src/raft/raft_state_manager.cpp | 2 +- src/replica_mgr.cpp | 21 ++++++++++++----- src/replica_mgr.hpp | 4 ++-- 8 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 4dc461d..755d944 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares @@ -54,8 +55,42 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } return b; } + } // namespace +// background worker for raft commit to run replica's business logic +class MemCraftReplica::RaftCommitWorker { + std::queue< std::move_only_function< void() > > queue_; + std::mutex mtx_; + std::condition_variable_any cv_; + std::jthread worker_; + +public: + RaftCommitWorker() : worker_([this](std::stop_token st) { run(st); }) {} + + void push_task(std::move_only_function< void() > work) { + { + std::lock_guard lk(mtx_); + queue_.push(std::move(work)); + } + cv_.notify_one(); + } + +private: + void run(std::stop_token st) { + while (!st.stop_requested()) { + std::unique_lock lk(mtx_); + cv_.wait(lk, st, [this] { return !queue_.empty(); }); // wakes on stop too + if (st.stop_requested() && queue_.empty()) return; + + auto task = std::move(queue_.front()); + queue_.pop(); + lk.unlock(); + task(); + } + } +}; + void MemCraftReplica::init() { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); @@ -101,6 +136,9 @@ void MemCraftReplica::init() { } }; raft_inst->add_commit_cb(std::move(commit_cb)); + + // start background commit offload worker + commit_worker_ = std::make_unique< MemCraftReplica::RaftCommitWorker >(); } MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : @@ -115,6 +153,8 @@ MemCraftReplica::MemCraftReplica(server_geometry geo) : geo_{std::move(geo)} { geo_.lba_size, geo_.capacity); } +MemCraftReplica::~MemCraftReplica() = default; + // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── template < class Fn > diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index a611313..2c4fd9e 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -135,6 +135,8 @@ class MemCraftReplica final : public craft_replica, MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); MemCraftReplica(server_geometry geo); + ~MemCraftReplica(); + // Snapshot this replica's state. Takes mu_ and deliberately does NOT consult net_: do_write() locks // the transport before mu_, so reading net_ under mu_ here would invert that order. Callers that want // liveness (is_up / write_allowed) ask the transport themselves. @@ -247,6 +249,7 @@ class MemCraftReplica final : public craft_replica, std::shared_ptr< std::vector< uint8_t > > buf; // one page at buf->data()+off std::size_t off{0}; }; + class RaftCommitWorker; // Synchronous cores: the SERVER. Each takes mu_. Deliverability, latency and payload ownership are the // transport's job (MemTransport::send_*), which is why nothing below consults net_ or copies bytes. @@ -337,6 +340,7 @@ class MemCraftReplica final : public craft_replica, std::mutex login_mu_; std::condition_variable login_cv_; bool login_done_{false}; + std::unique_ptr< RaftCommitWorker > commit_worker_; }; } // namespace craft diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 6a588ea..24eb481 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -126,16 +126,7 @@ void craft_tcp_server::serve(craft_conn conn) { void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { // session_active_ is stoll maintained here, change it once we support multi volume std::vector< uint8_t > out; - if (session_active_) { - LOGWARN("craft_srv LOGIN [rid:{}]: rejected, a session is already active (term={})", req.hdr.request_id, - session_term_); - wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::not_eligible), - req.hdr.request_id, {}, {}); - conn.send_all(out); - return; - } session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection - session_active_ = true; auto const lr = wire::decode< wire::login_req >(req.op_header); LOGINFO("craft_srv LOGIN [rid:{}]: client_token={} new_term={}", req.hdr.request_id, lr.client_token, session_term_); @@ -176,6 +167,7 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { LOGINFO("craft_srv LOGIN [rid:{}]: SUCCESS term={} dlsn={} members={}", req.hdr.request_id, rsp.term, rsp.dlsn, rsp.member_count); + session_active_ = true; wire::frame_message(out, wire::op::login_rsp, static_cast< uint8_t >(wire::status::ok), req.hdr.request_id, as_bytes(rsp), body); conn.send_all(out); diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index a28e29b..dcc853d 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -34,10 +34,14 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { // raft global manager for commits nuraft::nuraft_global_mgr::init(); std::call_once(raft_started_, [&] { - auto const my_port = replica_manager::instance()->get(server_uuid)->raft_port; + auto const& my_info = replica_manager::instance()->get(server_uuid); + if (!my_info) { + LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid); + return; + } auto params = nuraft_mesg::manager::params{ .server_uuid_ = server_uuid, - .mesg_port_ = my_port, + .mesg_port_ = my_info->raft_port, .default_group_type_ = default_group_type_, }; consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true /*with_data_svc*/); @@ -144,7 +148,11 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGWARN("No leader for the raft group {}", group_id); return {}; } - return boost::uuids::string_generator()(raft_ctx->raft_leader_id()); + if (auto const leader_id = raft_ctx->raft_leader_id(); !leader_id.empty()) { + return boost::uuids::string_generator()(raft_ctx->raft_leader_id()); + } + LOGWARN("No leader for the raft group {}", group_id); + return {}; } template < typename MsgT > @@ -161,7 +169,7 @@ result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT co } auto const append_status = raft_ctx->raft_server()->append_entries({create_message(nlohmann::json(payload))}); - if (append_status && !append_status->get_accepted()) { + if (!append_status || !append_status->get_accepted()) { return std::unexpected(nuraft_mesg::to_condition(append_status->get_result_code())); } return {}; diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 3be77b7..3e9e906 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -59,7 +59,8 @@ using raft_commit_cb_t = class echo_state_machine : public nuraft::state_machine { public: - echo_state_machine(raft_commit_cb_t cb, std::string const& group_id) : commit_cb_(std::move(cb)), group_id_{group_id}, last_commit_idx_(0) {} + echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id) : + commit_cb_{cb}, group_id_{group_id}, last_commit_idx_(0) {} virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) override { nlohmann::json j; @@ -90,13 +91,17 @@ class echo_state_machine : public nuraft::state_machine { virtual nuraft::ptr< nuraft::snapshot > last_snapshot() override { return nuraft::ptr< nuraft::snapshot >(); } - virtual void create_snapshot(nuraft::snapshot& s, nuraft::async_result< bool >::handler_type& when_done) override {} + virtual void create_snapshot(nuraft::snapshot& s, nuraft::async_result< bool >::handler_type& when_done) override { + auto null_except = std::shared_ptr< std::exception >(); + auto ret_val{true}; + if (when_done) { when_done(ret_val, null_except); } + } virtual nuraft::ulong last_commit_index() override { return last_commit_idx_; } private: - nuraft::ulong last_commit_idx_; raft_commit_cb_t commit_cb_; std::string group_id_; + nuraft::ulong last_commit_idx_; }; } \ No newline at end of file diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index 9e71c26..6af4a47 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -118,7 +118,7 @@ void raft_state_mgr::save_state(const nuraft::srv_state& state) { uint32_t raft_state_mgr::get_logstore_id() const { return 0; } std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { - return std::make_shared< echo_state_machine >(std::move(_commit_cb), _group_id); + return std::make_shared< echo_state_machine >(_commit_cb, _group_id); } void raft_state_mgr::permanent_destroy() {} diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index 1ec3599..047ded7 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -63,21 +64,31 @@ std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) return it->second; } -void replica_manager::register_volume(boost::uuids::uuid const& vol_uuid, - std::vector< replica_endpoint > const& members) { +result< void > replica_manager::register_volume(boost::uuids::uuid const& vol_uuid, + std::vector< replica_endpoint > const& members) { std::lock_guard< std::shared_mutex > g{mu_}; - std::vector< replica_info > rinfos; + std::vector< boost::uuids::uuid > rinfos; for (auto const& m : members) { - rinfos.emplace_back(replicas_[m.id]); + if (auto const it = replicas_.find(m.id); it == replicas_.end()) { + LOGERROR("Unknown replica {} in the volume {}", boost::uuids::to_string(m.id), + boost::uuids::to_string(vol_uuid)); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + rinfos.emplace_back(m.id); } volumes_[vol_uuid] = rinfos; + return {}; } std::vector< replica_info > replica_manager::get_volume(boost::uuids::uuid const& volume_id) { std::shared_lock< std::shared_mutex > g(mu_); auto const it = volumes_.find(volume_id); if (it == volumes_.end()) return {}; - return it->second; + std::vector< replica_info > rinfo; + for (auto const& m_id : it->second) { + rinfo.emplace_back(replicas_[m_id]); // we check the existance of the m_id in replicas_ during vol registration. + } + return rinfo; } } // namespace craft \ No newline at end of file diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index 3451be9..5e8df0b 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -35,7 +35,7 @@ class replica_manager { // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". std::string lookup_peer(boost::uuids::uuid const& id) const; std::optional< replica_info > get(boost::uuids::uuid const& id) const; - void register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members); + result< void > register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members); std::vector< replica_info > get_volume(boost::uuids::uuid const& volume_id); private: @@ -45,7 +45,7 @@ class replica_manager { uint32_t page_size_; mutable std::shared_mutex mu_; std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once - std::map< boost::uuids::uuid, std::vector< replica_info > > volumes_; + std::map< boost::uuids::uuid, std::vector< boost::uuids::uuid > > volumes_; }; } // namespace craft \ No newline at end of file From 71767de13aea50255385fd050f857b81e3c8bef4 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Thu, 6 Aug 2026 00:51:00 -0700 Subject: [PATCH 12/24] rc2 --- CMakeLists.txt | 5 +- conanfile.py | 3 +- include/craft/client.hpp | 9 +-- include/craft/types.hpp | 13 +++- include/craft/wire.hpp | 9 +-- src/client_impl.hpp | 4 +- src/craft_peer.hpp | 1 - src/dlsn_tracker.cpp | 2 +- src/helper.hpp | 12 ++-- src/local.cpp | 3 +- src/mem/cluster.cpp | 6 +- src/mem/replica.cpp | 104 +++++++++++++++++++------------ src/mem/replica.hpp | 15 ++--- src/net/async_conn.cpp | 17 ++--- src/net/cluster_server.cpp | 4 +- src/net/conn.cpp | 5 +- src/net/tcp_cluster.cpp | 3 +- src/net/tcp_peer.cpp | 25 +++++--- src/net/tcp_peer.hpp | 5 +- src/net/tcp_replica.cpp | 18 +++--- src/net/tcp_server.cpp | 35 +++++++---- src/net/tcp_server.hpp | 2 +- src/raft/in_memory_log_store.hpp | 1 - src/raft/raft_helpers.hpp | 0 src/raft/raft_service.cpp | 34 +++++++--- src/raft/raft_service.hpp | 2 +- src/raft/raft_state_machine.hpp | 10 +-- src/raft/raft_state_manager.cpp | 2 +- src/raft/raft_state_manager.hpp | 4 +- src/replica_mgr.cpp | 14 ++--- src/replica_mgr.hpp | 7 +-- src/wire.cpp | 12 ++-- test/test_memory.cpp | 6 +- test/test_tcp_replica.cpp | 3 +- tools/server_config.json | 2 +- 35 files changed, 236 insertions(+), 161 deletions(-) delete mode 100644 src/raft/raft_helpers.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fb708d..34da42c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,11 +81,10 @@ add_library(craft_reference STATIC src/mem/cluster.cpp src/local.cpp # craft/local.hpp: the public "no-remote, in-process" backend builder over the mem model src/net/cluster_server.cpp - src/net/tcp_server.cpp - src/net/tcp_peer.cpp) + src/net/tcp_server.cpp) target_include_directories(craft_reference PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # the internal headers it implements -target_link_libraries(craft_reference PUBLIC craft_replica_mgr raft_service) +target_link_libraries(craft_reference PUBLIC raft_service) target_compile_features(craft_reference PUBLIC cxx_std_23) # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to diff --git a/conanfile.py b/conanfile.py index e764e96..a49ac68 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,7 +10,7 @@ class CraftClientConan(ConanFile): name = "craft_client" - version = "0.4.3" + version = "0.4.6" description = ( "CRAFT reference client + wire protocol -- transport-agnostic, HomeStore-free" @@ -66,7 +66,6 @@ def build_requirements(self): def requirements(self): # craft_wire is a std-only leaf and needs nothing. craft_types / craft_client (added as they land) pull # sisl (result / async::result / sg_list) and liburing (the io_uring transport); - # nuraft_mesg is used by the tcp_server for the tests only # declared here so the package graph is right from the start. self.requires("sisl/[^14.8]@oss/dev", transitive_headers=True) self.requires("liburing/[^2.4]", transitive_headers=True) diff --git a/include/craft/client.hpp b/include/craft/client.hpp index 8f8899c..77d7a00 100644 --- a/include/craft/client.hpp +++ b/include/craft/client.hpp @@ -24,9 +24,7 @@ #include #include -#include // sisl::async::light_result / ::light_status (the co_await-able result carrier) -#include // sisl::sg_list -#include // sisl::result / ::status / ::ok +#include // sisl::sg_list #include // the CRAFT vocabulary + the result / async_result aliases @@ -53,11 +51,6 @@ class craft_replica; // pass its ring or tolerate foreign-thread resumption; a blocking consumer (sisl::async::sync_get) is safe // either way. (The previous exec::task currency could hop an async consumer back to its own scheduler; // nothing in this stack used that, and the on-ring data path is built on NOT doing it.) -template < typename T > -using async_result = sisl::async::light_result< T >; -using async_status = sisl::async::light_status; -template < typename T > -using result = sisl::result< T >; // ── construction: the ONE seam ── // diff --git a/include/craft/types.hpp b/include/craft/types.hpp index fcda555..2580272 100644 --- a/include/craft/types.hpp +++ b/include/craft/types.hpp @@ -27,8 +27,10 @@ #include #include -#include // boost::uuids::uuid (== peer_id_t) -#include // ENUM +#include // boost::uuids::uuid (== peer_id_t) +#include // ENUM +#include // result types +#include // sisl::async::light_result / ::light_status (the co_await-able result carrier) namespace craft { @@ -39,6 +41,13 @@ using peer_id_t = boost::uuids::uuid; // each replica's peer_id_t deterministically from it (mem_replica_id). using volume_id_t = boost::uuids::uuid; +template < typename T > +using async_result = sisl::async::light_result< T >; +using async_status = sisl::async::light_status; + +template < typename T > +using result = sisl::result< T >; + // Network address of a replica, as returned in login()'s member list. struct replica_endpoint { peer_id_t id; diff --git a/include/craft/wire.hpp b/include/craft/wire.hpp index ad4546a..8fe0266 100644 --- a/include/craft/wire.hpp +++ b/include/craft/wire.hpp @@ -77,7 +77,8 @@ enum class op : uint8_t { // 1. keep the request=odd / response=even convention, and // 2. bump k_max_op below -- is_response() is a range check, so a peer op added without it is silently // misclassified as "not a response". That is the trap this constant exists to close. - create_volume = 15, // client-requested volume creation (leader-only) + + create_volume = 15, // client-requested volume creation: bootstrap-only create_volume_rsp = 16, get_rs_commit_lsn = 17, get_rs_commit_lsn_rsp = 18, @@ -231,8 +232,7 @@ struct volume_create_req { // volume_create_rsp: status only (no operation header, no body). // GetRSCommitLSN: non-RAFT peer query of a replica's {commit_lsn, last_append_lsn}. is_login triggers the -// quiesce barrier on the responder (see CRAFT Design's Login section). my_commit/my_append are the LEADER's -// own watermarks, riding the request per "the poll set includes the leader itself". +// quiesce barrier on the responder (see CRAFT Design's Login section). struct get_rs_commit_lsn_req { uint64_t term; uint8_t is_login; // bool, but keep POD-packed layout consistent with the rest of this file @@ -253,6 +253,7 @@ struct fetch_slot_desc { int64_t lsn; // which dLSN this is uint64_t lba; // where it writes to uint32_t len; // how many blocks + uint64_t byte_len; // data bytes size uint8_t is_empty; // Empty verdict? (no data follows) uint8_t all_zeros; // zero write? (no data follows) uint8_t reserved[2]; // padding @@ -284,7 +285,7 @@ static_assert(sizeof(volume_create_req) == 32); static_assert(sizeof(get_rs_commit_lsn_req) == 16); static_assert(sizeof(get_rs_commit_lsn_rsp) == 16); static_assert(sizeof(fetch_data_req) == 8); -static_assert(sizeof(fetch_slot_desc) == 24); +static_assert(sizeof(fetch_slot_desc) == 32); static_assert(sizeof(fetch_data_rsp) == 8); // The fixed operation-header size for an op code (0 for a status-only response). nullopt = unknown op, which // is unframeable -- the caller resets the connection. diff --git a/src/client_impl.hpp b/src/client_impl.hpp index 861353a..ed1adbd 100644 --- a/src/client_impl.hpp +++ b/src/client_impl.hpp @@ -40,7 +40,9 @@ class craft_client { public: craft_client(std::vector< std::shared_ptr< craft_replica > > replicas, uint32_t leader = 0, uint32_t max_inflight = 128) : - replicas_(std::move(replicas)), leader_(leader), tracker_(std::make_shared< dlsn_tracker >(max_inflight)) {} + replicas_(std::move(replicas)), + leader_(leader), + tracker_(std::make_shared< dlsn_tracker >(max_inflight)) {} // Mid-session verbs carry the caller's queue ring (`q`, null = the blocking tier) straight through to every // backend leg they fan out -- one IO's whole leg chain rides one ring, so its resumptions all land back on diff --git a/src/craft_peer.hpp b/src/craft_peer.hpp index dd82590..144dc13 100644 --- a/src/craft_peer.hpp +++ b/src/craft_peer.hpp @@ -47,7 +47,6 @@ #include // sisl::sg_list #include // the CRAFT vocabulary + the result / async_result aliases -#include // result types namespace craft { diff --git a/src/dlsn_tracker.cpp b/src/dlsn_tracker.cpp index c0c4911..dd5c837 100644 --- a/src/dlsn_tracker.cpp +++ b/src/dlsn_tracker.cpp @@ -284,7 +284,7 @@ async_result< read_plan > dlsn_tracker::plan_read(uint64_t addr, uint64_t len) { gate_.leave(); break; } - co_await *ev; + co_await* ev; gate_.leave(); } if (p.degraded()) co_return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); diff --git a/src/helper.hpp b/src/helper.hpp index 99c2626..d645346 100644 --- a/src/helper.hpp +++ b/src/helper.hpp @@ -1,6 +1,12 @@ +#pragma once #include #include #include +#include +#include +#include + +#include namespace craft { @@ -31,9 +37,7 @@ inline auto sync_get(Task&& task) { inline std::error_condition jsonObjectFromFile(std::string const& filename, json& json_object) { std::ifstream istrm(filename, std::ios::binary); - if (!istrm.is_open()) { - return std::make_error_condition(std::errc::no_such_file_or_directory); - } + if (!istrm.is_open()) { return std::make_error_condition(std::errc::no_such_file_or_directory); } istrm >> json_object; if (!json_object.is_object()) { @@ -43,4 +47,4 @@ inline std::error_condition jsonObjectFromFile(std::string const& filename, json return std::error_condition(); } -} \ No newline at end of file +} // namespace craft diff --git a/src/local.cpp b/src/local.cpp index aab5ed6..1841c77 100644 --- a/src/local.cpp +++ b/src/local.cpp @@ -29,7 +29,8 @@ namespace craft { class local_cluster { public: explicit local_cluster(MemReplicaGroup&& group) : - group_{std::move(group)}, backends_(group_.replicas.begin(), group_.replicas.end()) {} + group_{std::move(group)}, + backends_(group_.replicas.begin(), group_.replicas.end()) {} // ~MemReplicaGroup (inside group_) drains the reference pools while it still owns every replica. std::vector< std::shared_ptr< craft_replica > > const& backends() const { return backends_; } diff --git a/src/mem/cluster.cpp b/src/mem/cluster.cpp index bd8a3fd..9d01364 100644 --- a/src/mem/cluster.cpp +++ b/src/mem/cluster.cpp @@ -92,7 +92,7 @@ async_result< lsn_pair > MemTransport::send_write(std::shared_ptr< MemCraftRepli // submit. It resumes on one of THIS replica's server threads. Hold the awaitable in a local across the // suspension -- it is non-movable and that thread needs its address to stay put. auto ev = after(p.wait, id); - co_await *ev; + co_await* ev; if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); // Re-read: the peer may have gone down while this request was on the wire. @@ -112,7 +112,7 @@ async_result< read_result > MemTransport::send_read(std::shared_ptr< MemCraftRep auto const p = plan_delivery(rf->delay, op_timeout()); { auto ev = after(p.wait, id); // always suspends: the reply crosses the wire - co_await *ev; + co_await* ev; } if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); if (!to->fault_snapshot()->up) co_return fail(craft_error::REPLICA_DOWN); @@ -127,7 +127,7 @@ async_result< lsn_pair > MemTransport::send_keep_alive(std::shared_ptr< MemCraft auto const p = plan_delivery(rf->delay, op_timeout()); { auto ev = after(p.wait, id); // always suspends: the reply crosses the wire - co_await *ev; + co_await* ev; } if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); if (!to->fault_snapshot()->up) co_return fail(craft_error::REPLICA_DOWN); diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 755d944..0f6a201 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -14,7 +14,7 @@ *********************************************************************************/ #include "mem/replica.hpp" -#include "mem/cluster.hpp" // the full MemTransport type +#include "mem/cluster.hpp" // the full MemTransport type #include "raft/raft_service.hpp" // for raft channel #include "replica_mgr.hpp" #include "raft/raft_state_machine.hpp" // for raft message payload types @@ -56,6 +56,8 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { return b; } +constexpr auto LoginWaitTime = std::chrono::seconds(2); + } // namespace // background worker for raft commit to run replica's business logic @@ -86,7 +88,7 @@ class MemCraftReplica::RaftCommitWorker { auto task = std::move(queue_.front()); queue_.pop(); lk.unlock(); - task(); + if (task) { task(); } } } }; @@ -98,6 +100,7 @@ void MemCraftReplica::init() { fault_retired_.push_back(std::move(initial)); // register raft callbacks + // do not block commit thread, offload the business logic to the commit_worker auto raft_inst = raft_service::instance(); if (!raft_inst->is_raft_enabled()) { return; } auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& vol_uuid_str) { @@ -114,7 +117,8 @@ void MemCraftReplica::init() { } LOGDEBUG("commit[{}][vol={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, boost::uuids::to_string(vol_uuid), m.rs_commit_lsn, m.empty_slots.size()); - apply_sync(vol_uuid, m.rs_commit_lsn, m.client_token, m.empty_slots); + commit_worker_->push_task( + [this, vol_uuid, m = std::move(m)]() mutable { apply_sync(vol_uuid, std::move(m)); }); break; } case Operation::InternalLogin: { @@ -127,7 +131,7 @@ void MemCraftReplica::init() { } LOGDEBUG("commit[{}][vol={}]: applying InternalLogin term={} client_token={}", log_idx, boost::uuids::to_string(vol_uuid), m.term, m.client_token); - internal_login(m.client_token, m.term); + commit_worker_->push_task([this, vol_uuid, m = std::move(m)]() mutable { internal_login(m); }); break; } default: @@ -135,6 +139,10 @@ void MemCraftReplica::init() { break; } }; + + auto group_create_cb = [this](boost::uuids::uuid const& group_id) { + + }; raft_inst->add_commit_cb(std::move(commit_cb)); // start background commit offload worker @@ -142,7 +150,8 @@ void MemCraftReplica::init() { } MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : - geo_{.lba_size = page_size, .ep = std::move(ep)}, net_{std::move(net)} { + geo_{.lba_size = page_size, .ep = std::move(ep)}, + net_{std::move(net)} { init(); LOGDEBUG("MemCraftReplica constructed [id={}] page_size={}", boost::uuids::to_string(geo_.ep.id), page_size); } @@ -305,8 +314,7 @@ async_status MemCraftReplica::late_write(::io_uring* q, client_hdr hdr, int64_t if (lf->up && lf->write_ok) (void)do_write(hdr, dlsn, addr, len, std::move(bytes)); // dropped iff down in flight co_return ok(); } -async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring* /*q*/, client_hdr hdr, - int64_t upto) { +async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring* /*q*/, client_hdr hdr, int64_t upto) { if (!net_) co_return fail(craft_error::NO_QUORUM); // srv-seam replicas resolve via srv_resolve instead if (!is_up()) co_return fail(craft_error::REPLICA_DOWN); // Term-fenced like logout: a deposed client must not be able to void the successor's in-flight slots. @@ -353,8 +361,8 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 slot.term = hdr.term; slot.lba = addr / geo_.lba_size; // byte offset -> block index slot.len = static_cast< lba_count_t >(len / geo_.lba_size); // byte length -> block count - slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag - slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again + slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag + slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again journal_[dlsn] = std::move(slot); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); apply_up_to(hdr.commit_lsn); // piggybacked commit: advance the frontier best-effort, in dLSN order @@ -387,11 +395,13 @@ result< lsn_pair > MemCraftReplica::do_keep_alive(client_hdr hdr) { } result< lsn_pair > MemCraftReplica::do_lsns() { + if (net_ && !net_->is_up(geo_.ep.id)) return fail(craft_error::REPLICA_DOWN); std::lock_guard< std::mutex > g{mu_}; return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; } result< lsn_pair > MemCraftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { + // TODO implement quiesce barrier if (net_ && !net_->is_up(geo_.ep.id)) return fail(craft_error::REPLICA_DOWN); std::lock_guard< std::mutex > g{mu_}; return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; @@ -706,23 +716,26 @@ std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply( int64_t stalled_lsn{-1}; std::vector< int64_t > empty_slots; for (auto lsn : missing_lsns) { - uint32_t missing = 0; + uint32_t lacks_count = 1; bool is_data{false}; for (auto const& resp : all_responses) { - if (is_data) { break; } - if (auto const it = std::ranges::find_if(resp, [&](auto const& s) { return s.lsn == lsn; }); - it != resp.end()) { - if (it->is_empty) { - missing++; - } else { - is_data = true; - cold_install_slot(lsn, to_mem_journal_slot(*it, term)); - } + if (is_data) break; + auto const it = std::ranges::find_if(resp, [&](auto const& s) { return s.lsn == lsn; }); + if (it == resp.end()) { + // Omitted from a responding peer's list == "not-present-here": positive + // lacks-evidence per the FetchData contract. + ++lacks_count; + } else if (it->is_empty) { + // Peer already holds a prior positive Empty verdict: also lacks-evidence. + ++lacks_count; + } else { + is_data = true; + cold_install_slot(lsn, to_mem_journal_slot(*it, term)); } } - if (missing >= peers.size() / 2) { + if (lacks_count >= peers.size() / 2 + 1) { empty_slots.push_back(lsn); } else if (!is_data) { // unresolved lsn @@ -766,8 +779,14 @@ result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term) { auto raft_service_inst = raft_service::instance(); - // return cold path if raft service has not started + // A note on dlsn: The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one + // to use -- the client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, + // which skipped slot 0 on a fresh cluster: every replica was then permanently Missing dLSN 0, + // apply_up_to() stalled there forever, and commit_lsn pinned at -1 -- so no journal reclaimed and every read + // walked the whole tail. See wire.hpp. + if (!raft_service_inst->is_raft_enabled()) { + // return cold path if raft service has not started std::lock_guard< std::mutex > g{mu_}; state_.term = term; state_.client_token = client_token; @@ -803,7 +822,7 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con auto const members = replica_manager::instance()->get_volume(vol_uuid); LOGDEBUG("apply_login[vol={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(vol_uuid), members.size()); - for (auto const m : members) { + for (auto const& m : members) { if (m.id == geo_.ep.id) { continue; } if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term, true /* is_login */)); r) { LOGDEBUG("apply_login[vol={}]: peer {} reported commit_lsn={} last_append_lsn={}", @@ -838,8 +857,8 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con std::lock_guard< std::mutex > lk(login_mu_); login_done_ = false; } - if (auto const r = - raft_service_inst->propose(vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term}); + if (auto const r = raft_service_inst->propose( + vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); !r) { // TODO: any cleanup required? LOGERROR("apply_login[vol={}]: propose(InternalLogin term={}) failed: {}", boost::uuids::to_string(vol_uuid), @@ -853,7 +872,7 @@ result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > con // This happens in the internal login commit. Wait until that happens. { std::unique_lock< std::mutex > lk(login_mu_); - login_cv_.wait_for(lk, std::chrono::seconds(2), [&] { return login_done_; }); + login_cv_.wait_for(lk, LoginWaitTime, [&] { return login_done_; }); if (!login_done_) { LOGERROR("apply_login[vol={}]: timed out waiting for InternalLogin(term={}) commit callback", boost::uuids::to_string(vol_uuid), term); @@ -916,11 +935,17 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, std::vector< replica_endpoint > const& members) { auto const vol_uuid = craft::to_uuid(volume_id); + auto const& repl_mgr = replica_manager::instance(); + // return success if the volume exists + if (auto const vol = repl_mgr->get_volume(vol_uuid); !vol.empty()) { + LOGINFO("Volume {} exists! Returning ok", boost::uuids::to_string(vol_uuid)); + return {}; + } LOGINFO("srv_create_volume[vol={}]: creating with {} member(s)", boost::uuids::to_string(vol_uuid), members.size()); auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members); if (r) { - replica_manager::instance()->register_volume(vol_uuid, members); + repl_mgr->register_volume(vol_uuid, members); LOGINFO("srv_create_volume[vol={}]: SUCCESS", boost::uuids::to_string(vol_uuid)); } else { LOGERROR("srv_create_volume[vol={}]: FAILED: {}", boost::uuids::to_string(vol_uuid), r.error().message()); @@ -930,10 +955,9 @@ result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > cons // Follower-side catch-up on SyncRSCommitLSN apply. Verdicts are already decided by the leader (empty_slots) // -- this never decides Empty itself, only obeys the verdict list or fetches real data. -void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, - std::vector< int64_t > const& empty_slots) { +void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m) { // Verdicts first -- permanent no-ops, no fetch needed. - for (auto lsn : empty_slots) + for (auto lsn : m.empty_slots) cold_mark_empty(lsn); uint64_t term; @@ -942,7 +966,7 @@ void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_ term = state_.term; } - auto missing = get_missing_slots(rs_commit_lsn); + auto missing = get_missing_slots(m.rs_commit_lsn); auto const peers = replica_manager::instance()->get_volume(vol_uuid); for (auto const& peer : peers) { if (missing.empty()) break; @@ -965,15 +989,15 @@ void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_ if (!missing.empty()) { LOGERROR("apply_sync[vol={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " - "stall until the next SyncRSCommitLSN round -- first missing={}", - boost::uuids::to_string(vol_uuid), missing.size(), rs_commit_lsn, missing.front()); + "stall until the next SyncRSCommitLSN round -- first missing={}", + boost::uuids::to_string(vol_uuid), missing.size(), m.rs_commit_lsn, missing.front()); } std::lock_guard< std::mutex > g{mu_}; - apply_up_to(rs_commit_lsn); - rs_commit_lsn_.store(rs_commit_lsn, std::memory_order_relaxed); + apply_up_to(m.rs_commit_lsn); + rs_commit_lsn_.store(m.rs_commit_lsn, std::memory_order_relaxed); LOGDEBUG("apply_sync[vol={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(vol_uuid), - state_.commit_lsn, rs_commit_lsn); + state_.commit_lsn, m.rs_commit_lsn); } session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { @@ -981,16 +1005,16 @@ session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) return {state_.term, state_.client_token}; } -void MemCraftReplica::internal_login(uint64_t client_token, uint64_t term) { - cold_apply_login(client_token, term); - cold_truncate_above(rs_commit_lsn_.load(std::memory_order_relaxed)); +void MemCraftReplica::internal_login(InternalLoginMsg m) { + cold_apply_login(m.client_token, m.term); + if (m.rs_commit_lsn >= 0) { cold_truncate_above(m.rs_commit_lsn); } { std::lock_guard< std::mutex > lk(login_mu_); login_done_ = true; } login_cv_.notify_one(); - LOGINFO("internal_login [id={}]: InternalLogin COMMITTED term={} client_token={}", - boost::uuids::to_string(geo_.ep.id), term, client_token); + LOGINFO("internal_login [id={}]: InternalLogin COMMITTED term={} client_token={} rs_commit_lsn {}", + boost::uuids::to_string(geo_.ep.id), m.term, m.client_token, m.rs_commit_lsn); } } // namespace craft \ No newline at end of file diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 2c4fd9e..12762f7 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -35,7 +35,8 @@ #include #include -#include // result types +#include // async result types +#include // result types #include "craft_peer.hpp" // the PEER plane: craft_peer + JournalSlot + lba_t (this model is its only implementer) #include "craft_replica.hpp" // the CLIENT plane: the craft_replica interface @@ -117,6 +118,9 @@ struct session_info { uint64_t client_token; }; +struct SyncRSCommitLSNMsg; +struct InternalLoginMsg; + // enable_shared_from_this: a write the transport timed out is delivered late, from the transport's timer // thread. That closure must hold a WEAK reference here (a strong one would cycle: replica -> net_ -> closure // -> replica), so the replica must be reachable as a shared_ptr. It always is; make_mem_replica_group is the @@ -133,7 +137,7 @@ class MemCraftReplica final : public craft_replica, static constexpr std::size_t k_missing_sample = 16; MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); - MemCraftReplica(server_geometry geo); + explicit MemCraftReplica(server_geometry geo); ~MemCraftReplica(); @@ -291,11 +295,8 @@ class MemCraftReplica final : public craft_replica, void cold_truncate_above(int64_t rs_commit_lsn); // real hooks using raft channel - void apply_sync(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, - std::vector< int64_t > const& empty_slots); + void apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m); result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); - // void apply_logout(); - // void apply_truncate_above(int64_t rs_commit_lsn); // Misc helpers void init(); @@ -305,7 +306,7 @@ class MemCraftReplica final : public craft_replica, resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, uint64_t term); result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, uint64_t term); - void internal_login(uint64_t client_token, uint64_t term); + void internal_login(InternalLoginMsg m); // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. diff --git a/src/net/async_conn.cpp b/src/net/async_conn.cpp index bfbb8d0..fb13e63 100644 --- a/src/net/async_conn.cpp +++ b/src/net/async_conn.cpp @@ -40,7 +40,10 @@ constexpr std::size_t k_recv_chunk = 64 * 1024; // per-recv landing size; a mess } // namespace craft_async_conn::craft_async_conn(std::string host, uint16_t port, uint32_t max_tx, ::io_uring* ring) : - host_{std::move(host)}, port_{port}, max_tx_{max_tx}, ring_{ring} {} + host_{std::move(host)}, + port_{port}, + max_tx_{max_tx}, + ring_{ring} {} craft_async_conn::~craft_async_conn() { shutdown(); } @@ -108,20 +111,20 @@ sisl::async::light_task< int > craft_async_conn::ring_connect() { int const one = 1; ::setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); } - if (fd_ < 0) co_return -errno; + if (fd_ < 0) co_return - errno; sockaddr_in sa{}; sa.sin_family = AF_INET; sa.sin_port = htons(port_); - if (::inet_pton(AF_INET, host_.c_str(), &sa.sin_addr) != 1) co_return -EINVAL; // loopback / IP literal only (v1) + if (::inet_pton(AF_INET, host_.c_str(), &sa.sin_addr) != 1) co_return - EINVAL; // loopback / IP literal only (v1) sisl::async::cqe_awaitable ev; auto* sqe = acquire_sqe(); - if (nullptr == sqe) co_return -EAGAIN; + if (nullptr == sqe) co_return - EAGAIN; ::io_uring_prep_connect(sqe, fd_, reinterpret_cast< sockaddr* >(&sa), sizeof(sa)); // sa is frame-local, stable ::io_uring_sqe_set_data64(sqe, sisl::async::encode_managed_user_data(&ev)); int const res = co_await ev; - co_return (res < 0) ? res : 0; + co_return(res < 0) ? res : 0; } sisl::async::light_task< int > craft_async_conn::ring_send_all(std::span< uint8_t const > data) { @@ -129,11 +132,11 @@ sisl::async::light_task< int > craft_async_conn::ring_send_all(std::span< uint8_ while (off < data.size()) { sisl::async::cqe_awaitable ev; auto* sqe = acquire_sqe(); - if (nullptr == sqe) co_return -EAGAIN; + if (nullptr == sqe) co_return - EAGAIN; ::io_uring_prep_send(sqe, fd_, data.data() + off, data.size() - off, MSG_NOSIGNAL); ::io_uring_sqe_set_data64(sqe, sisl::async::encode_managed_user_data(&ev)); int const n = co_await ev; - if (n <= 0) co_return (n == 0 ? -EPIPE : n); // 0 = peer closed; <0 = error + if (n <= 0) co_return(n == 0 ? -EPIPE : n); // 0 = peer closed; <0 = error off += static_cast< std::size_t >(n); } co_return 0; diff --git a/src/net/cluster_server.cpp b/src/net/cluster_server.cpp index 14f34a1..31b48a0 100644 --- a/src/net/cluster_server.cpp +++ b/src/net/cluster_server.cpp @@ -431,9 +431,7 @@ void craft_cluster_server::set_replica_up(std::size_t idx, bool up) { p_->set_re void craft_cluster_server::force_subquorum(std::vector< std::size_t > keep) { p_->force_subquorum(std::move(keep)); } void craft_cluster_server::clear_faults() { p_->clear_faults(); } void craft_cluster_server::set_delay(std::size_t idx, std::chrono::milliseconds d) { p_->set_delay(idx, d); } -std::size_t craft_cluster_server::connections_accepted() const { - return p_->accepted.load(std::memory_order_relaxed); -} +std::size_t craft_cluster_server::connections_accepted() const { return p_->accepted.load(std::memory_order_relaxed); } std::size_t craft_cluster_server::journal_slots(std::size_t idx) const { return p_->journal_slots(idx); } uint64_t craft_cluster_server::replica_term(std::size_t idx) const { return p_->replica_term(idx); } bool craft_cluster_server::read_replica(std::size_t idx, int64_t read_lsn, uint64_t addr, uint64_t len, diff --git a/src/net/conn.cpp b/src/net/conn.cpp index bd7fe4d..9bd0a0d 100644 --- a/src/net/conn.cpp +++ b/src/net/conn.cpp @@ -53,7 +53,10 @@ void set_nodelay(int fd) { craft_conn::~craft_conn() { close_all(); } craft_conn::craft_conn(craft_conn&& o) noexcept : - fd_(o.fd_), ring_(o.ring_), ring_ready_(o.ring_ready_), rx_(std::move(o.rx_)) { + fd_(o.fd_), + ring_(o.ring_), + ring_ready_(o.ring_ready_), + rx_(std::move(o.rx_)) { // io_uring's pointers reference external mmap'd memory, so copying the struct and disarming the source // (so only we exit the ring) is a valid move. o.fd_ = -1; diff --git a/src/net/tcp_cluster.cpp b/src/net/tcp_cluster.cpp index b21591d..a7b5d05 100644 --- a/src/net/tcp_cluster.cpp +++ b/src/net/tcp_cluster.cpp @@ -32,7 +32,8 @@ class tcp_cluster { public: tcp_cluster(std::vector< std::shared_ptr< CraftTcpReplica > > proxies, std::vector< std::shared_ptr< craft_replica > > backends) : - proxies_{std::move(proxies)}, backends_{std::move(backends)} {} + proxies_{std::move(proxies)}, + backends_{std::move(backends)} {} ~tcp_cluster() { for (auto& p : proxies_) if (p) p->shutdown(); // drain each proxy from HERE (the destroying thread) before any of them drops diff --git a/src/net/tcp_peer.cpp b/src/net/tcp_peer.cpp index 81c338e..582070e 100644 --- a/src/net/tcp_peer.cpp +++ b/src/net/tcp_peer.cpp @@ -27,16 +27,18 @@ std::error_condition net_to_error(net_error e) { } } // namespace -CraftTcpPeer::CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, uint32_t page_size, - std::chrono::milliseconds op_timeout) : - host_{std::move(host)}, port_{port}, id_{id}, page_size_{page_size}, op_timeout_{op_timeout} {} +CraftTcpPeer::CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, std::chrono::milliseconds op_timeout) : + host_{std::move(host)}, + port_{port}, + id_{id}, + op_timeout_{op_timeout} {} CraftTcpPeer::~CraftTcpPeer() = default; bool CraftTcpPeer::ensure_connected() { if (connected_) return true; - auto c = craft_conn::connect(host_, port_, op_timeout_ > std::chrono::milliseconds{0} ? op_timeout_ - : k_connect_timeout); + auto c = + craft_conn::connect(host_, port_, op_timeout_ > std::chrono::milliseconds{0} ? op_timeout_ : k_connect_timeout); if (!c) return false; conn_ = std::move(*c); connected_ = true; @@ -112,7 +114,16 @@ async_result< std::vector< JournalSlot > > CraftTcpPeer::fetch_data(std::vector< out_slots.reserve(rsp.slot_count); std::size_t data_off = desc_bytes; + // --- Parsing assumptions --- + // Layout: the response body is N fixed-size `fetch_slot_desc` headers laid out contiguously + // starting at offset 0 (server writes all descriptors first, then all slot data after — + // see fetch_data handler), so descriptor i lives at byte offset i * sizeof(fetch_slot_desc). + // This is NOT self-describing in the response; it's an assumed layout convention shared by + // both peers. If the wire format ever interleaves descriptors and data, this indexing breaks + // silently with no compiler or protocol-level signal. for (std::size_t i = 0; i < rsp.slot_count; ++i) { + std::size_t const desc_off = i * sizeof(wire::fetch_slot_desc); + if (desc_off + sizeof(wire::fetch_slot_desc) > parsed->body.size()) { co_return fail(craft_error::INTERNAL); } auto const sd = wire::decode< wire::fetch_slot_desc >(parsed->body.subspan(i * sizeof(wire::fetch_slot_desc))); JournalSlot js; @@ -123,7 +134,7 @@ async_result< std::vector< JournalSlot > > CraftTcpPeer::fetch_data(std::vector< js.all_zeros = sd.all_zeros != 0; if (!js.is_empty && !js.all_zeros) { - std::size_t const nbytes = static_cast< std::size_t >(sd.len) * page_size_; + std::size_t const nbytes = static_cast< std::size_t >(sd.byte_len); if (data_off + nbytes > parsed->body.size()) co_return fail(craft_error::INTERNAL); js.owned_data = std::make_shared< std::vector< uint8_t > >(parsed->body.begin() + data_off, @@ -143,4 +154,4 @@ async_result< std::vector< JournalSlot > > CraftTcpPeer::fetch_data(std::vector< async_result< lsn_pair > CraftTcpPeer::get_lsns() { co_return fail(craft_error::NOT_IMPLEMENTED); } async_status CraftTcpPeer::truncate(int64_t lsn) { co_return fail(craft_error::NOT_IMPLEMENTED); } -} // namespace craft::net \ No newline at end of file +} // namespace craft::net diff --git a/src/net/tcp_peer.hpp b/src/net/tcp_peer.hpp index e150d55..965b98d 100644 --- a/src/net/tcp_peer.hpp +++ b/src/net/tcp_peer.hpp @@ -14,7 +14,7 @@ namespace craft::net { class CraftTcpPeer final : public craft_peer { public: - CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, uint32_t page_size, + CraftTcpPeer(std::string host, uint16_t port, peer_id_t id, std::chrono::milliseconds op_timeout = std::chrono::milliseconds{0}); ~CraftTcpPeer() override; @@ -39,7 +39,6 @@ class CraftTcpPeer final : public craft_peer { std::string host_; uint16_t port_; peer_id_t id_; - uint32_t page_size_; std::chrono::milliseconds op_timeout_{0}; craft_conn conn_; @@ -47,4 +46,4 @@ class CraftTcpPeer final : public craft_peer { uint16_t next_rid_{1}; }; -} // namespace craft::net \ No newline at end of file +} // namespace craft::net diff --git a/src/net/tcp_replica.cpp b/src/net/tcp_replica.cpp index f50c2a8..1cc9d9f 100644 --- a/src/net/tcp_replica.cpp +++ b/src/net/tcp_replica.cpp @@ -88,8 +88,7 @@ net::craft_async_conn* CraftTcpReplica::conn_for(::io_uring* q) { std::size_t const n = n_slots_.load(std::memory_order_acquire); for (std::size_t i = 0; i < n; ++i) { if (slots_[i].ring == q) { - assert(slots_[i].owner == std::this_thread::get_id() && - "a ring must only be passed from its owner thread"); + assert(slots_[i].owner == std::this_thread::get_id() && "a ring must only be passed from its owner thread"); return slots_[i].conn.get(); } } @@ -235,7 +234,7 @@ std::optional< std::error_condition > CraftTcpReplica::ensure_bound(uint64_t ter async_result< LoginResult > CraftTcpReplica::login(uint64_t client_token) { auto ev = hop(); - co_await *ev; // now on the session-mgr thread + co_await* ev; // now on the session-mgr thread if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); auto r = conn_.login(vol_id_, client_token); // LOGIN names the volume, exactly as HELO does if (!r) co_return std::unexpected(on_net_fault(r.error())); @@ -267,7 +266,7 @@ async_result< LoginResult > CraftTcpReplica::login(uint64_t client_token) { async_status CraftTcpReplica::logout(client_hdr hdr) { auto ev = hop(); - co_await *ev; + co_await* ev; if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); (void)hdr; auto r = conn_.logout(); @@ -277,8 +276,7 @@ async_status CraftTcpReplica::logout(client_hdr hdr) { } async_result< lsn_pair > CraftTcpReplica::write(::io_uring* q, client_hdr hdr, int64_t dlsn, uint64_t addr, - uint64_t len, - sisl::sg_list data) { + uint64_t len, sisl::sg_list data) { // Serialize the payload NOW, on the CALLER's thread, before the first suspension. This write may be a // straggler that keeps running after craft_client acked at quorum and the caller recycled its buffer, so // nothing past here may reference `data` (the when_quorum payload contract). This owned copy is the mem @@ -304,7 +302,7 @@ async_result< lsn_pair > CraftTcpReplica::write(::io_uring* q, client_hdr hdr, i } auto ev = hop(); - co_await *ev; + co_await* ev; if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.write(dlsn, addr, len, payload, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); @@ -337,7 +335,7 @@ async_result< read_result > CraftTcpReplica::read(::io_uring* q, client_hdr hdr, reply = std::move(*r); } else { auto ev = hop(); - co_await *ev; + co_await* ev; if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.read(read_lsn, addr, len, d, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); @@ -373,7 +371,7 @@ async_result< lsn_pair > CraftTcpReplica::keep_alive(::io_uring* q, client_hdr h } auto ev = hop(); - co_await *ev; + co_await* ev; if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.keep_alive(hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); @@ -396,7 +394,7 @@ async_result< resolution_result > CraftTcpReplica::request_resolution(::io_uring // No-ring tier: the blocking session-mgr path, like login/logout. auto ev = hop(); - co_await *ev; + co_await* ev; if (auto e = ensure_bound(hdr.term, hdr.client_token)) co_return std::unexpected(*e); auto r = conn_.resolve(upto, hdr.commit_lsn, hdr.all_committed_lsn); if (!r) co_return std::unexpected(on_net_fault(r.error())); diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 24eb481..9499270 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -45,7 +45,7 @@ craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& serve // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { - replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id, geo.lba_size); + replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id); raft_service::instance()->start_raft_service(geo.ep.id); LOGINFO("craft_tcp_server: replica_manager + raft_service started [id={}]", boost::uuids::to_string(geo.ep.id)); } else { @@ -124,7 +124,7 @@ void craft_tcp_server::serve(craft_conn conn) { } void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { - // session_active_ is stoll maintained here, change it once we support multi volume + // session_active_ is still maintained here, change it once we support multi volume std::vector< uint8_t > out; session_term_ = ++next_term_; // a fresh session term, established (and fenced) on this connection auto const lr = wire::decode< wire::login_req >(req.op_header); @@ -175,19 +175,26 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { auto const hr = wire::decode< wire::helo_req >(req.op_header); - - // Fence: HELO must present the term + token of the session the replica already knows about. - // Only binds this connection if it matches. - auto const current = replica_->srv_session_info(hr.volume_id); wire::status code = wire::status::ok; - bool is_raft_enabled = raft_service::instance()->is_raft_enabled(); - - if (is_raft_enabled && (hr.term != current.term || hr.client_token != current.client_token)) { + if (bool is_raft_enabled = raft_service::instance()->is_raft_enabled(); !is_raft_enabled) { + // no raft, follow fake cold path + auto result = replica_->srv_establish(hr.volume_id, hr.client_token, session_term_); + if (!result) { + LOGERROR("craft_srv HELO [rid:{}]: token: {}, term: {}, srv_establish failed: {}", req.hdr.request_id, + hr.client_token, hr.term, result.error().message()); + code = to_wire_status(result.error()); + } + } else if (auto const current = replica_->srv_session_info(hr.volume_id); + hr.term != current.term || hr.client_token != current.client_token) { + // Raft is enabled, Fence: HELO must present the term + token of the session the replica already knows about. + // Only binds this connection if it matches. LOGWARN("craft_srv HELO [rid:{}]: FENCED -- presented term={} token={}, current term={} token={}", req.hdr.request_id, hr.term, hr.client_token, current.term, current.client_token); code = wire::status::stale_term; - } else { + } + + if (code == wire::status::ok) { session_term_ = hr.term; session_active_ = true; LOGDEBUG("craft_srv HELO [rid:{}]: bound connection at term={}", req.hdr.request_id, hr.term); @@ -355,8 +362,8 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r for (auto const& m : *members) { replica_members.emplace_back(replica_endpoint{.id = craft::to_uuid(m.id), .addr = m.addr}); } - auto const r = replica_->srv_create_volume(cr.volume_id, replica_members); - if (!r) { + + if (auto const r = replica_->srv_create_volume(cr.volume_id, replica_members); !r) { LOGERROR("craft_srv CREATE_VOLUME [rid:{}]: srv_create_volume failed: {}", req.hdr.request_id, r.error().message()); code = to_wire_status(r.error()); @@ -395,6 +402,7 @@ void craft_tcp_server::on_get_rs_commit_lsn(craft_conn& conn, wire::message cons } void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) { + // TODO: Apply max_tx cap and chunking for very large fetch requests. auto const fr = wire::decode< wire::fetch_data_req >(req.op_header); auto const lsns = wire::decode_lsns(req.body, fr.lsn_count); @@ -412,7 +420,6 @@ void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) code = to_wire_status(r.error()); } else { rsp.slot_count = static_cast< uint32_t >(r->size()); - // descriptors first (fixed-size, easy to walk), then concatenated data for non-empty/non-zero slots for (auto const& slot : *r) { wire::fetch_slot_desc sd{}; sd.lsn = slot.lsn; @@ -420,6 +427,8 @@ void craft_tcp_server::on_fetch_data(craft_conn& conn, wire::message const& req) sd.len = slot.len; sd.is_empty = slot.is_empty ? 1 : 0; sd.all_zeros = slot.all_zeros ? 1 : 0; + sd.byte_len = 0; + if (!slot.is_empty && !slot.all_zeros) { sd.byte_len += slot.data.size; } wire::put(body, sd); } for (auto const& slot : *r) { diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index b8f19db..a880381 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -35,7 +35,7 @@ namespace craft { class MemCraftReplica; // the server's state backing (pimpl; included only in craft_tcp_server.cpp) struct server_geometry; -} +} // namespace craft namespace craft::net { diff --git a/src/raft/in_memory_log_store.hpp b/src/raft/in_memory_log_store.hpp index 6cc8e1f..f5a802a 100644 --- a/src/raft/in_memory_log_store.hpp +++ b/src/raft/in_memory_log_store.hpp @@ -88,4 +88,3 @@ class inmem_log_store : public log_store { }; } // namespace nuraft - diff --git a/src/raft/raft_helpers.hpp b/src/raft/raft_helpers.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index dcc853d..55e70cc 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -58,16 +58,22 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { } raft_service::~raft_service() { - consensus_.reset(); - nuraft::nuraft_global_mgr::shutdown(); + if (consensus_) { + consensus_.reset(); + nuraft::nuraft_global_mgr::shutdown(); + } } result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members) { - auto consensus = raft_service::instance()->get_consensus(); + if (!consensus_) { + // raft_service::srv_create_volume should not be called if raft service is not enabled + LOGERROR("Raft not enabled!"); + return fail(craft_error::INTERNAL); + } // Seat THIS replica as leader by creating the group. - if (auto const status = craft::sync_get(consensus->create_group(group_id, raft_service::default_group_type_)); + if (auto const status = craft::sync_get(consensus_->create_group(group_id, raft_service::default_group_type_)); !status) { return fail(craft_error::INTERNAL); } @@ -79,9 +85,8 @@ result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_i } if (m.id == server_uuid_) continue; - auto srv_cfg = - nuraft::srv_config(nuraft_mesg::to_server_id(m.id), 0, boost::uuids::to_string(m.id), "", false); - if (auto const result = craft::sync_get(consensus->add_member(group_id, srv_cfg)); !result) { + auto srv_cfg = nuraft::srv_config(nuraft_mesg::to_server_id(m.id), 0, boost::uuids::to_string(m.id), "", false); + if (auto const result = craft::sync_get(consensus_->add_member(group_id, srv_cfg)); !result) { return fail(craft_error::INTERNAL); } } @@ -124,10 +129,20 @@ void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::s void raft_service::add_commit_cb(raft_commit_cb_t cb) { // we expect that this is called only once + if (!consensus_) { + // raft_service::srv_create_volume should not be called if raft service is not enabled + LOGERROR("Raft not enabled!"); + return; + } commit_cb_ = std::move(cb); } bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { + if (!consensus_) { + // raft_service::srv_create_volume should not be called if raft service is not enabled + LOGERROR("Raft not enabled!"); + return false; + } auto const state_mgr = get_state_mgr(group_id); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); @@ -138,6 +153,11 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { } nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& group_id) { + if (!consensus_) { + // raft_service::srv_create_volume should not be called if raft service is not enabled + LOGERROR("Raft not enabled!"); + return {}; + } auto const state_mgr = get_state_mgr(group_id); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 1ee63b6..2be4427 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -37,7 +37,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab bool is_leader(nuraft_mesg::group_id_t const& group_id); nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); - // raft append entrues + // raft append entries template < typename MsgT > result< void > propose(boost::uuids::uuid const& group_id, MsgT const& payload); diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 3e9e906..68e925f 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -40,6 +40,7 @@ inline void from_json(nlohmann::json const& j, SyncRSCommitLSNMsg& m) { struct InternalLoginMsg { uint64_t client_token{0}; uint64_t term{0}; + int64_t rs_commit_lsn{-1}; }; inline void to_json(nlohmann::json& j, InternalLoginMsg const& m) { @@ -54,13 +55,14 @@ inline void from_json(nlohmann::json const& j, InternalLoginMsg& m) { j.at("term").get_to(m.term); } -using raft_commit_cb_t = - std::function< void(uint64_t log_idx, nlohmann::json const& j, std::string const& group_id) >; +using raft_commit_cb_t = std::function< void(uint64_t log_idx, nlohmann::json const& j, std::string const& group_id) >; class echo_state_machine : public nuraft::state_machine { public: echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id) : - commit_cb_{cb}, group_id_{group_id}, last_commit_idx_(0) {} + commit_cb_{cb}, + group_id_{group_id}, + last_commit_idx_(0) {} virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) override { nlohmann::json j; @@ -104,4 +106,4 @@ class echo_state_machine : public nuraft::state_machine { std::string group_id_; nuraft::ulong last_commit_idx_; }; -} \ No newline at end of file +} // namespace craft diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index 6af4a47..b525270 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -125,4 +125,4 @@ void raft_state_mgr::permanent_destroy() {} void raft_state_mgr::leave() {} -} +} // namespace craft diff --git a/src/raft/raft_state_manager.hpp b/src/raft/raft_state_manager.hpp index 18443bc..c9b1fca 100644 --- a/src/raft/raft_state_manager.hpp +++ b/src/raft/raft_state_manager.hpp @@ -31,7 +31,7 @@ class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { int32_t const _srv_id; std::string const _srv_addr; std::string const _group_id; - raft_commit_cb_t _commit_cb; // temp storage, will be passed on to state machine + raft_commit_cb_t _commit_cb; }; -} +} // namespace craft diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp index 047ded7..4c95b6d 100644 --- a/src/replica_mgr.cpp +++ b/src/replica_mgr.cpp @@ -15,10 +15,8 @@ std::shared_ptr< replica_manager > replica_manager::instance() { return inst; } -void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid, - uint32_t page_size) { +void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid) { id_ = my_uuid; - page_size_ = page_size; std::ifstream istrm(path, std::ios::binary); if (!istrm.is_open()) { LOGERROR("replica_manager: could not open {}", path); @@ -42,10 +40,10 @@ void replica_manager::start_replica_service(std::string const& path, boost::uuid .host = m.at("host").get< std::string >(), .raft_port = m.at("raft_port").get< uint16_t >(), .tcp_port = m.at("tcp_port").get< uint16_t >(), - .peer_client = (id == my_uuid) ? nullptr - : std::make_shared< net::CraftTcpPeer >( - m.at("host").get< std::string >(), - m.at("tcp_port").get< uint16_t >(), id, page_size_), + .peer_client = (id == my_uuid) + ? nullptr + : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), + m.at("tcp_port").get< uint16_t >(), id), }); } } @@ -91,4 +89,4 @@ std::vector< replica_info > replica_manager::get_volume(boost::uuids::uuid const return rinfo; } -} // namespace craft \ No newline at end of file +} // namespace craft diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index 5e8df0b..c4abbdc 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -30,7 +30,7 @@ class replica_manager { public: static std::shared_ptr< replica_manager > instance(); - void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid, uint32_t page_size); + void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid); // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". std::string lookup_peer(boost::uuids::uuid const& id) const; @@ -42,10 +42,9 @@ class replica_manager { replica_manager() = default; boost::uuids::uuid id_; - uint32_t page_size_; mutable std::shared_mutex mu_; - std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once + std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once std::map< boost::uuids::uuid, std::vector< boost::uuids::uuid > > volumes_; }; -} // namespace craft \ No newline at end of file +} // namespace craft diff --git a/src/wire.cpp b/src/wire.cpp index f0740a2..0ed7849 100644 --- a/src/wire.cpp +++ b/src/wire.cpp @@ -58,11 +58,13 @@ bool is_response(uint8_t op_code) noexcept { } bool op_allows_body(uint8_t op_code) noexcept { - return op_code == static_cast< uint8_t >(op::write) || // data - op_code == static_cast< uint8_t >(op::login_rsp) || // member list - op_code == static_cast< uint8_t >(op::read_rsp) || // extents + data - op_code == static_cast< uint8_t >(op::resolve_rsp) || // Empty-verdict dLSN list - op_code == static_cast< uint8_t >(op::create_volume); // member list + return op_code == static_cast< uint8_t >(op::write) || // data + op_code == static_cast< uint8_t >(op::login_rsp) || // member list + op_code == static_cast< uint8_t >(op::read_rsp) || // extents + data + op_code == static_cast< uint8_t >(op::resolve_rsp) || // Empty-verdict dLSN list + op_code == static_cast< uint8_t >(op::create_volume) || // member list + op_code == static_cast< uint8_t >(op::fetch_data) || // lsns + op_code == static_cast< uint8_t >(op::fetch_data_rsp); // slot_count x fetch_slot_desc + data } namespace { diff --git a/test/test_memory.cpp b/test/test_memory.cpp index 5e663c1..aa86f9e 100644 --- a/test/test_memory.cpp +++ b/test/test_memory.cpp @@ -223,8 +223,8 @@ TEST(CraftMemModel, CommitPiggybacksOnWrite) { auto& r = *g.replicas[0]; auto b0 = page_of(0x11), b1 = page_of(0x22); ASSERT_TRUE(rg(r.write(nullptr, chdr(term), 0, blk(5), blk(1), one_iov(b0))).has_value()); // no commit yet - ASSERT_TRUE( - rg(r.write(nullptr, chdr(term, /*commit_lsn*/ 0), 1, blk(6), blk(1), one_iov(b1))).has_value()); // rides commit 0 + ASSERT_TRUE(rg(r.write(nullptr, chdr(term, /*commit_lsn*/ 0), 1, blk(6), blk(1), one_iov(b1))) + .has_value()); // rides commit 0 auto ls = rg(r.get_lsns()); ASSERT_TRUE(ls.has_value()); @@ -431,7 +431,7 @@ TEST(CraftMemModel, ClearingADelayLeavesAMissingSlotThatDrains) { auto const slow = rg(r.write(nullptr, chdr(term), 1, blk(2), blk(1), one_iov(buf))); // dLSN 1 times out ASSERT_FALSE(slow.has_value()); - r.set_delay(std::chrono::milliseconds{0}); // straggler recovers + r.set_delay(std::chrono::milliseconds{0}); // straggler recovers ASSERT_TRUE(rg(r.write(nullptr, chdr(term), 2, blk(3), blk(1), one_iov(buf))).has_value()); // dLSN 2 lands now ASSERT_TRUE(rg(r.keep_alive(nullptr, chdr(term, 2))).has_value()); diff --git a/test/test_tcp_replica.cpp b/test/test_tcp_replica.cpp index 7a75105..683b347 100644 --- a/test/test_tcp_replica.cpp +++ b/test/test_tcp_replica.cpp @@ -69,7 +69,8 @@ TEST(CraftTcpReplica, LoginWriteReadLogout) { uint64_t const term = lr->term; auto data = page_of(0xAB); - ASSERT_TRUE(rg(proxy.write(nullptr, chdr(term, /*commit=*/0), /*dlsn=*/0, /*addr=*/0, PAGE, one_iov(data))).has_value()); + ASSERT_TRUE(rg(proxy.write(nullptr, chdr(term, /*commit=*/0), /*dlsn=*/0, /*addr=*/0, PAGE, one_iov(data))) + .has_value()); std::vector< uint8_t > dst(PAGE, 0); auto r = rg(proxy.read(nullptr, chdr(term), /*read_lsn=*/0, /*addr=*/0, PAGE, one_iov(dst))); diff --git a/tools/server_config.json b/tools/server_config.json index ed82af6..35bbc39 100644 --- a/tools/server_config.json +++ b/tools/server_config.json @@ -19,4 +19,4 @@ "tcp_port": 7003 } ] -} \ No newline at end of file +} From 352963faabac3368ac03cf1a2d69c879eb7ad1b7 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 12 Aug 2026 07:02:33 -0700 Subject: [PATCH 13/24] remove server dependency on craft_reference library. Add raft replica --- CMakeLists.txt | 57 ++-- conanfile.py | 23 +- src/mem/replica.cpp | 511 ++---------------------------- src/mem/replica.hpp | 90 ++---- src/net/tcp_server.cpp | 32 +- src/net/tcp_server.hpp | 16 +- src/raft/raft_replica.cpp | 471 +++++++++++++++++++++++++++ src/raft/raft_replica.hpp | 82 +++++ test/test_tcp.cpp | 18 +- tools/craft_reference_tcp_srv.cpp | 11 +- 10 files changed, 685 insertions(+), 626 deletions(-) create mode 100644 src/raft/raft_replica.cpp create mode 100644 src/raft/raft_replica.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 34da42c..5e53271 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,17 @@ target_include_directories(craft_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(craft_client PUBLIC craft_wire craft_types sisl::sisl liburing::liburing) target_compile_features(craft_client PUBLIC cxx_std_23) +# ── craft_reference: the in-memory reference model and the local cluster builder used by the client +add_library(craft_reference STATIC + src/mem/replica.cpp + src/mem/cluster.cpp + src/local.cpp # craft/local.hpp: the public "no-remote, in-process" backend builder over the mem model +) +target_include_directories(craft_reference PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # the internal headers it implements +target_link_libraries(craft_reference PUBLIC craft_client) +target_compile_features(craft_reference PUBLIC cxx_std_23) + # ── craft_replica_mgr: the internal replica manager (testing only) ── add_library(craft_replica_mgr STATIC src/replica_mgr.cpp @@ -66,7 +77,6 @@ target_include_directories(craft_replica_mgr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} target_link_libraries(craft_replica_mgr PUBLIC craft_client) target_compile_features(craft_replica_mgr PUBLIC cxx_std_23) -# ── craft_reference: the in-memory reference model + loopback cluster server (test-support only) ── add_library(raft_service STATIC src/raft/raft_service.cpp src/raft/in_memory_log_store.cpp @@ -76,24 +86,28 @@ target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(raft_service PUBLIC nuraft_mesg::proto craft_replica_mgr) target_compile_features(raft_service PUBLIC cxx_std_23) -add_library(craft_reference STATIC - src/mem/replica.cpp - src/mem/cluster.cpp - src/local.cpp # craft/local.hpp: the public "no-remote, in-process" backend builder over the mem model - src/net/cluster_server.cpp +add_library(raft_tcp_server STATIC + src/raft/raft_replica.cpp src/net/tcp_server.cpp) -target_include_directories(craft_reference PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) -target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # the internal headers it implements -target_link_libraries(craft_reference PUBLIC raft_service) -target_compile_features(craft_reference PUBLIC cxx_std_23) - +target_include_directories(raft_tcp_server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(raft_tcp_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(raft_tcp_server PUBLIC craft_reference craft_replica_mgr raft_service) +target_compile_features(raft_tcp_server PUBLIC cxx_std_23) + # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to # form a multi-process cluster; point a client (ublkpp_disk --craft-tcp) at them. Test/dev support, not installed. -add_executable(craft_reference_tcp_srv tools/craft_reference_tcp_srv.cpp) -target_link_libraries(craft_reference_tcp_srv PRIVATE craft_reference) +add_executable(craft_reference_tcp_srv src/net/tcp_server.cpp tools/craft_reference_tcp_srv.cpp) +target_link_libraries(craft_reference_tcp_srv PRIVATE raft_tcp_server) target_include_directories(craft_reference_tcp_srv PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) # net/tcp_server.hpp target_compile_features(craft_reference_tcp_srv PRIVATE cxx_std_23) +add_library(cluster_server STATIC + src/net/cluster_server.cpp) +target_include_directories(cluster_server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(cluster_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(cluster_server PUBLIC craft_reference) +target_compile_features(cluster_server PUBLIC cxx_std_23) + # ── tests ── add_executable(test_wire test/test_wire.cpp) target_link_libraries(test_wire PRIVATE craft_wire GTest::gtest GTest::gtest_main) @@ -106,20 +120,21 @@ add_test(NAME CraftTypes COMMAND test_types) # Behavioral suites: link the libs under test + the shared sisl-logging main (direct object, not gtest_main). function(craft_test name) - add_executable(${name} test/${name}.cpp test/craft_test_main.cpp) - target_link_libraries(${name} PRIVATE ${ARGN} GTest::gmock) + cmake_parse_arguments(ARG "" "" "SOURCES" ${ARGN}) + add_executable(${name} test/${name}.cpp test/craft_test_main.cpp ${ARG_SOURCES}) + target_link_libraries(${name} PRIVATE ${ARG_UNPARSED_ARGUMENTS} GTest::gmock) add_test(NAME ${name} COMMAND ${name}) endfunction() craft_test(test_api craft_reference) # the PUBLIC surface, and ONLY it -- the canary that include/ suffices -craft_test(test_tcp_builder craft_reference) # craft/tcp.hpp: connect() a client to a reference server +craft_test(test_tcp_builder cluster_server) # craft/tcp.hpp: connect() a client to a reference server craft_test(test_memory craft_reference) craft_test(test_dlsn_tracker craft_client) -craft_test(test_cluster craft_reference) -craft_test(test_tcp craft_reference) -craft_test(test_tcp_replica craft_reference) +craft_test(test_cluster cluster_server) +craft_test(test_tcp raft_tcp_server) +craft_test(test_tcp_replica cluster_server) craft_test(test_async_mem craft_reference) # the on-ring async transport (prepare_for_async) driven at depth -craft_test(test_async_tcp craft_reference) # the on-ring TCP transport (recv-demux pump) over real loopback sockets +craft_test(test_async_tcp cluster_server) # the on-ring TCP transport (recv-demux pump) over real loopback sockets # Every suite EXCEPT test_wire / test_types / test_api reaches an internal header under src/ -- the reference # model, the TCP server/proxy, the craft_replica interface, the dlsn_tracker. That is the point of the split: @@ -138,7 +153,7 @@ function(craft_test_ownmain name) endfunction() craft_test_ownmain(test_client craft_reference) -craft_test_ownmain(test_client_tcp craft_reference) +craft_test_ownmain(test_client_tcp cluster_server) # white-box: these two reach the concrete craft_client + read_route_map (internal headers under src/) target_include_directories(test_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(test_client_tcp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) diff --git a/conanfile.py b/conanfile.py index a49ac68..62fce15 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,7 +10,7 @@ class CraftClientConan(ConanFile): name = "craft_client" - version = "0.4.6" + version = "0.4.7" description = ( "CRAFT reference client + wire protocol -- transport-agnostic, HomeStore-free" @@ -62,6 +62,7 @@ def config_options(self): def build_requirements(self): self.test_requires("gtest/[^1.17]") + self.test_requires("nuraft_mesg/[^5.0]@oss/dev") def requirements(self): # craft_wire is a std-only leaf and needs nothing. craft_types / craft_client (added as they land) pull @@ -69,7 +70,6 @@ def requirements(self): # declared here so the package graph is right from the start. self.requires("sisl/[^14.8]@oss/dev", transitive_headers=True) self.requires("liburing/[^2.4]", transitive_headers=True) - self.requires("nuraft_mesg/[^5.0]@oss/dev", transitive_headers=True) def validate(self): if self.info.settings.compiler.cppstd: @@ -171,26 +171,7 @@ def package_info(self): "liburing::liburing", ] - self.cpp_info.components["craft_replica_mgr"].libs = ["craft_replica_mgr"] - self.cpp_info.components["craft_replica_mgr"].requires = [ - "craft_types", - "craft_wire", - "nuraft_mesg::nuraft_mesg", - ] - - self.cpp_info.components["craft_raft_service"].libs = [ - "raft_service" - ] # actual CMake target name - self.cpp_info.components["craft_raft_service"].requires = [ - "craft_types", - "sisl::sisl", - "nuraft_mesg::nuraft_mesg", - "craft_replica_mgr", - ] - self.cpp_info.components["craft_reference"].libs = ["craft_reference"] self.cpp_info.components["craft_reference"].requires = [ "craft_client", - "craft_raft_service", - "craft_replica_mgr", ] diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 0f6a201..1d1dcc6 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -14,24 +14,15 @@ *********************************************************************************/ #include "mem/replica.hpp" -#include "mem/cluster.hpp" // the full MemTransport type -#include "raft/raft_service.hpp" // for raft channel -#include "replica_mgr.hpp" -#include "raft/raft_state_machine.hpp" // for raft message payload types -#include "helper.hpp" -#include "craft/types.hpp" +#include "mem/cluster.hpp" // the full MemTransport type #include #include #include #include -#include -#include -#include #include // the on-ring data path: SQE prep / user_data #include // sisl::async::cqe_awaitable + the managed-user_data contract the reap loop shares -#include namespace craft { @@ -55,115 +46,16 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } return b; } - -constexpr auto LoginWaitTime = std::chrono::seconds(2); - } // namespace -// background worker for raft commit to run replica's business logic -class MemCraftReplica::RaftCommitWorker { - std::queue< std::move_only_function< void() > > queue_; - std::mutex mtx_; - std::condition_variable_any cv_; - std::jthread worker_; - -public: - RaftCommitWorker() : worker_([this](std::stop_token st) { run(st); }) {} - - void push_task(std::move_only_function< void() > work) { - { - std::lock_guard lk(mtx_); - queue_.push(std::move(work)); - } - cv_.notify_one(); - } - -private: - void run(std::stop_token st) { - while (!st.stop_requested()) { - std::unique_lock lk(mtx_); - cv_.wait(lk, st, [this] { return !queue_.empty(); }); // wakes on stop too - if (st.stop_requested() && queue_.empty()) return; - - auto task = std::move(queue_.front()); - queue_.pop(); - lk.unlock(); - if (task) { task(); } - } - } -}; - -void MemCraftReplica::init() { +MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : + ep_{std::move(ep)}, page_size_{page_size}, net_{std::move(net)} { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); fault_retired_.push_back(std::move(initial)); - - // register raft callbacks - // do not block commit thread, offload the business logic to the commit_worker - auto raft_inst = raft_service::instance(); - if (!raft_inst->is_raft_enabled()) { return; } - auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& vol_uuid_str) { - auto const vol_uuid = boost::uuids::string_generator()(vol_uuid_str); - auto const op_val = j.at("op").get< int >(); - switch (static_cast< Operation >(op_val)) { - case Operation::SyncRSCommitLSN: { - SyncRSCommitLSNMsg m; - try { - m = j.get< SyncRSCommitLSNMsg >(); - } catch (nlohmann::json::exception const& e) { - LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); - return; - } - LOGDEBUG("commit[{}][vol={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, - boost::uuids::to_string(vol_uuid), m.rs_commit_lsn, m.empty_slots.size()); - commit_worker_->push_task( - [this, vol_uuid, m = std::move(m)]() mutable { apply_sync(vol_uuid, std::move(m)); }); - break; - } - case Operation::InternalLogin: { - InternalLoginMsg m; - try { - m = j.get< InternalLoginMsg >(); - } catch (nlohmann::json::exception const& e) { - LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); - return; - } - LOGDEBUG("commit[{}][vol={}]: applying InternalLogin term={} client_token={}", log_idx, - boost::uuids::to_string(vol_uuid), m.term, m.client_token); - commit_worker_->push_task([this, vol_uuid, m = std::move(m)]() mutable { internal_login(m); }); - break; - } - default: - LOGERROR("commit[{}]: unknown op={}", log_idx, op_val); - break; - } - }; - - auto group_create_cb = [this](boost::uuids::uuid const& group_id) { - - }; - raft_inst->add_commit_cb(std::move(commit_cb)); - - // start background commit offload worker - commit_worker_ = std::make_unique< MemCraftReplica::RaftCommitWorker >(); -} - -MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : - geo_{.lba_size = page_size, .ep = std::move(ep)}, - net_{std::move(net)} { - init(); - LOGDEBUG("MemCraftReplica constructed [id={}] page_size={}", boost::uuids::to_string(geo_.ep.id), page_size); } -MemCraftReplica::MemCraftReplica(server_geometry geo) : geo_{std::move(geo)} { - init(); - LOGDEBUG("MemCraftReplica constructed [id={}] lba_size={} capacity={}", boost::uuids::to_string(geo_.ep.id), - geo_.lba_size, geo_.capacity); -} - -MemCraftReplica::~MemCraftReplica() = default; - // ── fault injection (COW; readers never block, and a reader holding the old snapshot stays valid) ── template < class Fn > @@ -177,19 +69,15 @@ void MemCraftReplica::mutate_faults(Fn&& fn) { } void MemCraftReplica::set_up(bool up) { - LOGINFO("fault injection: set_up({}) [id={}]", up, boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.up = up; }); } void MemCraftReplica::set_delay(std::chrono::milliseconds d) { - LOGINFO("fault injection: set_delay({}ms) [id={}]", d.count(), boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.delay = (d.count() > 0) ? d : std::chrono::milliseconds{0}; }); } void MemCraftReplica::drop_writes(bool drop) { - LOGINFO("fault injection: drop_writes({}) [id={}]", drop, boost::uuids::to_string(geo_.ep.id)); mutate_faults([&](replica_faults& s) { s.write_ok = !drop; }); } void MemCraftReplica::clear_faults() { - LOGINFO("fault injection: clear_faults [id={}]", boost::uuids::to_string(geo_.ep.id)); mutate_faults([](replica_faults& s) { s = replica_faults{}; }); } bool MemCraftReplica::is_up() const { return fault_snapshot()->up; } @@ -209,7 +97,6 @@ async_status MemCraftReplica::logout(client_hdr hdr) { std::lock_guard< std::mutex > g{mu_}; if (hdr.term != state_.term) co_return fail(craft_error::STALE_TERM); } - LOGINFO("logout [id={}] term={}", boost::uuids::to_string(geo_.ep.id), hdr.term); co_return net_ ? net_->run_logout(this, hdr.term) : ok(); } // Two data paths behind one interface, chosen by the verb's leading `q`. Null q: every op crosses the wire @@ -314,7 +201,8 @@ async_status MemCraftReplica::late_write(::io_uring* q, client_hdr hdr, int64_t if (lf->up && lf->write_ok) (void)do_write(hdr, dlsn, addr, len, std::move(bytes)); // dropped iff down in flight co_return ok(); } -async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring* /*q*/, client_hdr hdr, int64_t upto) { +async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring* /*q*/, client_hdr hdr, + int64_t upto) { if (!net_) co_return fail(craft_error::NO_QUORUM); // srv-seam replicas resolve via srv_resolve instead if (!is_up()) co_return fail(craft_error::REPLICA_DOWN); // Term-fenced like logout: a deposed client must not be able to void the successor's in-flight slots. @@ -322,12 +210,11 @@ async_result< resolution_result > MemCraftReplica::request_resolution(::io_uring std::lock_guard< std::mutex > g{mu_}; if (hdr.term != state_.term) co_return fail(craft_error::STALE_TERM); } - LOGDEBUG("request_resolution [id={}] term={} upto={}", boost::uuids::to_string(geo_.ep.id), hdr.term, upto); co_return net_->run_resolution(this, hdr.term, upto); } async_result< lsn_pair > MemCraftReplica::get_lsns() { co_return do_lsns(); } -async_result< lsn_pair > MemCraftReplica::get_rs_commit_lsn(uint64_t term, bool is_login) { co_return do_lsns(); } +async_result< lsn_pair > MemCraftReplica::get_rs_commit_lsn(uint64_t, bool) { co_return do_lsns(); } async_result< std::vector< JournalSlot > > MemCraftReplica::fetch_data(std::vector< int64_t > lsns) { co_return do_fetch(lsns); } @@ -342,7 +229,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 std::shared_ptr< std::vector< uint8_t > > bytes) { // Deliverability is the transport's verdict, not ours: by the time we are called, the request arrived. // byte-based API: addr/len must be block-aligned (the model works in page_size blocks internally). - if (addr % geo_.lba_size != 0 || len % geo_.lba_size != 0 || len == 0) { + if (addr % page_size_ != 0 || len % page_size_ != 0 || len == 0) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } if (bytes && (bytes->size() != len)) { @@ -359,10 +246,10 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 MemJournalSlot slot; slot.term = hdr.term; - slot.lba = addr / geo_.lba_size; // byte offset -> block index - slot.len = static_cast< lba_count_t >(len / geo_.lba_size); // byte length -> block count - slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag - slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again + slot.lba = addr / page_size_; // byte offset -> block index + slot.len = static_cast< lba_count_t >(len / page_size_); // byte length -> block count + slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag + slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again journal_[dlsn] = std::move(slot); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); apply_up_to(hdr.commit_lsn); // piggybacked commit: advance the frontier best-effort, in dLSN order @@ -373,7 +260,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 result< read_result > MemCraftReplica::do_read(client_hdr hdr, int64_t read_lsn, uint64_t addr, uint64_t len, sisl::sg_list dest) { // byte-based API: addr/len block-aligned; dest is a single contiguous buffer covering [addr,addr+len) - if (addr % geo_.lba_size != 0 || len % geo_.lba_size != 0 || len == 0) { + if (addr % page_size_ != 0 || len % page_size_ != 0 || len == 0) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } if (dest.size < len) { return std::unexpected(std::make_error_condition(std::errc::invalid_argument)); } @@ -395,22 +282,13 @@ result< lsn_pair > MemCraftReplica::do_keep_alive(client_hdr hdr) { } result< lsn_pair > MemCraftReplica::do_lsns() { - if (net_ && !net_->is_up(geo_.ep.id)) return fail(craft_error::REPLICA_DOWN); - std::lock_guard< std::mutex > g{mu_}; - return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; -} - -result< lsn_pair > MemCraftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { - // TODO implement quiesce barrier - if (net_ && !net_->is_up(geo_.ep.id)) return fail(craft_error::REPLICA_DOWN); + if (net_ && !net_->is_up(ep_.id)) return fail(craft_error::REPLICA_DOWN); std::lock_guard< std::mutex > g{mu_}; return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; } status MemCraftReplica::do_truncate(int64_t lsn) { std::lock_guard< std::mutex > g{mu_}; - LOGDEBUG("do_truncate [id={}] above lsn={} (last_append_lsn was {})", boost::uuids::to_string(geo_.ep.id), lsn, - state_.last_append_lsn); journal_.erase(journal_.upper_bound(lsn), journal_.end()); state_.last_append_lsn = std::min(state_.last_append_lsn, lsn); return ok(); @@ -436,7 +314,6 @@ result< std::vector< JournalSlot > > MemCraftReplica::do_fetch(std::vector< int6 } out.push_back(std::move(js)); } - LOGDEBUG("do_fetch [id={}] requested={} returned={}", boost::uuids::to_string(geo_.ep.id), lsns.size(), out.size()); return out; } @@ -461,8 +338,6 @@ result< resolution_result > MemCraftReplica::do_resolve_local(client_hdr hdr, in } state_.last_append_lsn = std::max(state_.last_append_lsn, upto); apply_up_to(upto); - LOGDEBUG("do_resolve_local [id={}] upto={} empty_slots={} commit_lsn now {}", boost::uuids::to_string(geo_.ep.id), - upto, out.empty_slots.size(), state_.commit_lsn); return out; } @@ -475,7 +350,7 @@ void MemCraftReplica::apply_slot(int64_t dlsn, MemJournalSlot const& s) { } } else { for (lba_count_t i = 0; i < s.len; ++i) { - index_[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * geo_.lba_size}; + index_[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * page_size_}; } } } @@ -506,11 +381,11 @@ MemCraftReplica::MemJournalSlot const* MemCraftReplica::highest_slot_le(lba_t x, std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, uint64_t len, sisl::sg_list const& dest) { - lba_t const lba0 = addr / geo_.lba_size; - lba_count_t const nblk = static_cast< lba_count_t >(len / geo_.lba_size); + lba_t const lba0 = addr / page_size_; + lba_count_t const nblk = static_cast< lba_count_t >(len / page_size_); std::vector< io_extent > layout; - // Scatter writer: advances through dest's iovecs sequentially, one geo_.lba_size chunk at a time. + // Scatter writer: advances through dest's iovecs sequentially, one page_size_ chunk at a time. std::size_t iov_idx{0}, iov_off{0}; auto sg_write = [&](uint8_t const* src, std::size_t n) { while (n > 0 && iov_idx < dest.iovs.size()) { @@ -541,7 +416,7 @@ std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, u bool hole = true; if (auto* s = highest_slot_le(x, H)) { if (!s->all_zeros) { - page = s->bytes->data() + static_cast< std::size_t >(x - s->lba) * geo_.lba_size; + page = s->bytes->data() + static_cast< std::size_t >(x - s->lba) * page_size_; hole = false; } // else: zero write => hole } else if (auto it = index_.find(x); it != index_.end()) { @@ -549,17 +424,17 @@ std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, u hole = false; } // read-time scan: an all-zero data page reads back thin (as a hole). - if (!hole && all_zero(page, geo_.lba_size)) hole = true; + if (!hole && all_zero(page, page_size_)) hole = true; // fill the caller's scatter-gather buffer: data pages get bytes, holes get zeros. - sg_write(hole ? nullptr : page, geo_.lba_size); + sg_write(hole ? nullptr : page, page_size_); // coalesce the returned layout, in BYTES, with the previous extent if contiguous. - uint64_t const x_addr = static_cast< uint64_t >(x) * geo_.lba_size; + uint64_t const x_addr = static_cast< uint64_t >(x) * page_size_; if (!layout.empty() && layout.back().hole == hole && layout.back().addr + layout.back().len == x_addr) { - layout.back().len += geo_.lba_size; + layout.back().len += page_size_; } else { - layout.push_back(io_extent{x_addr, geo_.lba_size, hole}); + layout.push_back(io_extent{x_addr, page_size_, hole}); } } return layout; @@ -571,9 +446,9 @@ replica_stats MemCraftReplica::stats() const { std::lock_guard< std::mutex > g{mu_}; replica_stats s; - s.id = geo_.ep.id; - s.addr = geo_.ep.addr; - s.page_size = geo_.lba_size; + s.id = ep_.id; + s.addr = ep_.addr; + s.page_size = page_size_; s.commit_lsn = state_.commit_lsn; s.last_append_lsn = state_.last_append_lsn; s.term = state_.term; @@ -591,7 +466,7 @@ replica_stats MemCraftReplica::stats() const { } else if (slot.all_zeros) { ++s.zero_write_slots; } else { - s.journal_data_bytes += static_cast< uint64_t >(slot.len) * geo_.lba_size; + s.journal_data_bytes += static_cast< uint64_t >(slot.len) * page_size_; } } @@ -634,266 +509,20 @@ void MemCraftReplica::cold_apply_sync(int64_t rs_commit_lsn, uint64_t /*client_t } void MemCraftReplica::cold_apply_login(uint64_t client_token, uint64_t term) { std::lock_guard< std::mutex > g{mu_}; - LOGDEBUG("cold_apply_login [id={}] client_token={} term={} (was term={})", boost::uuids::to_string(geo_.ep.id), - client_token, term, state_.term); state_.client_token = client_token; state_.term = term; } void MemCraftReplica::cold_apply_logout() { std::lock_guard< std::mutex > g{mu_}; - LOGINFO("cold_apply_logout [id={}] clearing term={} client_token={}", boost::uuids::to_string(geo_.ep.id), - state_.term, state_.client_token); state_.client_token = 0; state_.term = 0; // no active session; subsequent IOs with old term fail STALE_TERM } void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { std::lock_guard< std::mutex > g{mu_}; - LOGDEBUG("cold_truncate_above [id={}] rs_commit_lsn={} (last_append_lsn was {})", - boost::uuids::to_string(geo_.ep.id), rs_commit_lsn, state_.last_append_lsn); journal_.erase(journal_.upper_bound(rs_commit_lsn), journal_.end()); state_.last_append_lsn = std::min(state_.last_append_lsn, rs_commit_lsn); } -// ── peer comm hooks (driven by raft) ── - -MemCraftReplica::MemJournalSlot MemCraftReplica::to_mem_journal_slot(JournalSlot const& j, uint64_t term) { - std::shared_ptr< std::vector< uint8_t > > bytes; - if (j.owned_data) { - bytes = j.owned_data; - } else if (j.data.size > 0) { - bytes = take_payload(j.data); - } - return MemJournalSlot{ - .term = term, - .lba = j.lba, - .len = j.len, - .all_zeros = j.all_zeros, - .is_empty = j.is_empty, - .bytes = std::move(bytes), - }; -} - -std::vector< int64_t > MemCraftReplica::get_missing_slots(int64_t watermark) { - std::lock_guard< std::mutex > g{mu_}; - std::vector< int64_t > missing; - int64_t expect = state_.commit_lsn + 1; - auto it = journal_.lower_bound(expect); // first present entry >= expect - for (; it != journal_.end() && it->first <= watermark; ++it) { - for (; expect < it->first; ++expect) - missing.push_back(expect); // gap before this entry - expect = it->first + 1; - } - for (; expect <= watermark; ++expect) - missing.push_back(expect); // trailing gap after the last present entry - LOGDEBUG("get_missing_slots [id={}] watermark={} missing_count={}", boost::uuids::to_string(geo_.ep.id), watermark, - missing.size()); - return missing; -} - -std::pair< std::vector< int64_t >, int64_t > MemCraftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, - int64_t watermark, - uint64_t client_token, uint64_t term) { - auto const peers = replica_manager::instance()->get_volume(vol_uuid); - auto const missing_lsns = get_missing_slots(watermark); - LOGDEBUG("resolve_and_apply[vol={}] watermark={} missing={} peers={}", boost::uuids::to_string(vol_uuid), watermark, - missing_lsns.size(), peers.size()); - if (missing_lsns.empty()) { return {{}, -1}; } - - // Brute force, no optimizations for now - // Step 1: ask every peer for the full missing list, collect ALL responses first. - std::vector< std::vector< JournalSlot > > all_responses; - for (auto const& peer : peers) { - if (peer.id == geo_.ep.id) { continue; } - if (auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing_lsns)); r) { - all_responses.emplace_back(std::move(r.value())); - } else { - LOGWARN("resolve_and_apply[vol={}]: fetch_data to peer {} failed/unreachable, error: {}", - boost::uuids::to_string(vol_uuid), boost::uuids::to_string(peer.id), r.error().message()); - } - } - - // Step 2: for each requested lsn, look across every response and decide its fate. - int64_t stalled_lsn{-1}; - std::vector< int64_t > empty_slots; - for (auto lsn : missing_lsns) { - uint32_t lacks_count = 1; - bool is_data{false}; - - for (auto const& resp : all_responses) { - if (is_data) break; - auto const it = std::ranges::find_if(resp, [&](auto const& s) { return s.lsn == lsn; }); - if (it == resp.end()) { - // Omitted from a responding peer's list == "not-present-here": positive - // lacks-evidence per the FetchData contract. - ++lacks_count; - } else if (it->is_empty) { - // Peer already holds a prior positive Empty verdict: also lacks-evidence. - ++lacks_count; - } else { - is_data = true; - cold_install_slot(lsn, to_mem_journal_slot(*it, term)); - } - } - - if (lacks_count >= peers.size() / 2 + 1) { - empty_slots.push_back(lsn); - } else if (!is_data) { - // unresolved lsn - stalled_lsn = (stalled_lsn == -1) ? lsn : std::min(lsn, stalled_lsn); - } - } - if (stalled_lsn != -1) { - LOGWARN("resolve_and_apply[vol={}]: could not resolve past lsn={} (quorum-lacks evidence insufficient)", - boost::uuids::to_string(vol_uuid), stalled_lsn); - } else { - LOGDEBUG("resolve_and_apply[vol={}]: fully resolved up to watermark={}, empty_slots={}", - boost::uuids::to_string(vol_uuid), watermark, empty_slots.size()); - } - return {empty_slots, stalled_lsn}; -} - -result< void > MemCraftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, - uint64_t client_token, uint64_t term) { - auto const [empty_slots, stalled_lsn] = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); - if (stalled_lsn != -1) { - // leader could not resolve all the missing lsns - LOGERROR("sync_rs_commit_lsn[vol={}]: leader could not resolve all missing lsns, stalled at {}", - boost::uuids::to_string(vol_uuid), stalled_lsn); - return std::unexpected(make_error_condition(craft_error::INTERNAL)); - } - if (auto const r = raft_service::instance()->propose(vol_uuid, - SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, - .client_token = client_token, - .empty_slots = std::move(empty_slots)}); - !r) { - // TODO: any cleanup required? - LOGERROR("sync_rs_commit_lsn[vol={}]: propose(SyncRSCommitLSN={}) failed: {}", - boost::uuids::to_string(vol_uuid), rs_commit_lsn, r.error().message()); - return std::unexpected(r.error()); - } - LOGINFO("sync_rs_commit_lsn[vol={}]: proposed rs_commit_lsn={} OK", boost::uuids::to_string(vol_uuid), - rs_commit_lsn); - return {}; -} - -result< LoginResult > MemCraftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, - uint64_t term) { - auto raft_service_inst = raft_service::instance(); - // A note on dlsn: The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one - // to use -- the client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, - // which skipped slot 0 on a fresh cluster: every replica was then permanently Missing dLSN 0, - // apply_up_to() stalled there forever, and commit_lsn pinned at -1 -- so no journal reclaimed and every read - // walked the whole tail. See wire.hpp. - - if (!raft_service_inst->is_raft_enabled()) { - // return cold path if raft service has not started - std::lock_guard< std::mutex > g{mu_}; - state_.term = term; - state_.client_token = client_token; - LOGINFO("apply_login [id={}]: raft disabled, cold-path login OK, term={} token={}", - boost::uuids::to_string(geo_.ep.id), term, client_token); - return LoginResult{.members = {geo_.ep}, - .dLSN = state_.last_append_lsn, - .term = state_.term, - .lba_size = geo_.lba_size, - .capacity = geo_.capacity, - .max_tx = geo_.max_tx}; - } - // Phase 1: collect replica LSN state (non-RAFT broadcast) - // 1.1: accepted by leader only. - // TODO: what happens if the leader changes before the login is complete? - auto vol_uuid = craft::to_uuid(volume_id); - LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(vol_uuid), client_token, - term); - if (!raft_service_inst->is_leader(vol_uuid)) { - LOGERROR("current replica not a raft leader"); - return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; - } - - // 1.2 collect replica LSN state (non-RAFT broadcast) - std::vector< lsn_pair > peer_resp; - uint64_t current_term; - { - std::lock_guard< std::mutex > g{mu_}; - current_term = state_.term; - peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); - } - - auto const members = replica_manager::instance()->get_volume(vol_uuid); - LOGDEBUG("apply_login[vol={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(vol_uuid), - members.size()); - for (auto const& m : members) { - if (m.id == geo_.ep.id) { continue; } - if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term, true /* is_login */)); r) { - LOGDEBUG("apply_login[vol={}]: peer {} reported commit_lsn={} last_append_lsn={}", - boost::uuids::to_string(vol_uuid), boost::uuids::to_string(m.id), r->commit_lsn, - r->last_append_lsn); - peer_resp.emplace_back(r.value()); - } else { - LOGWARN("apply_login[vol={}]: peer {} did not respond to GetRSCommitLSN", boost::uuids::to_string(vol_uuid), - boost::uuids::to_string(m.id)); - } - } - // compute watermark as max(quorum.last_append) - if (peer_resp.size() <= members.size() / 2) { - LOGERROR("apply_login[vol={}]: quorum not reached ({} of {} responded)", boost::uuids::to_string(vol_uuid), - peer_resp.size(), members.size()); - return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); - } - auto const rs_commit_lsn = std::ranges::max_element(peer_resp, {}, &lsn_pair::last_append_lsn)->last_append_lsn; - LOGINFO("apply_login[vol={}]: computed rs_commit_lsn={} from {} responder(s)", boost::uuids::to_string(vol_uuid), - rs_commit_lsn, peer_resp.size()); - - // Phase 1b: Leader behind - resolve all the missing lsns and - // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) - if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, current_term); !r) { - LOGERROR("apply_login[vol={}]: sync_rs_commit_lsn failed: {}", boost::uuids::to_string(vol_uuid), - r.error().message()); - return std::unexpected(r.error()); - } - - // Phase 3: InternalLogin(token, term) via RAFT - { - std::lock_guard< std::mutex > lk(login_mu_); - login_done_ = false; - } - if (auto const r = raft_service_inst->propose( - vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); - !r) { - // TODO: any cleanup required? - LOGERROR("apply_login[vol={}]: propose(InternalLogin term={}) failed: {}", boost::uuids::to_string(vol_uuid), - term, r.error().message()); - return std::unexpected(r.error()); - } - LOGDEBUG("apply_login[vol={}]: InternalLogin(term={}) proposed, waiting for commit", - boost::uuids::to_string(vol_uuid), term); - - // Phase 4: truncate above rs_commit_lsn - // This happens in the internal login commit. Wait until that happens. - { - std::unique_lock< std::mutex > lk(login_mu_); - login_cv_.wait_for(lk, LoginWaitTime, [&] { return login_done_; }); - if (!login_done_) { - LOGERROR("apply_login[vol={}]: timed out waiting for InternalLogin(term={}) commit callback", - boost::uuids::to_string(vol_uuid), term); - return std::unexpected(make_error_condition(craft_error::INTERNAL)); - } - } - - std::vector< replica_endpoint > replicas; - for (auto const& m : members) { - replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); - } - LOGINFO("apply_login[vol={}]: LOGIN SUCCESS term={} dLSN={} members={}", boost::uuids::to_string(vol_uuid), term, - rs_commit_lsn, replicas.size()); - return LoginResult{.members = replicas, - .dLSN = rs_commit_lsn, - .term = term, - .lba_size = geo_.lba_size, - .capacity = geo_.capacity, - .max_tx = geo_.max_tx}; -} - // ── resolution-round hooks (driven by MemTransport::run_resolution) ── std::optional< MemCraftReplica::MemJournalSlot > MemCraftReplica::peek_slot(int64_t dlsn) { @@ -931,90 +560,4 @@ std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { return out; } -// create peer raft group and add members to it. -result< void > MemCraftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members) { - auto const vol_uuid = craft::to_uuid(volume_id); - auto const& repl_mgr = replica_manager::instance(); - // return success if the volume exists - if (auto const vol = repl_mgr->get_volume(vol_uuid); !vol.empty()) { - LOGINFO("Volume {} exists! Returning ok", boost::uuids::to_string(vol_uuid)); - return {}; - } - LOGINFO("srv_create_volume[vol={}]: creating with {} member(s)", boost::uuids::to_string(vol_uuid), members.size()); - - auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members); - if (r) { - repl_mgr->register_volume(vol_uuid, members); - LOGINFO("srv_create_volume[vol={}]: SUCCESS", boost::uuids::to_string(vol_uuid)); - } else { - LOGERROR("srv_create_volume[vol={}]: FAILED: {}", boost::uuids::to_string(vol_uuid), r.error().message()); - } - return r; -} - -// Follower-side catch-up on SyncRSCommitLSN apply. Verdicts are already decided by the leader (empty_slots) -// -- this never decides Empty itself, only obeys the verdict list or fetches real data. -void MemCraftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m) { - // Verdicts first -- permanent no-ops, no fetch needed. - for (auto lsn : m.empty_slots) - cold_mark_empty(lsn); - - uint64_t term; - { - std::lock_guard< std::mutex > g{mu_}; - term = state_.term; - } - - auto missing = get_missing_slots(m.rs_commit_lsn); - auto const peers = replica_manager::instance()->get_volume(vol_uuid); - for (auto const& peer : peers) { - if (missing.empty()) break; - if (peer.id == geo_.ep.id) continue; // don't ask self - - auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing)); - if (!r) continue; // unreachable, try next peer - - std::erase_if(missing, [&](int64_t lsn) { - auto const it = std::ranges::find_if(*r, [&](auto const& s) { return s.lsn == lsn; }); - if (it == r->end()) return false; // this peer doesn't have it either - if (it->is_empty) { - cold_mark_empty(lsn); // a prior verdict this peer already knows about - } else { - cold_install_slot(lsn, to_mem_journal_slot(*it, term)); - } - return true; - }); - } - - if (!missing.empty()) { - LOGERROR("apply_sync[vol={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " - "stall until the next SyncRSCommitLSN round -- first missing={}", - boost::uuids::to_string(vol_uuid), missing.size(), m.rs_commit_lsn, missing.front()); - } - - std::lock_guard< std::mutex > g{mu_}; - apply_up_to(m.rs_commit_lsn); - rs_commit_lsn_.store(m.rs_commit_lsn, std::memory_order_relaxed); - LOGDEBUG("apply_sync[vol={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(vol_uuid), - state_.commit_lsn, m.rs_commit_lsn); -} - -session_info MemCraftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { - std::lock_guard< std::mutex > g{mu_}; - return {state_.term, state_.client_token}; -} - -void MemCraftReplica::internal_login(InternalLoginMsg m) { - cold_apply_login(m.client_token, m.term); - if (m.rs_commit_lsn >= 0) { cold_truncate_above(m.rs_commit_lsn); } - { - std::lock_guard< std::mutex > lk(login_mu_); - login_done_ = true; - } - login_cv_.notify_one(); - LOGINFO("internal_login [id={}]: InternalLogin COMMITTED term={} client_token={} rs_commit_lsn {}", - boost::uuids::to_string(geo_.ep.id), m.term, m.client_token, m.rs_commit_lsn); -} - } // namespace craft \ No newline at end of file diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 12762f7..2890845 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -35,16 +35,18 @@ #include #include -#include // async result types -#include // result types +#include // result types #include "craft_peer.hpp" // the PEER plane: craft_peer + JournalSlot + lba_t (this model is its only implementer) #include "craft_replica.hpp" // the CLIENT plane: the craft_replica interface namespace craft { class MemTransport; // in-process network + cold path +class RaftReplica; // raft based replica server using sisl::ok; +template < typename T > +using result = sisl::result< T >; using status = sisl::status; // Per-partition CRAFT state, internal to a replica implementation. Authoritative in memory; a production replica @@ -104,23 +106,6 @@ struct replica_faults { std::chrono::milliseconds delay{0}; // injected network latency to this replica }; -// The server's per-volume geometry -- what LOGIN advertises; the replica's journal/index is built from it. -struct server_geometry { - uint64_t capacity; - uint32_t lba_size; - replica_endpoint ep; - uint32_t max_tx; -}; - -// for srv helo validation -struct session_info { - uint64_t term; - uint64_t client_token; -}; - -struct SyncRSCommitLSNMsg; -struct InternalLoginMsg; - // enable_shared_from_this: a write the transport timed out is delivered late, from the transport's timer // thread. That closure must hold a WEAK reference here (a strong one would cycle: replica -> net_ -> closure // -> replica), so the replica must be reachable as a shared_ptr. It always is; make_mem_replica_group is the @@ -129,17 +114,14 @@ struct InternalLoginMsg; // accident of the model: a real replica is exactly the thing that can answer both "serve this client's read" and // "hand a peer the journal slot it is Missing". A client-side transport proxy (CraftTcpReplica) implements only // craft_replica, because a client never asks a peer question. -class MemCraftReplica final : public craft_replica, - public craft_peer, - public std::enable_shared_from_this< MemCraftReplica > { +class MemCraftReplica : public craft_replica, + public craft_peer, + public std::enable_shared_from_this< MemCraftReplica > { public: // How many Missing dLSNs stats() lists individually. The count is always exact. static constexpr std::size_t k_missing_sample = 16; MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net); - explicit MemCraftReplica(server_geometry geo); - - ~MemCraftReplica(); // Snapshot this replica's state. Takes mu_ and deliberately does NOT consult net_: do_write() locks // the transport before mu_, so reading net_ under mu_ here would invert that order. Callers that want @@ -191,7 +173,7 @@ class MemCraftReplica final : public craft_replica, async_result< std::vector< JournalSlot > > fetch_data(std::vector< int64_t > lsns) override; async_status truncate(int64_t lsn) override; - peer_id_t id() const override { return geo_.ep.id; } // craft_replica + peer_id_t id() const override { return ep_.id; } // craft_replica // ── local-server surface: drive this replica directly, with an EXTERNAL transport (the TCP frontend, // craft_tcp_server, or any real network) as the wire. Each wraps a synchronous core WITHOUT a @@ -212,24 +194,14 @@ class MemCraftReplica final : public craft_replica, // The standalone (one-process = one-replica) resolution round: itself lacking a slot IS the quorum-lacks // evidence at N=1, so every hole <= upto is verdicted Empty and the frontier advances through it. result< resolution_result > srv_resolve(client_hdr hdr, int64_t upto) { return do_resolve_local(hdr, upto); } - result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, - uint64_t term) { - return apply_login(volume_id, client_token, term); - } + void srv_establish(uint64_t client_token, uint64_t term) { cold_apply_login(client_token, term); } void srv_end() { cold_apply_logout(); } lsn_pair srv_lsns() { return peek_lsns(); } - result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members); - result< lsn_pair > srv_get_rs_commit_lsn(uint64_t term, bool is_login) { - return do_get_rs_commit_lsn(term, is_login); - } - result< std::vector< JournalSlot > > srv_fetch_data(std::vector< int64_t > const& lsns) { return do_fetch(lsns); } - session_info srv_session_info(std::array< uint8_t, 16 > const& volume_id) const; - private: friend class MemTransport; // the cold path drives the cold_* / peek helpers below directly, and the IO // path (send_*) reads fault_snapshot() to decide deliverability / latency + friend class RaftReplica; // tcp server backed replica that uses the MemCraftReplica as the foundation // The IO path's view of this replica's faults: one acquire load, no lock. Hold the returned pointer for the // whole of one op so its checks (up / write_ok / delay) see a consistent snapshot (a superseded one is @@ -253,7 +225,6 @@ class MemCraftReplica final : public craft_replica, std::shared_ptr< std::vector< uint8_t > > buf; // one page at buf->data()+off std::size_t off{0}; }; - class RaftCommitWorker; // Synchronous cores: the SERVER. Each takes mu_. Deliverability, latency and payload ownership are the // transport's job (MemTransport::send_*), which is why nothing below consults net_ or copies bytes. @@ -263,11 +234,12 @@ class MemCraftReplica final : public craft_replica, result< read_result > do_read(client_hdr hdr, int64_t read_lsn, uint64_t addr, uint64_t len, sisl::sg_list dest); result< lsn_pair > do_keep_alive(client_hdr hdr); result< lsn_pair > do_lsns(); - result< lsn_pair > do_get_rs_commit_lsn(uint64_t term, bool is_login); status do_truncate(int64_t lsn); - result< std::vector< JournalSlot > > do_fetch(std::vector< int64_t > const& lsns); result< resolution_result > do_resolve_local(client_hdr hdr, int64_t upto); // N=1 resolution (srv seam) +protected: + result< std::vector< JournalSlot > > do_fetch(std::vector< int64_t > const& lsns); +private: // ── on-ring transport (a verb's non-null `q`) ── // ring_delay suspends the calling leg on a timeout/nop SQE placed on `q`; the reap loop's // complete_cqe_state resumes it. The ring is a parameter, not a member: each leg rides the ring its verb @@ -290,32 +262,22 @@ class MemCraftReplica final : public craft_replica, // cold-path hooks used by MemTransport (each takes mu_) lsn_pair peek_lsns(); void cold_apply_sync(int64_t rs_commit_lsn, uint64_t client_token); - void cold_apply_login(uint64_t client_token, uint64_t term); void cold_apply_logout(); - void cold_truncate_above(int64_t rs_commit_lsn); - - // real hooks using raft channel - void apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m); - result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); - - // Misc helpers - void init(); - MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); - std::vector< int64_t > get_missing_slots(int64_t watermark); - std::pair< std::vector< int64_t >, int64_t > - resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, uint64_t term); - result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, - uint64_t term); - void internal_login(InternalLoginMsg m); + // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. std::optional< MemJournalSlot > peek_slot(int64_t dlsn); // copy of the slot, or nullopt if absent - void cold_install_slot(int64_t dlsn, MemJournalSlot s); // fill a hole; never overwrites an entry + void cold_mark_empty(int64_t dlsn); // Empty verdict tombstone; overwrites held // data (reconciliation: Empty beats data) std::vector< int64_t > peek_empties(int64_t upto); // every is_empty dLSN <= upto +protected: + void cold_apply_login(uint64_t client_token, uint64_t term); + void cold_truncate_above(int64_t rs_commit_lsn); + void cold_install_slot(int64_t dlsn, MemJournalSlot s); // fill a hole; never overwrites an entry + // Test observability: how many reads this replica actually served. Lets a test witness read routing // (e.g. round-robin distribution across members). Not part of the CRAFT surface. std::atomic< std::size_t > reads_served_{0}; @@ -329,19 +291,15 @@ class MemCraftReplica final : public craft_replica, std::mutex fault_mu_; // serializes mutators only std::vector< std::unique_ptr< replica_faults const > > fault_retired_; // guarded by fault_mu_ - server_geometry geo_; +protected: + replica_endpoint ep_; + uint32_t page_size_; std::shared_ptr< MemTransport > net_; CraftPartitionState state_; std::map< int64_t, MemJournalSlot > journal_; // dLSN -> slot (out-of-order arrival tolerated) std::map< lba_t, IndexCell > index_; // applied prefix (<= commit_lsn); an absent LBA is a hole mutable std::mutex mu_; - - std::atomic< int64_t > rs_commit_lsn_{-1}; - std::mutex login_mu_; - std::condition_variable login_cv_; - bool login_done_{false}; - std::unique_ptr< RaftCommitWorker > commit_worker_; }; -} // namespace craft +} // namespace craft \ No newline at end of file diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 9499270..443b895 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -14,7 +14,7 @@ *********************************************************************************/ // This is the reference TCP server: it backs the server with the reference model -// (MemCraftReplica) and speaks its domain types (client_hdr, lsn_pair, io_extent, craft_error). It references +// (RaftReplica) and speaks its domain types (client_hdr, lsn_pair, io_extent, craft_error). It references // no homestore SYMBOL, so it still links without the engine (see test_craft_tcp). #include "net/tcp_server.hpp" @@ -24,7 +24,7 @@ #include // server-side r/w trace (base module; visible with -v trace / when a consumer inits logging) -#include "mem/replica.hpp" // the full MemCraftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) +#include "raft/raft_replica.hpp" // the full RaftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) #include // to_wire_status (the shared wire <-> craft_error bridge) #include "raft/raft_service.hpp" #include "replica_mgr.hpp" @@ -39,20 +39,20 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : max_tx_{geo.max_tx} { - LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(geo.ep.id), - server_config_file); +craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : geo_{std::move(geo)} { + auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; + LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). // start replica service and raft service if server_config_file is provided if (!server_config_file.empty()) { - replica_manager::instance()->start_replica_service(server_config_file, geo.ep.id); - raft_service::instance()->start_raft_service(geo.ep.id); - LOGINFO("craft_tcp_server: replica_manager + raft_service started [id={}]", boost::uuids::to_string(geo.ep.id)); + replica_manager::instance()->start_replica_service(server_config_file, ep.id); + raft_service::instance()->start_raft_service(ep.id); + LOGINFO("craft_tcp_server: replica_manager + raft_service started [id={}]", boost::uuids::to_string(ep.id)); } else { LOGINFO("craft_tcp_server: no server_config_file given -- running in standalone/cold-path mode [id={}]", - boost::uuids::to_string(geo.ep.id)); + boost::uuids::to_string(ep.id)); } - replica_ = std::make_shared< MemCraftReplica >(std::move(geo)); + replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx); } craft_tcp_server::~craft_tcp_server() = default; @@ -75,12 +75,12 @@ void craft_tcp_server::log_stats() const { void craft_tcp_server::serve(craft_conn conn) { LOGDEBUG("craft_srv: connection accepted, serving"); for (;;) { - auto msg = conn.recv_message(max_tx_); + auto msg = conn.recv_message(geo_.max_tx); if (!msg) { LOGDEBUG("craft_srv: connection closed (peer closed, or framing error)"); return; // peer closed, or a framing error -- done with this connection } - auto parsed = wire::parse_message(*msg, max_tx_); + auto parsed = wire::parse_message(*msg, geo_.max_tx); if (!parsed) { LOGWARN("craft_srv: malformed message, resetting connection"); return; @@ -150,11 +150,11 @@ void craft_tcp_server::on_login(craft_conn& conn, wire::message const& req) { return; } wire::login_rsp rsp{}; - rsp.term = srv_rsp.term; + rsp.term = session_term_; rsp.dlsn = srv_rsp.dLSN; - rsp.capacity = srv_rsp.capacity; - rsp.lba_size = srv_rsp.lba_size; - rsp.max_tx = srv_rsp.max_tx; + rsp.capacity = geo_.capacity; + rsp.lba_size = geo_.lba_size; + rsp.max_tx = geo_.max_tx; rsp.member_count = static_cast< uint32_t >(srv_rsp.members.size()); std::vector< uint8_t > body; diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index a880381..c1a44fd 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -33,12 +33,20 @@ #include namespace craft { -class MemCraftReplica; // the server's state backing (pimpl; included only in craft_tcp_server.cpp) -struct server_geometry; +struct replica_endpoint; +class RaftReplica; // the server's state backing (pimpl; included only in craft_tcp_server.cpp) } // namespace craft namespace craft::net { +// The server's per-volume geometry -- what LOGIN advertises; the replica's journal/index is built from it. +struct server_geometry { + uint64_t capacity = 0; + uint32_t lba_size = 0; + uint32_t max_tx = 0; + wire::member member; +}; + class craft_tcp_server { public: explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}); @@ -57,8 +65,8 @@ class craft_tcp_server { void log_stats() const; private: - uint32_t max_tx_; - std::shared_ptr< MemCraftReplica > replica_; // the real state; driven via its srv_* local-server seam + server_geometry geo_; + std::shared_ptr< RaftReplica > replica_; // the real state; driven via its srv_* local-server seam uint64_t next_term_ = 0; // monotonic term source; a fresh LOGIN takes ++next_term_ uint64_t session_term_ = 0; // the current session's term, stamped on every IO bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp new file mode 100644 index 0000000..88e1cb1 --- /dev/null +++ b/src/raft/raft_replica.cpp @@ -0,0 +1,471 @@ +/********************************************************************************* + * Modifications Copyright 2026 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ + +#include "raft/raft_replica.hpp" +#include "raft/raft_service.hpp" // for raft channel +#include "replica_mgr.hpp" +#include "raft/raft_state_machine.hpp" // for raft message payload types +#include "helper.hpp" +#include "craft/types.hpp" + +#include +#include +#include +#include +#include +#include +#include + + +#include + +namespace craft { + +namespace { + +auto fail(craft_error e) { return std::unexpected(make_error_condition(e)); } + +std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { + auto b = std::make_shared< std::vector< uint8_t > >(); + b->reserve(s.size); + for (auto const& io : s.iovs) { + auto const* p = static_cast< uint8_t const* >(io.iov_base); + b->insert(b->end(), p, p + io.iov_len); + } + return b; +} + +constexpr auto LoginWaitTime = std::chrono::seconds(2); + +} // namespace + +// background worker for raft commit to run replica's business logic +class RaftReplica::RaftCommitWorker { + std::queue< std::move_only_function< void() > > queue_; + std::mutex mtx_; + std::condition_variable_any cv_; + std::jthread worker_; + +public: + RaftCommitWorker() : worker_([this](std::stop_token st) { run(st); }) {} + + void push_task(std::move_only_function< void() > work) { + { + std::lock_guard lk(mtx_); + queue_.push(std::move(work)); + } + cv_.notify_one(); + } + +private: + void run(std::stop_token st) { + while (!st.stop_requested()) { + std::unique_lock lk(mtx_); + cv_.wait(lk, st, [this] { return !queue_.empty(); }); // wakes on stop too + if (st.stop_requested() && queue_.empty()) return; + + auto task = std::move(queue_.front()); + queue_.pop(); + lk.unlock(); + if (task) { task(); } + } + } +}; + +RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx) : MemCraftReplica{std::move(ep), page_size, nullptr}, + max_tx_{max_tx} { + // register raft callbacks + // do not block commit thread, offload the business logic to the commit_worker + auto raft_inst = raft_service::instance(); + if (!raft_inst->is_raft_enabled()) { return; } + auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& vol_uuid_str) { + auto const vol_uuid = boost::uuids::string_generator()(vol_uuid_str); + auto const op_val = j.at("op").get< int >(); + switch (static_cast< Operation >(op_val)) { + case Operation::SyncRSCommitLSN: { + SyncRSCommitLSNMsg m; + try { + m = j.get< SyncRSCommitLSNMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); + return; + } + LOGDEBUG("commit[{}][vol={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, + boost::uuids::to_string(vol_uuid), m.rs_commit_lsn, m.empty_slots.size()); + commit_worker_->push_task( + [this, vol_uuid, m = std::move(m)]() mutable { apply_sync(vol_uuid, std::move(m)); }); + break; + } + case Operation::InternalLogin: { + InternalLoginMsg m; + try { + m = j.get< InternalLoginMsg >(); + } catch (nlohmann::json::exception const& e) { + LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); + return; + } + LOGDEBUG("commit[{}][vol={}]: applying InternalLogin term={} client_token={}", log_idx, + boost::uuids::to_string(vol_uuid), m.term, m.client_token); + commit_worker_->push_task([this, vol_uuid, m = std::move(m)]() mutable { internal_login(m); }); + break; + } + default: + LOGERROR("commit[{}]: unknown op={}", log_idx, op_val); + break; + } + }; + + auto group_create_cb = [this](boost::uuids::uuid const& group_id) { + + }; + raft_inst->add_commit_cb(std::move(commit_cb)); + + // start background commit offload worker + commit_worker_ = std::make_unique< RaftReplica::RaftCommitWorker >(); + LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), + page_size_); +} + +RaftReplica::~RaftReplica() = default; + +result< lsn_pair > RaftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { + // TODO implement quiesce barrier + std::lock_guard< std::mutex > g{mu_}; + return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; +} + +result< std::vector< JournalSlot > > RaftReplica::srv_fetch_data(std::vector< int64_t > const& lsns) { + return do_fetch(lsns); +} + +// ── peer comm hooks (driven by raft) ── + +MemCraftReplica::MemJournalSlot RaftReplica::to_mem_journal_slot(JournalSlot const& j, uint64_t term) { + std::shared_ptr< std::vector< uint8_t > > bytes; + if (j.owned_data) { + bytes = j.owned_data; + } else if (j.data.size > 0) { + bytes = take_payload(j.data); + } + return MemCraftReplica::MemJournalSlot{ + .term = term, + .lba = j.lba, + .len = j.len, + .all_zeros = j.all_zeros, + .is_empty = j.is_empty, + .bytes = std::move(bytes), + }; +} + +std::vector< int64_t > RaftReplica::get_missing_slots(int64_t watermark) { + std::lock_guard< std::mutex > g{mu_}; + std::vector< int64_t > missing; + int64_t expect = state_.commit_lsn + 1; + auto it = journal_.lower_bound(expect); // first present entry >= expect + for (; it != journal_.end() && it->first <= watermark; ++it) { + for (; expect < it->first; ++expect) + missing.push_back(expect); // gap before this entry + expect = it->first + 1; + } + for (; expect <= watermark; ++expect) + missing.push_back(expect); // trailing gap after the last present entry + LOGDEBUG("get_missing_slots [id={}] watermark={} missing_count={}", boost::uuids::to_string(ep_.id), watermark, + missing.size()); + return missing; +} + +std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, + int64_t watermark, + uint64_t client_token, uint64_t term) { + auto const peers = replica_manager::instance()->get_volume(vol_uuid); + auto const missing_lsns = get_missing_slots(watermark); + LOGDEBUG("resolve_and_apply[vol={}] watermark={} missing={} peers={}", boost::uuids::to_string(vol_uuid), watermark, + missing_lsns.size(), peers.size()); + if (missing_lsns.empty()) { return {{}, -1}; } + + // Brute force, no optimizations for now + // Step 1: ask every peer for the full missing list, collect ALL responses first. + std::vector< std::vector< JournalSlot > > all_responses; + for (auto const& peer : peers) { + if (peer.id == ep_.id) { continue; } + if (auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing_lsns)); r) { + all_responses.emplace_back(std::move(r.value())); + } else { + LOGWARN("resolve_and_apply[vol={}]: fetch_data to peer {} failed/unreachable, error: {}", + boost::uuids::to_string(vol_uuid), boost::uuids::to_string(peer.id), r.error().message()); + } + } + + // Step 2: for each requested lsn, look across every response and decide its fate. + int64_t stalled_lsn{-1}; + std::vector< int64_t > empty_slots; + for (auto lsn : missing_lsns) { + uint32_t lacks_count = 1; + bool is_data{false}; + + for (auto const& resp : all_responses) { + if (is_data) break; + auto const it = std::ranges::find_if(resp, [&](auto const& s) { return s.lsn == lsn; }); + if (it == resp.end()) { + // Omitted from a responding peer's list == "not-present-here": positive + // lacks-evidence per the FetchData contract. + ++lacks_count; + } else if (it->is_empty) { + // Peer already holds a prior positive Empty verdict: also lacks-evidence. + ++lacks_count; + } else { + is_data = true; + cold_install_slot(lsn, to_mem_journal_slot(*it, term)); + } + } + + if (lacks_count >= peers.size() / 2 + 1) { + empty_slots.push_back(lsn); + } else if (!is_data) { + // unresolved lsn + stalled_lsn = (stalled_lsn == -1) ? lsn : std::min(lsn, stalled_lsn); + } + } + if (stalled_lsn != -1) { + LOGWARN("resolve_and_apply[vol={}]: could not resolve past lsn={} (quorum-lacks evidence insufficient)", + boost::uuids::to_string(vol_uuid), stalled_lsn); + } else { + LOGDEBUG("resolve_and_apply[vol={}]: fully resolved up to watermark={}, empty_slots={}", + boost::uuids::to_string(vol_uuid), watermark, empty_slots.size()); + } + return {empty_slots, stalled_lsn}; +} + +result< void > RaftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, + uint64_t client_token, uint64_t term) { + auto const [empty_slots, stalled_lsn] = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); + if (stalled_lsn != -1) { + // leader could not resolve all the missing lsns + LOGERROR("sync_rs_commit_lsn[vol={}]: leader could not resolve all missing lsns, stalled at {}", + boost::uuids::to_string(vol_uuid), stalled_lsn); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + if (auto const r = raft_service::instance()->propose(vol_uuid, + SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, + .client_token = client_token, + .empty_slots = std::move(empty_slots)}); + !r) { + // TODO: any cleanup required? + LOGERROR("sync_rs_commit_lsn[vol={}]: propose(SyncRSCommitLSN={}) failed: {}", + boost::uuids::to_string(vol_uuid), rs_commit_lsn, r.error().message()); + return std::unexpected(r.error()); + } + LOGINFO("sync_rs_commit_lsn[vol={}]: proposed rs_commit_lsn={} OK", boost::uuids::to_string(vol_uuid), + rs_commit_lsn); + return {}; +} + +result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, + uint64_t term) { + auto raft_service_inst = raft_service::instance(); + // A note on dlsn: The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one + // to use -- the client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, + // which skipped slot 0 on a fresh cluster: every replica was then permanently Missing dLSN 0, + // apply_up_to() stalled there forever, and commit_lsn pinned at -1 -- so no journal reclaimed and every read + // walked the whole tail. See wire.hpp. + + if (!raft_service_inst->is_raft_enabled()) { + // return cold path if raft service has not started + std::lock_guard< std::mutex > g{mu_}; + state_.term = term; + state_.client_token = client_token; + LOGINFO("apply_login [id={}]: raft disabled, cold-path login OK, term={} token={}", + boost::uuids::to_string(ep_.id), term, client_token); + return LoginResult{.members = {ep_}, + .dLSN = state_.last_append_lsn}; + } + // Phase 1: collect replica LSN state (non-RAFT broadcast) + // 1.1: accepted by leader only. + // TODO: what happens if the leader changes before the login is complete? + auto vol_uuid = craft::to_uuid(volume_id); + LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(vol_uuid), client_token, + term); + if (!raft_service_inst->is_leader(vol_uuid)) { + LOGERROR("current replica not a raft leader"); + return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; + } + + // 1.2 collect replica LSN state (non-RAFT broadcast) + std::vector< lsn_pair > peer_resp; + uint64_t current_term; + { + std::lock_guard< std::mutex > g{mu_}; + current_term = state_.term; + peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); + } + + auto const members = replica_manager::instance()->get_volume(vol_uuid); + LOGDEBUG("apply_login[vol={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(vol_uuid), + members.size()); + for (auto const& m : members) { + if (m.id == ep_.id) { continue; } + if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(current_term, true /* is_login */)); r) { + LOGDEBUG("apply_login[vol={}]: peer {} reported commit_lsn={} last_append_lsn={}", + boost::uuids::to_string(vol_uuid), boost::uuids::to_string(m.id), r->commit_lsn, + r->last_append_lsn); + peer_resp.emplace_back(r.value()); + } else { + LOGWARN("apply_login[vol={}]: peer {} did not respond to GetRSCommitLSN", boost::uuids::to_string(vol_uuid), + boost::uuids::to_string(m.id)); + } + } + // compute watermark as max(quorum.last_append) + if (peer_resp.size() <= members.size() / 2) { + LOGERROR("apply_login[vol={}]: quorum not reached ({} of {} responded)", boost::uuids::to_string(vol_uuid), + peer_resp.size(), members.size()); + return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); + } + auto const rs_commit_lsn = std::ranges::max_element(peer_resp, {}, &lsn_pair::last_append_lsn)->last_append_lsn; + LOGINFO("apply_login[vol={}]: computed rs_commit_lsn={} from {} responder(s)", boost::uuids::to_string(vol_uuid), + rs_commit_lsn, peer_resp.size()); + + // Phase 1b: Leader behind - resolve all the missing lsns and + // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) + if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, current_term); !r) { + LOGERROR("apply_login[vol={}]: sync_rs_commit_lsn failed: {}", boost::uuids::to_string(vol_uuid), + r.error().message()); + return std::unexpected(r.error()); + } + + // Phase 3: InternalLogin(token, term) via RAFT + { + std::lock_guard< std::mutex > lk(login_mu_); + login_done_ = false; + } + if (auto const r = raft_service_inst->propose( + vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); + !r) { + // TODO: any cleanup required? + LOGERROR("apply_login[vol={}]: propose(InternalLogin term={}) failed: {}", boost::uuids::to_string(vol_uuid), + term, r.error().message()); + return std::unexpected(r.error()); + } + LOGDEBUG("apply_login[vol={}]: InternalLogin(term={}) proposed, waiting for commit", + boost::uuids::to_string(vol_uuid), term); + + // Phase 4: truncate above rs_commit_lsn + // This happens in the internal login commit. Wait until that happens. + { + std::unique_lock< std::mutex > lk(login_mu_); + login_cv_.wait_for(lk, LoginWaitTime, [&] { return login_done_; }); + if (!login_done_) { + LOGERROR("apply_login[vol={}]: timed out waiting for InternalLogin(term={}) commit callback", + boost::uuids::to_string(vol_uuid), term); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + } + + std::vector< replica_endpoint > replicas; + for (auto const& m : members) { + replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); + } + LOGINFO("apply_login[vol={}]: LOGIN SUCCESS term={} dLSN={} members={}", boost::uuids::to_string(vol_uuid), term, + rs_commit_lsn, replicas.size()); + return LoginResult{.members = replicas, + .dLSN = rs_commit_lsn}; +} + +// create peer raft group and add members to it. +result< void > RaftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< replica_endpoint > const& members) { + auto const vol_uuid = craft::to_uuid(volume_id); + auto const& repl_mgr = replica_manager::instance(); + // return success if the volume exists + if (auto const vol = repl_mgr->get_volume(vol_uuid); !vol.empty()) { + LOGINFO("Volume {} exists! Returning ok", boost::uuids::to_string(vol_uuid)); + return {}; + } + LOGINFO("srv_create_volume[vol={}]: creating with {} member(s)", boost::uuids::to_string(vol_uuid), members.size()); + + auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members); + if (r) { + repl_mgr->register_volume(vol_uuid, members); + LOGINFO("srv_create_volume[vol={}]: SUCCESS", boost::uuids::to_string(vol_uuid)); + } else { + LOGERROR("srv_create_volume[vol={}]: FAILED: {}", boost::uuids::to_string(vol_uuid), r.error().message()); + } + return r; +} + +// Follower-side catch-up on SyncRSCommitLSN apply. Verdicts are already decided by the leader (empty_slots) +// -- this never decides Empty itself, only obeys the verdict list or fetches real data. +void RaftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m) { + // Verdicts first -- permanent no-ops, no fetch needed. + for (auto lsn : m.empty_slots) + cold_mark_empty(lsn); + + uint64_t term; + { + std::lock_guard< std::mutex > g{mu_}; + term = state_.term; + } + + auto missing = get_missing_slots(m.rs_commit_lsn); + auto const peers = replica_manager::instance()->get_volume(vol_uuid); + for (auto const& peer : peers) { + if (missing.empty()) break; + if (peer.id == ep_.id) continue; // don't ask self + + auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing)); + if (!r) continue; // unreachable, try next peer + + std::erase_if(missing, [&](int64_t lsn) { + auto const it = std::ranges::find_if(*r, [&](auto const& s) { return s.lsn == lsn; }); + if (it == r->end()) return false; // this peer doesn't have it either + if (it->is_empty) { + cold_mark_empty(lsn); // a prior verdict this peer already knows about + } else { + cold_install_slot(lsn, to_mem_journal_slot(*it, term)); + } + return true; + }); + } + + if (!missing.empty()) { + LOGERROR("apply_sync[vol={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " + "stall until the next SyncRSCommitLSN round -- first missing={}", + boost::uuids::to_string(vol_uuid), missing.size(), m.rs_commit_lsn, missing.front()); + } + + std::lock_guard< std::mutex > g{mu_}; + apply_up_to(m.rs_commit_lsn); + rs_commit_lsn_.store(m.rs_commit_lsn, std::memory_order_relaxed); + LOGDEBUG("apply_sync[vol={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(vol_uuid), + state_.commit_lsn, m.rs_commit_lsn); +} + +session_info RaftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { + std::lock_guard< std::mutex > g{mu_}; + return {state_.term, state_.client_token}; +} + +void RaftReplica::internal_login(InternalLoginMsg m) { + cold_apply_login(m.client_token, m.term); + if (m.rs_commit_lsn >= 0) { cold_truncate_above(m.rs_commit_lsn); } + { + std::lock_guard< std::mutex > lk(login_mu_); + login_done_ = true; + } + login_cv_.notify_one(); + LOGINFO("internal_login [id={}]: InternalLogin COMMITTED term={} client_token={} rs_commit_lsn {}", + boost::uuids::to_string(ep_.id), m.term, m.client_token, m.rs_commit_lsn); +} + +} // namespace craft \ No newline at end of file diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp new file mode 100644 index 0000000..f96109f --- /dev/null +++ b/src/raft/raft_replica.hpp @@ -0,0 +1,82 @@ +/********************************************************************************* + * Modifications Copyright 2026 eBay Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + *********************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mem/replica.hpp" + +namespace craft { + +// for srv helo validation +struct session_info { + uint64_t term; + uint64_t client_token; +}; + +struct SyncRSCommitLSNMsg; +struct InternalLoginMsg; + +class RaftReplica final : public MemCraftReplica { +public: + + RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx); + + ~RaftReplica(); + + result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, + uint64_t term) { + return apply_login(volume_id, client_token, term); + } + + result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, + std::vector< replica_endpoint > const& members); + result< lsn_pair > srv_get_rs_commit_lsn(uint64_t term, bool is_login) { + return do_get_rs_commit_lsn(term, is_login); + } + result< std::vector< JournalSlot > > srv_fetch_data(std::vector< int64_t > const& lsns); + session_info srv_session_info(std::array< uint8_t, 16 > const& volume_id) const; + +private: + result< lsn_pair > do_get_rs_commit_lsn(uint64_t term, bool is_login); + void apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m); + result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); + MemCraftReplica::MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); + std::vector< int64_t > get_missing_slots(int64_t watermark); + std::pair< std::vector< int64_t >, int64_t > + resolve_and_apply(boost::uuids::uuid const& vol_uuid, int64_t watermark, uint64_t client_token, uint64_t term); + result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, + uint64_t term); + void internal_login(InternalLoginMsg m); + + uint32_t max_tx_; + std::atomic< int64_t > rs_commit_lsn_{-1}; + std::mutex login_mu_; + std::condition_variable login_cv_; + bool login_done_{false}; + + class RaftCommitWorker; + std::unique_ptr< RaftCommitWorker > commit_worker_; +}; + +} // namespace craft diff --git a/test/test_tcp.cpp b/test/test_tcp.cpp index f679cf8..fae4a61 100644 --- a/test/test_tcp.cpp +++ b/test/test_tcp.cpp @@ -31,7 +31,6 @@ #include #include "net/wire_client.hpp" #include "net/tcp_server.hpp" -#include "mem/replica.hpp" using namespace craft::net; namespace wire = craft::wire; @@ -40,13 +39,16 @@ namespace { constexpr uint32_t k_lba = 4096; -craft::server_geometry make_geo() { - boost::uuids::uuid ep_id{}; - ep_id.data[0] = 0x01; - return craft::server_geometry{.capacity = uint64_t{1} << 30, - .lba_size = k_lba, - .ep = {.id = ep_id, .addr = "127.0.0.1:0"}, - .max_tx = 512 * 1024}; +craft::net::server_geometry make_geo() { + craft::wire::member self{}; + self.addr = "127.0.0.1:0"; + self.id[0] = 0x01; + return craft::net::server_geometry{ + .capacity = uint64_t{1} << 30, + .lba_size = k_lba, + .max_tx = 512 * 1024, + .member = std::move(self), + }; } // A per-byte-nonzero pattern of `n` bytes -- nonzero so no 4 KiB page collapses to a hole on the read path diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 79c9634..91842e1 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -39,7 +39,6 @@ #include #include "net/tcp_server.hpp" -#include "mem/replica.hpp" #include // A 0 default means "unset" -> resolved in code (capacity to 1 GiB, max_tx to the single-sourced wire default), so @@ -112,11 +111,11 @@ int main(int argc, char** argv) { // Advertise this one replica in login_rsp. The id is cosmetic here (the client routes by index, and HELO // fences by term, not id) -- a fresh random id is fine; the client's --craft-tcp supplies its own members. - auto geo = - craft::server_geometry{.capacity = capacity, - .lba_size = lba_size, - .ep = craft::replica_endpoint{.id = id, .addr = fmt::format("127.0.0.1:{}", port)}, - .max_tx = max_tx}; + craft::wire::member self{}; + std::copy(id.begin(), id.end(), self.id.begin()); + self.addr = fmt::format("127.0.0.1:{}", port); + auto geo = craft::net::server_geometry{ + .capacity = capacity, .lba_size = lba_size, .max_tx = max_tx, .member = std::move(self)}; craft::net::craft_tcp_server server{std::move(geo), server_config_file}; // sigaction WITHOUT SA_RESTART: glibc's signal() sets SA_RESTART, which auto-restarts the blocking accept() From 3d1ef6ee8ff7f3d423bc6208dbc0fa5e8cc35792 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Fri, 14 Aug 2026 09:11:15 -0700 Subject: [PATCH 14/24] Add a static registry class to store type erased key value pairs. This can be used during restart --- CMakeLists.txt | 17 ++- src/raft/raft_replica.cpp | 241 +++++++++++++++++++++++++------------- src/raft/raft_replica.hpp | 19 +-- src/raft/raft_service.cpp | 9 +- src/raft/raft_service.hpp | 4 +- src/registry_mgr.cpp | 10 ++ src/registry_mgr.hpp | 37 ++++++ src/replica_mgr.hpp | 7 -- 8 files changed, 230 insertions(+), 114 deletions(-) create mode 100644 src/registry_mgr.cpp create mode 100644 src/registry_mgr.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e53271..f2fc381 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,14 +68,11 @@ target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/s target_link_libraries(craft_reference PUBLIC craft_client) target_compile_features(craft_reference PUBLIC cxx_std_23) -# ── craft_replica_mgr: the internal replica manager (testing only) ── -add_library(craft_replica_mgr STATIC - src/replica_mgr.cpp - src/net/tcp_peer.cpp) -target_include_directories(craft_replica_mgr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) -target_include_directories(craft_replica_mgr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(craft_replica_mgr PUBLIC craft_client) -target_compile_features(craft_replica_mgr PUBLIC cxx_std_23) +add_library(registry_mgr STATIC + src/registry_mgr.cpp) +target_include_directories(registry_mgr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(registry_mgr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_compile_features(registry_mgr PUBLIC cxx_std_23) add_library(raft_service STATIC src/raft/raft_service.cpp @@ -83,7 +80,7 @@ add_library(raft_service STATIC src/raft/raft_state_manager.cpp) target_include_directories(raft_service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(raft_service PUBLIC nuraft_mesg::proto craft_replica_mgr) +target_link_libraries(raft_service PUBLIC nuraft_mesg::proto registry_mgr) target_compile_features(raft_service PUBLIC cxx_std_23) add_library(raft_tcp_server STATIC @@ -91,7 +88,7 @@ add_library(raft_tcp_server STATIC src/net/tcp_server.cpp) target_include_directories(raft_tcp_server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_tcp_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(raft_tcp_server PUBLIC craft_reference craft_replica_mgr raft_service) +target_link_libraries(raft_tcp_server PUBLIC craft_reference raft_service) target_compile_features(raft_tcp_server PUBLIC cxx_std_23) # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 88e1cb1..7b4ccb2 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -15,7 +15,7 @@ #include "raft/raft_replica.hpp" #include "raft/raft_service.hpp" // for raft channel -#include "replica_mgr.hpp" +#include "registry_mgr.hpp" #include "raft/raft_state_machine.hpp" // for raft message payload types #include "helper.hpp" #include "craft/types.hpp" @@ -49,6 +49,28 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { constexpr auto LoginWaitTime = std::chrono::seconds(2); +struct replica_info { + boost::uuids::uuid id{}; + std::string host; + uint16_t raft_port{0}; + uint16_t tcp_port{0}; + std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; +}; + +using partition_peers_list_t = std::vector< boost::uuids::uuid >; + +std::string partition_peers_list_key(boost::uuids::uuid const& partition_id){return fmt::format("partition_{}", )} + +partition_peers_list_t peer_list(boost::uuids::uuid const& partition_id) { + partition_peers_list_t peers; + if (auto peers_ptr = + registry_manager::instance()->get< partition_peers_list_t >(partition_peers_list_key(partition_id)); + peers_ptr) { + peers = *peers_ptr; + } + return peers; +} + } // namespace // background worker for raft commit to run replica's business logic @@ -84,14 +106,52 @@ class RaftReplica::RaftCommitWorker { } }; -RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx) : MemCraftReplica{std::move(ep), page_size, nullptr}, - max_tx_{max_tx} { - // register raft callbacks - // do not block commit thread, offload the business logic to the commit_worker +std::string replica_info_key() const { return fmt::format("replica_info_{}", boost::uuids::to_string(ep_.id)); } + +void RaftReplica::replics_init(std::string const& replica_config_path) { + auto registry = registry_manager::instance(); + if (auto const rinfo = registry->get< replica_info >(replica_info_key())) { + // recovery from registry + return; + } + + std::ifstream istrm(replica_config_path, std::ios::binary); + if (!istrm.is_open()) { + LOGERROR("Could not open {}", replica_config_path); + return; + } + + nlohmann::json j; + try { + istrm >> j; + } catch (nlohmann::json::parse_error const& e) { + LOGERROR("Could not parse {}: {}", replica_config_path, e.what()); + return; + } + + for (auto const& m : j.at("members")) { + auto const id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); + auto r = std::make_shared< replica_info >( + id, + replica_info{ + .id = id, + .host = m.at("host").get< std::string >(), + .raft_port = m.at("raft_port").get< uint16_t >(), + .tcp_port = m.at("tcp_port").get< uint16_t >(), + .peer_client = (id == ep_.id) + ? nullptr + : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), + m.at("tcp_port").get< uint16_t >(), id), + }); + registry->put(replica_info_key(), std::move(r)); + } +} + +void RaftReplica::raft_init() { auto raft_inst = raft_service::instance(); if (!raft_inst->is_raft_enabled()) { return; } - auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& vol_uuid_str) { - auto const vol_uuid = boost::uuids::string_generator()(vol_uuid_str); + auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& partition_uuid_str) { + auto const partition_uuid = boost::uuids::string_generator()(partition_uuid_str); auto const op_val = j.at("op").get< int >(); switch (static_cast< Operation >(op_val)) { case Operation::SyncRSCommitLSN: { @@ -102,10 +162,10 @@ RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_t LOGERROR("commit[{}]: malformed SyncRSCommitLSN: {}", log_idx, e.what()); return; } - LOGDEBUG("commit[{}][vol={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, - boost::uuids::to_string(vol_uuid), m.rs_commit_lsn, m.empty_slots.size()); + LOGDEBUG("commit[{}][partition={}]: applying SyncRSCommitLSN rs_commit_lsn={} empty_slots={}", log_idx, + boost::uuids::to_string(partition_uuid), m.rs_commit_lsn, m.empty_slots.size()); commit_worker_->push_task( - [this, vol_uuid, m = std::move(m)]() mutable { apply_sync(vol_uuid, std::move(m)); }); + [this, partition_uuid, m = std::move(m)]() mutable { apply_sync(partition_uuid, std::move(m)); }); break; } case Operation::InternalLogin: { @@ -116,9 +176,9 @@ RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_t LOGERROR("commit[{}]: malformed InternalLogin: {}", log_idx, e.what()); return; } - LOGDEBUG("commit[{}][vol={}]: applying InternalLogin term={} client_token={}", log_idx, - boost::uuids::to_string(vol_uuid), m.term, m.client_token); - commit_worker_->push_task([this, vol_uuid, m = std::move(m)]() mutable { internal_login(m); }); + LOGDEBUG("commit[{}][partition={}]: applying InternalLogin term={} client_token={}", log_idx, + boost::uuids::to_string(partition_uuid), m.term, m.client_token); + commit_worker_->push_task([this, partition_uuid, m = std::move(m)]() mutable { internal_login(m); }); break; } default: @@ -138,6 +198,14 @@ RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_t page_size_); } +RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, + std::string const& replica_config_path) : + MemCraftReplica{std::move(ep), page_size, nullptr}, + max_tx_{max_tx} { + replica_init(replica_config_path); + raft_init(); +} + RaftReplica::~RaftReplica() = default; result< lsn_pair > RaftReplica::do_get_rs_commit_lsn(uint64_t term, bool is_login) { @@ -186,13 +254,18 @@ std::vector< int64_t > RaftReplica::get_missing_slots(int64_t watermark) { return missing; } -std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boost::uuids::uuid const& vol_uuid, - int64_t watermark, - uint64_t client_token, uint64_t term) { - auto const peers = replica_manager::instance()->get_volume(vol_uuid); +std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boost::uuids::uuid const& partition_uuid, + int64_t watermark, uint64_t client_token, + uint64_t term) { + auto const peers = peer_list(partition_uuid); + if (peers.empty()) { + LOGERROR("resolve_and_apply[partition={}]: no peers found in registry", + boost::uuids::to_string(partition_uuid)); + return {{}, watermark}; // every lsn is stalled + } auto const missing_lsns = get_missing_slots(watermark); - LOGDEBUG("resolve_and_apply[vol={}] watermark={} missing={} peers={}", boost::uuids::to_string(vol_uuid), watermark, - missing_lsns.size(), peers.size()); + LOGDEBUG("resolve_and_apply[partition={}] watermark={} missing={} peers={}", + boost::uuids::to_string(partition_uuid), watermark, missing_lsns.size(), peers.size()); if (missing_lsns.empty()) { return {{}, -1}; } // Brute force, no optimizations for now @@ -203,8 +276,8 @@ std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boos if (auto r = sisl::async::sync_get(peer.peer_client->fetch_data(missing_lsns)); r) { all_responses.emplace_back(std::move(r.value())); } else { - LOGWARN("resolve_and_apply[vol={}]: fetch_data to peer {} failed/unreachable, error: {}", - boost::uuids::to_string(vol_uuid), boost::uuids::to_string(peer.id), r.error().message()); + LOGWARN("resolve_and_apply[partition={}]: fetch_data to peer {} failed/unreachable, error: {}", + boost::uuids::to_string(partition_uuid), boost::uuids::to_string(peer.id), r.error().message()); } } @@ -239,41 +312,41 @@ std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boos } } if (stalled_lsn != -1) { - LOGWARN("resolve_and_apply[vol={}]: could not resolve past lsn={} (quorum-lacks evidence insufficient)", - boost::uuids::to_string(vol_uuid), stalled_lsn); + LOGWARN("resolve_and_apply[partition={}]: could not resolve past lsn={} (quorum-lacks evidence insufficient)", + boost::uuids::to_string(partition_uuid), stalled_lsn); } else { - LOGDEBUG("resolve_and_apply[vol={}]: fully resolved up to watermark={}, empty_slots={}", - boost::uuids::to_string(vol_uuid), watermark, empty_slots.size()); + LOGDEBUG("resolve_and_apply[partition={}]: fully resolved up to watermark={}, empty_slots={}", + boost::uuids::to_string(partition_uuid), watermark, empty_slots.size()); } return {empty_slots, stalled_lsn}; } -result< void > RaftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, - uint64_t client_token, uint64_t term) { - auto const [empty_slots, stalled_lsn] = resolve_and_apply(vol_uuid, rs_commit_lsn, client_token, term); +result< void > RaftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& partition_uuid, int64_t rs_commit_lsn, + uint64_t client_token, uint64_t term) { + auto const [empty_slots, stalled_lsn] = resolve_and_apply(partition_uuid, rs_commit_lsn, client_token, term); if (stalled_lsn != -1) { // leader could not resolve all the missing lsns - LOGERROR("sync_rs_commit_lsn[vol={}]: leader could not resolve all missing lsns, stalled at {}", - boost::uuids::to_string(vol_uuid), stalled_lsn); + LOGERROR("sync_rs_commit_lsn[partition={}]: leader could not resolve all missing lsns, stalled at {}", + boost::uuids::to_string(partition_uuid), stalled_lsn); return std::unexpected(make_error_condition(craft_error::INTERNAL)); } - if (auto const r = raft_service::instance()->propose(vol_uuid, + if (auto const r = raft_service::instance()->propose(partition_uuid, SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, .client_token = client_token, .empty_slots = std::move(empty_slots)}); !r) { // TODO: any cleanup required? - LOGERROR("sync_rs_commit_lsn[vol={}]: propose(SyncRSCommitLSN={}) failed: {}", - boost::uuids::to_string(vol_uuid), rs_commit_lsn, r.error().message()); + LOGERROR("sync_rs_commit_lsn[partition={}]: propose(SyncRSCommitLSN={}) failed: {}", + boost::uuids::to_string(partition_uuid), rs_commit_lsn, r.error().message()); return std::unexpected(r.error()); } - LOGINFO("sync_rs_commit_lsn[vol={}]: proposed rs_commit_lsn={} OK", boost::uuids::to_string(vol_uuid), + LOGINFO("sync_rs_commit_lsn[partition={}]: proposed rs_commit_lsn={} OK", boost::uuids::to_string(partition_uuid), rs_commit_lsn); return {}; } -result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, - uint64_t term) { +result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& partition_id, uint64_t client_token, + uint64_t term) { auto raft_service_inst = raft_service::instance(); // A note on dlsn: The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one // to use -- the client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, @@ -294,12 +367,12 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& // Phase 1: collect replica LSN state (non-RAFT broadcast) // 1.1: accepted by leader only. // TODO: what happens if the leader changes before the login is complete? - auto vol_uuid = craft::to_uuid(volume_id); - LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(vol_uuid), client_token, + auto partition_uuid = craft::to_uuid(partition_id); + LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(partition_uuid), client_token, term); - if (!raft_service_inst->is_leader(vol_uuid)) { + if (!raft_service_inst->is_leader(partition_uuid)) { LOGERROR("current replica not a raft leader"); - return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(vol_uuid)}; + return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(partition_uuid)}; } // 1.2 collect replica LSN state (non-RAFT broadcast) @@ -311,35 +384,35 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); } - auto const members = replica_manager::instance()->get_volume(vol_uuid); - LOGDEBUG("apply_login[vol={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(vol_uuid), - members.size()); + auto const members = peer_list(partition_uuid); + LOGDEBUG("apply_login[partition={}]: polling {} member(s) for GetRSCommitLSN", + boost::uuids::to_string(partition_uuid), members.size()); for (auto const& m : members) { if (m.id == ep_.id) { continue; } if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(current_term, true /* is_login */)); r) { - LOGDEBUG("apply_login[vol={}]: peer {} reported commit_lsn={} last_append_lsn={}", - boost::uuids::to_string(vol_uuid), boost::uuids::to_string(m.id), r->commit_lsn, + LOGDEBUG("apply_login[partition={}]: peer {} reported commit_lsn={} last_append_lsn={}", + boost::uuids::to_string(partition_uuid), boost::uuids::to_string(m.id), r->commit_lsn, r->last_append_lsn); peer_resp.emplace_back(r.value()); } else { - LOGWARN("apply_login[vol={}]: peer {} did not respond to GetRSCommitLSN", boost::uuids::to_string(vol_uuid), - boost::uuids::to_string(m.id)); + LOGWARN("apply_login[partition={}]: peer {} did not respond to GetRSCommitLSN", + boost::uuids::to_string(partition_uuid), boost::uuids::to_string(m.id)); } } // compute watermark as max(quorum.last_append) if (peer_resp.size() <= members.size() / 2) { - LOGERROR("apply_login[vol={}]: quorum not reached ({} of {} responded)", boost::uuids::to_string(vol_uuid), - peer_resp.size(), members.size()); + LOGERROR("apply_login[partition={}]: quorum not reached ({} of {} responded)", + boost::uuids::to_string(partition_uuid), peer_resp.size(), members.size()); return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); } auto const rs_commit_lsn = std::ranges::max_element(peer_resp, {}, &lsn_pair::last_append_lsn)->last_append_lsn; - LOGINFO("apply_login[vol={}]: computed rs_commit_lsn={} from {} responder(s)", boost::uuids::to_string(vol_uuid), - rs_commit_lsn, peer_resp.size()); + LOGINFO("apply_login[partition={}]: computed rs_commit_lsn={} from {} responder(s)", + boost::uuids::to_string(partition_uuid), rs_commit_lsn, peer_resp.size()); // Phase 1b: Leader behind - resolve all the missing lsns and // Phase 2: SyncRSCommitLSN() via RAFT (data NOT in log) - if (auto const r = sync_rs_commit_lsn(vol_uuid, rs_commit_lsn, client_token, current_term); !r) { - LOGERROR("apply_login[vol={}]: sync_rs_commit_lsn failed: {}", boost::uuids::to_string(vol_uuid), + if (auto const r = sync_rs_commit_lsn(partition_uuid, rs_commit_lsn, client_token, current_term); !r) { + LOGERROR("apply_login[partition={}]: sync_rs_commit_lsn failed: {}", boost::uuids::to_string(partition_uuid), r.error().message()); return std::unexpected(r.error()); } @@ -350,15 +423,16 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& login_done_ = false; } if (auto const r = raft_service_inst->propose( - vol_uuid, InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); + partition_uuid, + InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); !r) { // TODO: any cleanup required? - LOGERROR("apply_login[vol={}]: propose(InternalLogin term={}) failed: {}", boost::uuids::to_string(vol_uuid), - term, r.error().message()); + LOGERROR("apply_login[partition={}]: propose(InternalLogin term={}) failed: {}", + boost::uuids::to_string(partition_uuid), term, r.error().message()); return std::unexpected(r.error()); } - LOGDEBUG("apply_login[vol={}]: InternalLogin(term={}) proposed, waiting for commit", - boost::uuids::to_string(vol_uuid), term); + LOGDEBUG("apply_login[partition={}]: InternalLogin(term={}) proposed, waiting for commit", + boost::uuids::to_string(partition_uuid), term); // Phase 4: truncate above rs_commit_lsn // This happens in the internal login commit. Wait until that happens. @@ -366,8 +440,8 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& std::unique_lock< std::mutex > lk(login_mu_); login_cv_.wait_for(lk, LoginWaitTime, [&] { return login_done_; }); if (!login_done_) { - LOGERROR("apply_login[vol={}]: timed out waiting for InternalLogin(term={}) commit callback", - boost::uuids::to_string(vol_uuid), term); + LOGERROR("apply_login[partition={}]: timed out waiting for InternalLogin(term={}) commit callback", + boost::uuids::to_string(partition_uuid), term); return std::unexpected(make_error_condition(craft_error::INTERNAL)); } } @@ -376,37 +450,39 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& for (auto const& m : members) { replicas.emplace_back(replica_endpoint{.id = m.id, .addr = fmt::format("{}:{}", m.host, m.tcp_port)}); } - LOGINFO("apply_login[vol={}]: LOGIN SUCCESS term={} dLSN={} members={}", boost::uuids::to_string(vol_uuid), term, - rs_commit_lsn, replicas.size()); + LOGINFO("apply_login[partition={}]: LOGIN SUCCESS term={} dLSN={} members={}", + boost::uuids::to_string(partition_uuid), term, rs_commit_lsn, replicas.size()); return LoginResult{.members = replicas, .dLSN = rs_commit_lsn}; } // create peer raft group and add members to it. -result< void > RaftReplica::srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members) { - auto const vol_uuid = craft::to_uuid(volume_id); - auto const& repl_mgr = replica_manager::instance(); - // return success if the volume exists - if (auto const vol = repl_mgr->get_volume(vol_uuid); !vol.empty()) { - LOGINFO("Volume {} exists! Returning ok", boost::uuids::to_string(vol_uuid)); +result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const& partition_id, + std::vector< replica_endpoint > const& members) { + auto const partition_uuid = craft::to_uuid(partition_id); + auto const& registry = registry_manager::instance(); + // return success if the partition exists + if (auto const p = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_uuid)); !p.empty()) { + LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; } - LOGINFO("srv_create_volume[vol={}]: creating with {} member(s)", boost::uuids::to_string(vol_uuid), members.size()); + LOGINFO("srv_create_partition[partition={}]: creating with {} member(s)", boost::uuids::to_string(partition_uuid), + members.size()); - auto const r = raft_service::instance()->srv_create_volume(vol_uuid, members); + auto const r = raft_service::instance()->srv_create_partition(partition_uuid, members); if (r) { - repl_mgr->register_volume(vol_uuid, members); - LOGINFO("srv_create_volume[vol={}]: SUCCESS", boost::uuids::to_string(vol_uuid)); + registry->put< partition_peers_list_t >(partition_peers_list_key(partition_uuid), members); + LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { - LOGERROR("srv_create_volume[vol={}]: FAILED: {}", boost::uuids::to_string(vol_uuid), r.error().message()); + LOGERROR("srv_create_partition[partition={}]: FAILED: {}", boost::uuids::to_string(partition_uuid), + r.error().message()); } return r; } // Follower-side catch-up on SyncRSCommitLSN apply. Verdicts are already decided by the leader (empty_slots) // -- this never decides Empty itself, only obeys the verdict list or fetches real data. -void RaftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m) { +void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCommitLSNMsg m) { // Verdicts first -- permanent no-ops, no fetch needed. for (auto lsn : m.empty_slots) cold_mark_empty(lsn); @@ -418,7 +494,10 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSN } auto missing = get_missing_slots(m.rs_commit_lsn); - auto const peers = replica_manager::instance()->get_volume(vol_uuid); + auto const peers = peer_list(partition_uuid); + if (peers.empty()) { + LOGERROR("apply_sync[partition={}]: no peers found in registry", boost::uuids::to_string(partition_uuid)); + } for (auto const& peer : peers) { if (missing.empty()) break; if (peer.id == ep_.id) continue; // don't ask self @@ -439,16 +518,16 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSN } if (!missing.empty()) { - LOGERROR("apply_sync[vol={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " + LOGERROR("apply_sync[partition={}]: still missing {} slot(s) <= {} after asking all peers; commit_lsn will " "stall until the next SyncRSCommitLSN round -- first missing={}", - boost::uuids::to_string(vol_uuid), missing.size(), m.rs_commit_lsn, missing.front()); + boost::uuids::to_string(partition_uuid), missing.size(), m.rs_commit_lsn, missing.front()); } std::lock_guard< std::mutex > g{mu_}; apply_up_to(m.rs_commit_lsn); rs_commit_lsn_.store(m.rs_commit_lsn, std::memory_order_relaxed); - LOGDEBUG("apply_sync[vol={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(vol_uuid), - state_.commit_lsn, m.rs_commit_lsn); + LOGDEBUG("apply_sync[partition={}]: done, commit_lsn now {} (target rs_commit_lsn={})", + boost::uuids::to_string(partition_uuid), state_.commit_lsn, m.rs_commit_lsn); } session_info RaftReplica::srv_session_info(std::array< uint8_t, 16 > const&) const { @@ -457,8 +536,8 @@ session_info RaftReplica::srv_session_info(std::array< uint8_t, 16 > const&) con } void RaftReplica::internal_login(InternalLoginMsg m) { - cold_apply_login(m.client_token, m.term); if (m.rs_commit_lsn >= 0) { cold_truncate_above(m.rs_commit_lsn); } + cold_apply_login(m.client_token, m.term); { std::lock_guard< std::mutex > lk(login_mu_); login_done_ = true; diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index f96109f..3a60b66 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -39,28 +39,28 @@ struct InternalLoginMsg; class RaftReplica final : public MemCraftReplica { public: - - RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx); + RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path); ~RaftReplica(); - result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, + result< LoginResult > srv_establish(std::array< uint8_t, 16 > const& partition_id, uint64_t client_token, uint64_t term) { - return apply_login(volume_id, client_token, term); + return apply_login(partition_id, client_token, term); } - result< void > srv_create_volume(std::array< uint8_t, 16 > const& volume_id, - std::vector< replica_endpoint > const& members); + result< void > srv_create_partition(std::array< uint8_t, 16 > const& partition_id, + std::vector< replica_endpoint > const& members); result< lsn_pair > srv_get_rs_commit_lsn(uint64_t term, bool is_login) { return do_get_rs_commit_lsn(term, is_login); } result< std::vector< JournalSlot > > srv_fetch_data(std::vector< int64_t > const& lsns); - session_info srv_session_info(std::array< uint8_t, 16 > const& volume_id) const; + session_info srv_session_info(std::array< uint8_t, 16 > const& partition_id) const; private: result< lsn_pair > do_get_rs_commit_lsn(uint64_t term, bool is_login); void apply_sync(boost::uuids::uuid const& vol_uuid, SyncRSCommitLSNMsg m); - result< LoginResult > apply_login(std::array< uint8_t, 16 > const& volume_id, uint64_t client_token, uint64_t term); + result< LoginResult > apply_login(std::array< uint8_t, 16 > const& partition_id, uint64_t client_token, + uint64_t term); MemCraftReplica::MemJournalSlot to_mem_journal_slot(JournalSlot const& j, uint64_t term); std::vector< int64_t > get_missing_slots(int64_t watermark); std::pair< std::vector< int64_t >, int64_t > @@ -68,6 +68,9 @@ class RaftReplica final : public MemCraftReplica { result< void > sync_rs_commit_lsn(boost::uuids::uuid const& vol_uuid, int64_t rs_commit_lsn, uint64_t client_token, uint64_t term); void internal_login(InternalLoginMsg m); + void raft_init(); + void replica_init(std::string const& replica_config_path); + std::string replica_info_key() const; uint32_t max_tx_; std::atomic< int64_t > rs_commit_lsn_{-1}; diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 55e70cc..5bbb123 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -64,10 +64,10 @@ raft_service::~raft_service() { } } -result< void > raft_service::srv_create_volume(boost::uuids::uuid const& group_id, - std::vector< replica_endpoint > const& members) { +result< void > raft_service::srv_create_partition(boost::uuids::uuid const& group_id, + std::vector< replica_endpoint > const& members) { if (!consensus_) { - // raft_service::srv_create_volume should not be called if raft service is not enabled + // raft_service::srv_create_partition should not be called if raft service is not enabled LOGERROR("Raft not enabled!"); return fail(craft_error::INTERNAL); } @@ -130,7 +130,6 @@ void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::s void raft_service::add_commit_cb(raft_commit_cb_t cb) { // we expect that this is called only once if (!consensus_) { - // raft_service::srv_create_volume should not be called if raft service is not enabled LOGERROR("Raft not enabled!"); return; } @@ -139,7 +138,6 @@ void raft_service::add_commit_cb(raft_commit_cb_t cb) { bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { if (!consensus_) { - // raft_service::srv_create_volume should not be called if raft service is not enabled LOGERROR("Raft not enabled!"); return false; } @@ -154,7 +152,6 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& group_id) { if (!consensus_) { - // raft_service::srv_create_volume should not be called if raft service is not enabled LOGERROR("Raft not enabled!"); return {}; } diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 2be4427..425adef 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -31,8 +31,8 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab bool is_raft_enabled() { return consensus_ != nullptr; } consensus_handle get_consensus(); void start_raft_service(boost::uuids::uuid const& server_uuid); - result< void > srv_create_volume(boost::uuids::uuid const& group_id, - std::vector< replica_endpoint > const& members); + result< void > srv_create_partition(boost::uuids::uuid const& group_id, + std::vector< replica_endpoint > const& members); void add_commit_cb(raft_commit_cb_t cb); bool is_leader(nuraft_mesg::group_id_t const& group_id); nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); diff --git a/src/registry_mgr.cpp b/src/registry_mgr.cpp new file mode 100644 index 0000000..8171115 --- /dev/null +++ b/src/registry_mgr.cpp @@ -0,0 +1,10 @@ +#include "registry_mgr.hpp" + +namespace craft { + +std::shared_ptr< registry_manager > registry_manager::instance() { + static std::shared_ptr< registry_manager > inst{new registry_manager()}; + return inst; +} + +} \ No newline at end of file diff --git a/src/registry_mgr.hpp b/src/registry_mgr.hpp new file mode 100644 index 0000000..fbaf514 --- /dev/null +++ b/src/registry_mgr.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace craft { + +class registry_manager { +public: + static std::shared_ptr< registry_manager > instance(); + + template + void put(std::string const& key, std::shared_ptr value) { + std::lock_guard lock(component_mutex_); + component_store_[key] = std::move(value); + } + + template + std::shared_ptr get(std::string const& key) const { + std::lock_guard lock(component_mutex_); + auto it = component_store_.find(key); + if (it == component_store_.end()) return nullptr; + auto* ptr = std::any_cast>(&it->second); + return ptr ? *ptr : nullptr; + } + + +private: + registry_manager() = default; + + mutable std::mutex component_mutex_; + std::unordered_map component_store_; +}; + +} \ No newline at end of file diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp index c4abbdc..52bfab8 100644 --- a/src/replica_mgr.hpp +++ b/src/replica_mgr.hpp @@ -15,13 +15,6 @@ namespace craft { // One member's full identity + reachability, everything replica_manager needs to answer both raft's // lookup_peer and the wire-plane peer client from a single source of truth. -struct replica_info { - boost::uuids::uuid id{}; - std::string host; - uint16_t raft_port{0}; - uint16_t tcp_port{0}; - std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; -}; // Process-wide registry: peer identity -> reachability, and (lazily) the open peer-plane connection to it. // Replaces peer_comm::peer_lookup_map_ and net::peer_client_service's addrs_/peers_ split -- one map for From 74e185c1391c085a2ef20ea742f640e910ba6699 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Fri, 14 Aug 2026 11:07:09 -0700 Subject: [PATCH 15/24] remove replica manager and use the registry --- CMakeLists.txt | 5 ++- src/net/tcp_server.cpp | 22 +++------- src/raft/raft_replica.cpp | 72 +++++++++++++++++++----------- src/raft/raft_replica.hpp | 1 - src/raft/raft_service.cpp | 16 ++++--- src/raft/raft_service.hpp | 4 ++ src/replica_mgr.cpp | 92 --------------------------------------- src/replica_mgr.hpp | 43 ------------------ 8 files changed, 68 insertions(+), 187 deletions(-) delete mode 100644 src/replica_mgr.cpp delete mode 100644 src/replica_mgr.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f2fc381..b48644f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,15 +80,16 @@ add_library(raft_service STATIC src/raft/raft_state_manager.cpp) target_include_directories(raft_service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(raft_service PUBLIC nuraft_mesg::proto registry_mgr) +target_link_libraries(raft_service PUBLIC nuraft_mesg::proto registry_mgr craft_reference) target_compile_features(raft_service PUBLIC cxx_std_23) add_library(raft_tcp_server STATIC src/raft/raft_replica.cpp + src/net/tcp_peer.cpp src/net/tcp_server.cpp) target_include_directories(raft_tcp_server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_tcp_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(raft_tcp_server PUBLIC craft_reference raft_service) +target_link_libraries(raft_tcp_server PUBLIC raft_service) target_compile_features(raft_tcp_server PUBLIC cxx_std_23) # ── craft_reference_tcp_srv: a STANDALONE single-replica reference server. Run N of them on different ports to diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 443b895..c789779 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -21,13 +21,13 @@ #include #include +#include #include // server-side r/w trace (base module; visible with -v trace / when a consumer inits logging) #include "raft/raft_replica.hpp" // the full RaftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) -#include // to_wire_status (the shared wire <-> craft_error bridge) #include "raft/raft_service.hpp" -#include "replica_mgr.hpp" +#include // to_wire_status (the shared wire <-> craft_error bridge) #include "helper.hpp" namespace craft::net { @@ -42,17 +42,7 @@ std::span< uint8_t const > as_bytes(T const& v) { craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : geo_{std::move(geo)} { auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); - // net == nullptr: this replica serves exclusively through its srv_* seam (the TCP frontend IS the wire). - // start replica service and raft service if server_config_file is provided - if (!server_config_file.empty()) { - replica_manager::instance()->start_replica_service(server_config_file, ep.id); - raft_service::instance()->start_raft_service(ep.id); - LOGINFO("craft_tcp_server: replica_manager + raft_service started [id={}]", boost::uuids::to_string(ep.id)); - } else { - LOGINFO("craft_tcp_server: no server_config_file given -- running in standalone/cold-path mode [id={}]", - boost::uuids::to_string(ep.id)); - } - replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx); + replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx, server_config_file); } craft_tcp_server::~craft_tcp_server() = default; @@ -177,7 +167,7 @@ void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { auto const hr = wire::decode< wire::helo_req >(req.op_header); wire::status code = wire::status::ok; - if (bool is_raft_enabled = raft_service::instance()->is_raft_enabled(); !is_raft_enabled) { + if (!raft_service::instance()->is_raft_enabled()) { // no raft, follow fake cold path auto result = replica_->srv_establish(hr.volume_id, hr.client_token, session_term_); if (!result) { @@ -363,8 +353,8 @@ void craft_tcp_server::on_create_volume(craft_conn& conn, wire::message const& r replica_members.emplace_back(replica_endpoint{.id = craft::to_uuid(m.id), .addr = m.addr}); } - if (auto const r = replica_->srv_create_volume(cr.volume_id, replica_members); !r) { - LOGERROR("craft_srv CREATE_VOLUME [rid:{}]: srv_create_volume failed: {}", req.hdr.request_id, + if (auto const r = replica_->srv_create_partition(cr.volume_id, replica_members); !r) { + LOGERROR("craft_srv CREATE_VOLUME [rid:{}]: srv_create_partition failed: {}", req.hdr.request_id, r.error().message()); code = to_wire_status(r.error()); } diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 7b4ccb2..e4fa125 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -19,6 +19,7 @@ #include "raft/raft_state_machine.hpp" // for raft message payload types #include "helper.hpp" #include "craft/types.hpp" +#include "net/tcp_peer.hpp" #include #include @@ -27,7 +28,7 @@ #include #include #include - +#include #include @@ -57,18 +58,31 @@ struct replica_info { std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; }; +std::string replica_info_key(boost::uuids::uuid const& replica_id) { + return fmt::format("replica_info_{}", boost::uuids::to_string(replica_id)); +} + using partition_peers_list_t = std::vector< boost::uuids::uuid >; -std::string partition_peers_list_key(boost::uuids::uuid const& partition_id){return fmt::format("partition_{}", )} +std::string partition_peers_list_key(boost::uuids::uuid const& partition_id) { + return fmt::format("partition_{}", boost::uuids::to_string(partition_id)); +} -partition_peers_list_t peer_list(boost::uuids::uuid const& partition_id) { +std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id) { partition_peers_list_t peers; - if (auto peers_ptr = - registry_manager::instance()->get< partition_peers_list_t >(partition_peers_list_key(partition_id)); - peers_ptr) { + auto registry = registry_manager::instance(); + if (auto peers_ptr = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_id)); peers_ptr) { peers = *peers_ptr; } - return peers; + std::vector< replica_info > replica_members; + for (auto const& peer_id : peers) { + if (auto rinfo = registry->get< replica_info >(replica_info_key(peer_id)); rinfo) { + replica_members.emplace_back(*rinfo); + } else { + LOGWARN("Peer {} not found in registry", boost::uuids::to_string(peer_id)); + } + } + return replica_members; } } // namespace @@ -106,11 +120,13 @@ class RaftReplica::RaftCommitWorker { } }; -std::string replica_info_key() const { return fmt::format("replica_info_{}", boost::uuids::to_string(ep_.id)); } - -void RaftReplica::replics_init(std::string const& replica_config_path) { +void RaftReplica::replica_init(std::string const& replica_config_path) { + if (replica_config_path.empty()) { + LOGWARN("No replica config path provided, skipping replica initialization"); + return; + } auto registry = registry_manager::instance(); - if (auto const rinfo = registry->get< replica_info >(replica_info_key())) { + if (auto const rinfo = registry->get< replica_info >(replica_info_key(ep_.id)); rinfo) { // recovery from registry return; } @@ -131,24 +147,25 @@ void RaftReplica::replics_init(std::string const& replica_config_path) { for (auto const& m : j.at("members")) { auto const id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); - auto r = std::make_shared< replica_info >( - id, - replica_info{ - .id = id, - .host = m.at("host").get< std::string >(), - .raft_port = m.at("raft_port").get< uint16_t >(), - .tcp_port = m.at("tcp_port").get< uint16_t >(), - .peer_client = (id == ep_.id) - ? nullptr - : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), - m.at("tcp_port").get< uint16_t >(), id), - }); - registry->put(replica_info_key(), std::move(r)); + auto r = std::make_shared< replica_info >(replica_info{ + .id = id, + .host = m.at("host").get< std::string >(), + .raft_port = m.at("raft_port").get< uint16_t >(), + .tcp_port = m.at("tcp_port").get< uint16_t >(), + .peer_client = (id == ep_.id) + ? nullptr + : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), + m.at("tcp_port").get< uint16_t >(), id), + }); + registry->put< raft_peer_t >(raft_service::peer_id_key(id), + std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); + registry->put< replica_info >(replica_info_key(r->id), std::move(r)); } } void RaftReplica::raft_init() { auto raft_inst = raft_service::instance(); + raft_inst->start_raft_service(ep_.id); if (!raft_inst->is_raft_enabled()) { return; } auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& partition_uuid_str) { auto const partition_uuid = boost::uuids::string_generator()(partition_uuid_str); @@ -462,7 +479,7 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const auto const partition_uuid = craft::to_uuid(partition_id); auto const& registry = registry_manager::instance(); // return success if the partition exists - if (auto const p = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_uuid)); !p.empty()) { + if (auto p = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_uuid)); p && !p->empty()) { LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; } @@ -471,7 +488,10 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const auto const r = raft_service::instance()->srv_create_partition(partition_uuid, members); if (r) { - registry->put< partition_peers_list_t >(partition_peers_list_key(partition_uuid), members); + auto view = members | std::views::transform(&replica_endpoint::id); + std::vector< boost::uuids::uuid > peer_uuids(view.begin(), view.end()); + registry->put< partition_peers_list_t >(partition_peers_list_key(partition_uuid), + std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { LOGERROR("srv_create_partition[partition={}]: FAILED: {}", boost::uuids::to_string(partition_uuid), diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 3a60b66..09e3a85 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -70,7 +70,6 @@ class RaftReplica final : public MemCraftReplica { void internal_login(InternalLoginMsg m); void raft_init(); void replica_init(std::string const& replica_config_path); - std::string replica_info_key() const; uint32_t max_tx_; std::atomic< int64_t > rs_commit_lsn_{-1}; diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 5bbb123..0f35ac7 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -1,8 +1,8 @@ #include #include "raft_service.hpp" #include "raft_state_manager.hpp" +#include "registry_mgr.hpp" #include "helper.hpp" -#include "replica_mgr.hpp" #include #include @@ -34,14 +34,14 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { // raft global manager for commits nuraft::nuraft_global_mgr::init(); std::call_once(raft_started_, [&] { - auto const& my_info = replica_manager::instance()->get(server_uuid); + auto my_info = registry_manager::instance()->get< raft_peer_t >(raft_service::peer_id_key(server_uuid)); if (!my_info) { LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid); return; } auto params = nuraft_mesg::manager::params{ .server_uuid_ = server_uuid, - .mesg_port_ = my_info->raft_port, + .mesg_port_ = my_info->second, .default_group_type_ = default_group_type_, }; consensus_ = nuraft_mesg::init_messaging(params, weak_from_this(), true /*with_data_svc*/); @@ -95,10 +95,12 @@ result< void > raft_service::srv_create_partition(boost::uuids::uuid const& grou } std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { - - auto peer_addr = replica_manager::instance()->lookup_peer(peer_id); - if (peer_addr.empty()) { LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); } - return peer_addr; + if (auto peer_addr = registry_manager::instance()->get< raft_peer_t >(raft_service::peer_id_key(peer_id)); + peer_addr) { + return fmt::format("{}:{}", peer_addr->first, peer_addr->second); + } + LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); + return {}; } std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 425adef..015265e 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -14,6 +14,7 @@ class manager; } using consensus_handle = std::shared_ptr< nuraft_mesg::manager >; +using raft_peer_t = std::pair< std::string, uint16_t >; // namespace craft { @@ -25,6 +26,9 @@ class raft_state_mgr; class raft_service : public nuraft_mesg::messaging_application, public std::enable_shared_from_this< raft_service > { public: inline static const std::string default_group_type_{"raft_service_raft"}; + inline static const std::string peer_id_key(boost::uuids::uuid const& peer_id) { + return fmt::format("raft_peer_{}", boost::uuids::to_string(peer_id)); + } virtual ~raft_service(); static std::shared_ptr< raft_service > instance(); diff --git a/src/replica_mgr.cpp b/src/replica_mgr.cpp deleted file mode 100644 index 4c95b6d..0000000 --- a/src/replica_mgr.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "replica_mgr.hpp" -#include "helper.hpp" - -#include - -#include -#include -#include -#include - -namespace craft { - -std::shared_ptr< replica_manager > replica_manager::instance() { - static std::shared_ptr< replica_manager > inst{new replica_manager()}; - return inst; -} - -void replica_manager::start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid) { - id_ = my_uuid; - std::ifstream istrm(path, std::ios::binary); - if (!istrm.is_open()) { - LOGERROR("replica_manager: could not open {}", path); - return; - } - - nlohmann::json j; - try { - istrm >> j; - } catch (nlohmann::json::parse_error const& e) { - LOGERROR("replica_manager: parse error in {}: {}", path, e.what()); - return; - } - - replicas_.clear(); - for (auto const& m : j.at("members")) { - auto const id = boost::uuids::string_generator()(m.at("uuid").get< std::string >()); - replicas_.emplace(id, - replica_info{ - .id = id, - .host = m.at("host").get< std::string >(), - .raft_port = m.at("raft_port").get< uint16_t >(), - .tcp_port = m.at("tcp_port").get< uint16_t >(), - .peer_client = (id == my_uuid) - ? nullptr - : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), - m.at("tcp_port").get< uint16_t >(), id), - }); - } -} - -std::string replica_manager::lookup_peer(boost::uuids::uuid const& id) const { - std::shared_lock< std::shared_mutex > g(mu_); - auto const it = replicas_.find(id); - if (it == replicas_.end()) return {}; - return it->second.host + ":" + std::to_string(it->second.raft_port); -} - -std::optional< replica_info > replica_manager::get(boost::uuids::uuid const& id) const { - std::shared_lock< std::shared_mutex > g(mu_); - auto const it = replicas_.find(id); - if (it == replicas_.end()) return std::nullopt; - return it->second; -} - -result< void > replica_manager::register_volume(boost::uuids::uuid const& vol_uuid, - std::vector< replica_endpoint > const& members) { - std::lock_guard< std::shared_mutex > g{mu_}; - std::vector< boost::uuids::uuid > rinfos; - for (auto const& m : members) { - if (auto const it = replicas_.find(m.id); it == replicas_.end()) { - LOGERROR("Unknown replica {} in the volume {}", boost::uuids::to_string(m.id), - boost::uuids::to_string(vol_uuid)); - return std::unexpected(make_error_condition(craft_error::INTERNAL)); - } - rinfos.emplace_back(m.id); - } - volumes_[vol_uuid] = rinfos; - return {}; -} - -std::vector< replica_info > replica_manager::get_volume(boost::uuids::uuid const& volume_id) { - std::shared_lock< std::shared_mutex > g(mu_); - auto const it = volumes_.find(volume_id); - if (it == volumes_.end()) return {}; - std::vector< replica_info > rinfo; - for (auto const& m_id : it->second) { - rinfo.emplace_back(replicas_[m_id]); // we check the existance of the m_id in replicas_ during vol registration. - } - return rinfo; -} - -} // namespace craft diff --git a/src/replica_mgr.hpp b/src/replica_mgr.hpp deleted file mode 100644 index 52bfab8..0000000 --- a/src/replica_mgr.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include // peer_id_t -#include "net/tcp_peer.hpp" - -namespace craft { - -// One member's full identity + reachability, everything replica_manager needs to answer both raft's -// lookup_peer and the wire-plane peer client from a single source of truth. - -// Process-wide registry: peer identity -> reachability, and (lazily) the open peer-plane connection to it. -// Replaces peer_comm::peer_lookup_map_ and net::peer_client_service's addrs_/peers_ split -- one map for -// static info (loaded once, rarely mutated), one for live connections (grown lazily, per actual use). -class replica_manager { -public: - static std::shared_ptr< replica_manager > instance(); - - void start_replica_service(std::string const& path, boost::uuids::uuid const& my_uuid); - - // raft's messaging_application::lookup_peer bridge: peer_id -> "host:raft_port". - std::string lookup_peer(boost::uuids::uuid const& id) const; - std::optional< replica_info > get(boost::uuids::uuid const& id) const; - result< void > register_volume(boost::uuids::uuid const& vol_uuid, std::vector< replica_endpoint > const& members); - std::vector< replica_info > get_volume(boost::uuids::uuid const& volume_id); - -private: - replica_manager() = default; - - boost::uuids::uuid id_; - mutable std::shared_mutex mu_; - std::map< boost::uuids::uuid, replica_info > replicas_; // static, loaded once - std::map< boost::uuids::uuid, std::vector< boost::uuids::uuid > > volumes_; -}; - -} // namespace craft From 971831da7a2ab275d15d287ac0927218c5b7d633 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sat, 15 Aug 2026 07:08:06 -0700 Subject: [PATCH 16/24] add support torestart replica by moving journal and index into registry --- CMakeLists.txt | 8 +---- src/mem/replica.cpp | 64 +++++++++++++++++++----------------- src/mem/replica.hpp | 8 +++-- src/raft/raft_replica.cpp | 69 ++++++++++++++++++++++++--------------- src/raft/raft_replica.hpp | 2 +- src/registry_mgr.hpp | 4 --- 6 files changed, 85 insertions(+), 70 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b48644f..55faf96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,19 +68,13 @@ target_include_directories(craft_reference PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/s target_link_libraries(craft_reference PUBLIC craft_client) target_compile_features(craft_reference PUBLIC cxx_std_23) -add_library(registry_mgr STATIC - src/registry_mgr.cpp) -target_include_directories(registry_mgr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) -target_include_directories(registry_mgr PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_compile_features(registry_mgr PUBLIC cxx_std_23) - add_library(raft_service STATIC src/raft/raft_service.cpp src/raft/in_memory_log_store.cpp src/raft/raft_state_manager.cpp) target_include_directories(raft_service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(raft_service PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(raft_service PUBLIC nuraft_mesg::proto registry_mgr craft_reference) +target_link_libraries(raft_service PUBLIC nuraft_mesg::proto craft_reference) target_compile_features(raft_service PUBLIC cxx_std_23) add_library(raft_tcp_server STATIC diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 1d1dcc6..cb65e3c 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -49,7 +49,11 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } // namespace MemCraftReplica::MemCraftReplica(replica_endpoint ep, uint32_t page_size, std::shared_ptr< MemTransport > net) : - ep_{std::move(ep)}, page_size_{page_size}, net_{std::move(net)} { + ep_{std::move(ep)}, + page_size_{page_size}, + net_{std::move(net)}, + journal_{std::make_shared< journal_t >()}, + index_{std::make_shared< index_t >()} { // Publish the initial (healthy) fault snapshot before any IO can read it. auto initial = std::make_unique< replica_faults const >(); faults_.store(initial.get(), std::memory_order_release); @@ -237,7 +241,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 } std::lock_guard< std::mutex > g{mu_}; if (hdr.term != state_.term) return fail(craft_error::STALE_TERM); - if (auto it = journal_.find(dlsn); it != journal_.end() && it->second.is_empty) { + if (auto it = journal_->find(dlsn); it != journal_->end() && it->second.is_empty) { // An Empty verdict is permanent (reconciliation: Empty beats data). A late arrival into the slot is // REJECTED -- deterministically -- so that write's own ack path concludes the slot is void, matching // the verdict instead of phantom-acking a write every replica discarded. @@ -250,7 +254,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 slot.len = static_cast< lba_count_t >(len / page_size_); // byte length -> block count slot.all_zeros = !bytes; // no payload => zero write; no all_zeros flag slot.bytes = std::move(bytes); // adopt the buffer; do not copy it again - journal_[dlsn] = std::move(slot); + (*journal_)[dlsn] = std::move(slot); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); apply_up_to(hdr.commit_lsn); // piggybacked commit: advance the frontier best-effort, in dLSN order // Piggyback the watermarks on the ack (the wire's write_rsp), so any round-trip refreshes the client. @@ -289,7 +293,7 @@ result< lsn_pair > MemCraftReplica::do_lsns() { status MemCraftReplica::do_truncate(int64_t lsn) { std::lock_guard< std::mutex > g{mu_}; - journal_.erase(journal_.upper_bound(lsn), journal_.end()); + journal_->erase(journal_->upper_bound(lsn), journal_->end()); state_.last_append_lsn = std::min(state_.last_append_lsn, lsn); return ok(); } @@ -298,8 +302,8 @@ result< std::vector< JournalSlot > > MemCraftReplica::do_fetch(std::vector< int6 std::lock_guard< std::mutex > g{mu_}; std::vector< JournalSlot > out; for (auto lsn : lsns) { - auto it = journal_.find(lsn); - if (it == journal_.end()) continue; // not-present-here => omit + auto it = journal_->find(lsn); + if (it == journal_->end()) continue; // not-present-here => omit auto const& s = it->second; JournalSlot js; js.lsn = lsn; @@ -325,12 +329,12 @@ result< resolution_result > MemCraftReplica::do_resolve_local(client_hdr hdr, in if (hdr.term != state_.term) return fail(craft_error::STALE_TERM); resolution_result out{upto, {}}; for (int64_t d = state_.commit_lsn + 1; d <= upto; ++d) { - auto it = journal_.find(d); - if (it == journal_.end()) { + auto it = journal_->find(d); + if (it == journal_->end()) { MemJournalSlot s; s.term = state_.term; s.is_empty = true; - journal_[d] = std::move(s); + (*journal_)[d] = std::move(s); out.empty_slots.push_back(d); } else if (it->second.is_empty) { out.empty_slots.push_back(d); // a prior verdict; re-report it so the client can retire the slot @@ -346,11 +350,11 @@ result< resolution_result > MemCraftReplica::do_resolve_local(client_hdr hdr, in void MemCraftReplica::apply_slot(int64_t dlsn, MemJournalSlot const& s) { if (s.all_zeros) { for (lba_count_t i = 0; i < s.len; ++i) { - index_.erase(s.lba + i); // unmap => hole + index_->erase(s.lba + i); // unmap => hole } } else { for (lba_count_t i = 0; i < s.len; ++i) { - index_[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * page_size_}; + (*index_)[s.lba + i] = IndexCell{dlsn, s.bytes, static_cast< std::size_t >(i) * page_size_}; } } } @@ -358,8 +362,8 @@ void MemCraftReplica::apply_slot(int64_t dlsn, MemJournalSlot const& s) { void MemCraftReplica::apply_up_to(int64_t target) { int64_t next = state_.commit_lsn + 1; while (next <= target) { - auto it = journal_.find(next); - if (it == journal_.end()) break; // Missing hole -> stall (best-effort) + auto it = journal_->find(next); + if (it == journal_->end()) break; // Missing hole -> stall (best-effort) if (!it->second.is_empty) apply_slot(next, it->second); state_.commit_lsn = next; // Empty slots are skipped on apply but still advance the frontier ++next; @@ -369,7 +373,7 @@ void MemCraftReplica::apply_up_to(int64_t target) { // Highest-dLSN journal-tail slot with commit_lsn < dLSN <= H that covers `x` (the journal-tail overlay, // materialized on demand). Slots above H are never examined -- that is the horizon clamp. MemCraftReplica::MemJournalSlot const* MemCraftReplica::highest_slot_le(lba_t x, int64_t H) const { - for (auto it = journal_.upper_bound(H); it != journal_.begin();) { + for (auto it = journal_->upper_bound(H); it != journal_->begin();) { --it; if (it->first <= state_.commit_lsn) break; // reached the applied prefix (served from index_) auto const& s = it->second; @@ -419,7 +423,7 @@ std::vector< io_extent > MemCraftReplica::read_range(int64_t H, uint64_t addr, u page = s->bytes->data() + static_cast< std::size_t >(x - s->lba) * page_size_; hole = false; } // else: zero write => hole - } else if (auto it = index_.find(x); it != index_.end()) { + } else if (auto it = index_->find(x); it != index_->end()) { page = it->second.buf->data() + it->second.off; hole = false; } @@ -453,14 +457,14 @@ replica_stats MemCraftReplica::stats() const { s.last_append_lsn = state_.last_append_lsn; s.term = state_.term; s.client_token = state_.client_token; - s.mapped_blocks = index_.size(); + s.mapped_blocks = index_->size(); - s.journal_slots = journal_.size(); - if (!journal_.empty()) { - s.journal_first_dlsn = journal_.begin()->first; - s.journal_last_dlsn = journal_.rbegin()->first; + s.journal_slots = journal_->size(); + if (!journal_->empty()) { + s.journal_first_dlsn = journal_->begin()->first; + s.journal_last_dlsn = journal_->rbegin()->first; } - for (auto const& [dlsn, slot] : journal_) { + for (auto const& [dlsn, slot] : (*journal_)) { if (slot.is_empty) { ++s.empty_slots; } else if (slot.all_zeros) { @@ -476,8 +480,8 @@ replica_stats MemCraftReplica::stats() const { int64_t const lo = state_.commit_lsn + 1; int64_t const hi = state_.last_append_lsn; if (hi >= lo) { - auto const first = journal_.lower_bound(lo); - auto const last = journal_.upper_bound(hi); + auto const first = journal_->lower_bound(lo); + auto const last = journal_->upper_bound(hi); auto const present = static_cast< std::size_t >(std::distance(first, last)); s.missing_count = static_cast< std::size_t >(hi - lo + 1) - present; @@ -519,7 +523,7 @@ void MemCraftReplica::cold_apply_logout() { } void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { std::lock_guard< std::mutex > g{mu_}; - journal_.erase(journal_.upper_bound(rs_commit_lsn), journal_.end()); + journal_->erase(journal_->upper_bound(rs_commit_lsn), journal_->end()); state_.last_append_lsn = std::min(state_.last_append_lsn, rs_commit_lsn); } @@ -527,15 +531,15 @@ void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { std::optional< MemCraftReplica::MemJournalSlot > MemCraftReplica::peek_slot(int64_t dlsn) { std::lock_guard< std::mutex > g{mu_}; - auto const it = journal_.find(dlsn); - if (it == journal_.end()) return std::nullopt; + auto const it = journal_->find(dlsn); + if (it == journal_->end()) return std::nullopt; return it->second; // copies the slot; `bytes` is shared (immutable once appended), so no payload copy } void MemCraftReplica::cold_install_slot(int64_t dlsn, MemJournalSlot s) { std::lock_guard< std::mutex > g{mu_}; - if (journal_.contains(dlsn)) return; // already holds it (or a verdict); a fetch never overwrites - journal_[dlsn] = std::move(s); + if (journal_->contains(dlsn)) return; // already holds it (or a verdict); a fetch never overwrites + (*journal_)[dlsn] = std::move(s); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); } @@ -546,14 +550,14 @@ void MemCraftReplica::cold_mark_empty(int64_t dlsn) { MemJournalSlot s; s.term = state_.term; s.is_empty = true; - journal_[dlsn] = std::move(s); + (*journal_)[dlsn] = std::move(s); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); } std::vector< int64_t > MemCraftReplica::peek_empties(int64_t upto) { std::lock_guard< std::mutex > g{mu_}; std::vector< int64_t > out; - for (auto const& [d, s] : journal_) { + for (auto const& [d, s] : (*journal_)) { if (d > upto) break; if (s.is_empty) out.push_back(d); // ascending: journal_ is an ordered map } diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 2890845..0b28214 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -226,6 +226,9 @@ class MemCraftReplica : public craft_replica, std::size_t off{0}; }; + using journal_t = std::map< int64_t, MemJournalSlot >; + using index_t = std::map< lba_t, IndexCell >; + // Synchronous cores: the SERVER. Each takes mu_. Deliverability, latency and payload ownership are the // transport's job (MemTransport::send_*), which is why nothing below consults net_ or copies bytes. // do_write takes the payload already owned and adopts it; `bytes == nullptr` is a zero write. @@ -297,8 +300,9 @@ class MemCraftReplica : public craft_replica, std::shared_ptr< MemTransport > net_; CraftPartitionState state_; - std::map< int64_t, MemJournalSlot > journal_; // dLSN -> slot (out-of-order arrival tolerated) - std::map< lba_t, IndexCell > index_; // applied prefix (<= commit_lsn); an absent LBA is a hole + // shared_ptr for restart support using registry_manager. + std::shared_ptr< journal_t > journal_; // dLSN -> slot (out-of-order arrival tolerated) + std::shared_ptr< index_t > index_; // applied prefix (<= commit_lsn); an absent LBA is a hole mutable std::mutex mu_; }; diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index e4fa125..8c5f51e 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -32,6 +32,8 @@ #include +#define REGISTRY_KEY(prefix, id) fmt::format("{}_{}", prefix, boost::uuids::to_string(id)) + namespace craft { namespace { @@ -49,6 +51,11 @@ std::shared_ptr< std::vector< uint8_t > > take_payload(sisl::sg_list const& s) { } constexpr auto LoginWaitTime = std::chrono::seconds(2); +std::string const replica_info_key_prefix{"replica_info"}; +std::string const partition_info_key_prefix{"partition_info"}; +std::string const journal_key_prefix{"journal"}; +std::string const index_key_prefix{"index"}; +using partition_peers_list_t = std::vector< boost::uuids::uuid >; struct replica_info { boost::uuids::uuid id{}; @@ -58,25 +65,16 @@ struct replica_info { std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; }; -std::string replica_info_key(boost::uuids::uuid const& replica_id) { - return fmt::format("replica_info_{}", boost::uuids::to_string(replica_id)); -} - -using partition_peers_list_t = std::vector< boost::uuids::uuid >; - -std::string partition_peers_list_key(boost::uuids::uuid const& partition_id) { - return fmt::format("partition_{}", boost::uuids::to_string(partition_id)); -} - std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id) { partition_peers_list_t peers; auto registry = registry_manager::instance(); - if (auto peers_ptr = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_id)); peers_ptr) { + if (auto peers_ptr = registry->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_id)); + peers_ptr) { peers = *peers_ptr; } std::vector< replica_info > replica_members; for (auto const& peer_id : peers) { - if (auto rinfo = registry->get< replica_info >(replica_info_key(peer_id)); rinfo) { + if (auto rinfo = registry->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, peer_id)); rinfo) { replica_members.emplace_back(*rinfo); } else { LOGWARN("Peer {} not found in registry", boost::uuids::to_string(peer_id)); @@ -126,7 +124,7 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { return; } auto registry = registry_manager::instance(); - if (auto const rinfo = registry->get< replica_info >(replica_info_key(ep_.id)); rinfo) { + if (auto const rinfo = registry->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, ep_.id)); rinfo) { // recovery from registry return; } @@ -159,12 +157,16 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { }); registry->put< raft_peer_t >(raft_service::peer_id_key(id), std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); - registry->put< replica_info >(replica_info_key(r->id), std::move(r)); + registry->put< replica_info >(REGISTRY_KEY(replica_info_key_prefix, r->id), std::move(r)); } } void RaftReplica::raft_init() { auto raft_inst = raft_service::instance(); + if (raft_inst->is_raft_enabled()) { + LOGINFO("RAFT already initialized for replica {}", boost::uuids::to_string(ep_.id)); + return; + } raft_inst->start_raft_service(ep_.id); if (!raft_inst->is_raft_enabled()) { return; } auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& partition_uuid_str) { @@ -204,23 +206,37 @@ void RaftReplica::raft_init() { } }; - auto group_create_cb = [this](boost::uuids::uuid const& group_id) { - - }; raft_inst->add_commit_cb(std::move(commit_cb)); - - // start background commit offload worker - commit_worker_ = std::make_unique< RaftReplica::RaftCommitWorker >(); LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), page_size_); } +void RaftReplica::journal_init() { + auto registry = registry_manager::instance(); + if (auto existing = registry->get< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id)); !existing) { + LOGINFO("No journal found in registry for replica {}", boost::uuids::to_string(ep_.id)); + registry->put< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id), journal_); + } else { + LOGINFO("Journal found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); + journal_ = existing; + } + if (auto existing = registry->get< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id)); !existing) { + LOGINFO("No index found in registry for replica {}", boost::uuids::to_string(ep_.id)); + registry->put< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id), index_); + } else { + LOGINFO("Index found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); + index_ = existing; + } +} + RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path) : MemCraftReplica{std::move(ep), page_size, nullptr}, - max_tx_{max_tx} { + max_tx_{max_tx}, + commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()} { replica_init(replica_config_path); raft_init(); + journal_init(); } RaftReplica::~RaftReplica() = default; @@ -258,8 +274,8 @@ std::vector< int64_t > RaftReplica::get_missing_slots(int64_t watermark) { std::lock_guard< std::mutex > g{mu_}; std::vector< int64_t > missing; int64_t expect = state_.commit_lsn + 1; - auto it = journal_.lower_bound(expect); // first present entry >= expect - for (; it != journal_.end() && it->first <= watermark; ++it) { + auto it = journal_->lower_bound(expect); // first present entry >= expect + for (; it != journal_->end() && it->first <= watermark; ++it) { for (; expect < it->first; ++expect) missing.push_back(expect); // gap before this entry expect = it->first + 1; @@ -479,7 +495,8 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const auto const partition_uuid = craft::to_uuid(partition_id); auto const& registry = registry_manager::instance(); // return success if the partition exists - if (auto p = registry->get< partition_peers_list_t >(partition_peers_list_key(partition_uuid)); p && !p->empty()) { + if (auto p = registry->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid)); + p && !p->empty()) { LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; } @@ -490,7 +507,7 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const if (r) { auto view = members | std::views::transform(&replica_endpoint::id); std::vector< boost::uuids::uuid > peer_uuids(view.begin(), view.end()); - registry->put< partition_peers_list_t >(partition_peers_list_key(partition_uuid), + registry->put< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid), std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { @@ -545,7 +562,7 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCom std::lock_guard< std::mutex > g{mu_}; apply_up_to(m.rs_commit_lsn); - rs_commit_lsn_.store(m.rs_commit_lsn, std::memory_order_relaxed); + state_.commit_lsn = m.rs_commit_lsn; LOGDEBUG("apply_sync[partition={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(partition_uuid), state_.commit_lsn, m.rs_commit_lsn); } diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 09e3a85..95571dc 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -70,9 +70,9 @@ class RaftReplica final : public MemCraftReplica { void internal_login(InternalLoginMsg m); void raft_init(); void replica_init(std::string const& replica_config_path); + void journal_init(); uint32_t max_tx_; - std::atomic< int64_t > rs_commit_lsn_{-1}; std::mutex login_mu_; std::condition_variable login_cv_; bool login_done_{false}; diff --git a/src/registry_mgr.hpp b/src/registry_mgr.hpp index fbaf514..0832632 100644 --- a/src/registry_mgr.hpp +++ b/src/registry_mgr.hpp @@ -9,8 +9,6 @@ namespace craft { class registry_manager { public: - static std::shared_ptr< registry_manager > instance(); - template void put(std::string const& key, std::shared_ptr value) { std::lock_guard lock(component_mutex_); @@ -28,8 +26,6 @@ class registry_manager { private: - registry_manager() = default; - mutable std::mutex component_mutex_; std::unordered_map component_store_; }; From a06aab1da4c3cbfe9dc81672a236343198e0518f Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 19 Aug 2026 05:49:57 -0700 Subject: [PATCH 17/24] Move replica journal, index volume and peer info to registry --- src/net/tcp_server.cpp | 13 +++-- src/net/tcp_server.hpp | 7 ++- src/raft/raft_replica.cpp | 86 +++++++++++++++++-------------- src/raft/raft_replica.hpp | 8 ++- src/raft/raft_service.cpp | 22 +++----- src/raft/raft_service.hpp | 8 ++- src/registry_mgr.cpp | 10 ---- tools/craft_reference_tcp_srv.cpp | 4 +- 8 files changed, 83 insertions(+), 75 deletions(-) delete mode 100644 src/registry_mgr.cpp diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index c789779..2fa31aa 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -26,7 +26,7 @@ #include // server-side r/w trace (base module; visible with -v trace / when a consumer inits logging) #include "raft/raft_replica.hpp" // the full RaftReplica (+ sisl::sg_list via sisl/fds/buffer.hpp) -#include "raft/raft_service.hpp" +#include "registry_mgr.hpp" #include // to_wire_status (the shared wire <-> craft_error bridge) #include "helper.hpp" @@ -39,10 +39,15 @@ std::span< uint8_t const > as_bytes(T const& v) { } } // namespace -craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file) : geo_{std::move(geo)} { +craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file, + std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service) : + geo_{std::move(geo)}, + registry_mgr_{std::move(registry_mgr)}, + raft_enabled_{init_raft_service} { auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); - replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx, server_config_file); + replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx, server_config_file, + registry_mgr_, init_raft_service); } craft_tcp_server::~craft_tcp_server() = default; @@ -167,7 +172,7 @@ void craft_tcp_server::on_helo(craft_conn& conn, wire::message const& req) { auto const hr = wire::decode< wire::helo_req >(req.op_header); wire::status code = wire::status::ok; - if (!raft_service::instance()->is_raft_enabled()) { + if (!raft_enabled_) { // no raft, follow fake cold path auto result = replica_->srv_establish(hr.volume_id, hr.client_token, session_term_); if (!result) { diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index c1a44fd..cbe7f0b 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -35,6 +35,7 @@ namespace craft { struct replica_endpoint; class RaftReplica; // the server's state backing (pimpl; included only in craft_tcp_server.cpp) +class registry_manager; } // namespace craft namespace craft::net { @@ -49,7 +50,9 @@ struct server_geometry { class craft_tcp_server { public: - explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}); + explicit craft_tcp_server(server_geometry geo, std::string const& server_config_file = {}, + std::shared_ptr< registry_manager > registry_mgr = nullptr, + bool init_raft_service = false); ~craft_tcp_server(); craft_tcp_server(craft_tcp_server&&) = default; craft_tcp_server& operator=(craft_tcp_server&&) = default; @@ -70,6 +73,8 @@ class craft_tcp_server { uint64_t next_term_ = 0; // monotonic term source; a fresh LOGIN takes ++next_term_ uint64_t session_term_ = 0; // the current session's term, stamped on every IO bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced + std::shared_ptr< registry_manager > registry_mgr_ = nullptr; // the registry manager instance + bool raft_enabled_ = false; void on_login(craft_conn&, wire::message const&); void on_helo(craft_conn&, wire::message const&); diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 8c5f51e..354f59e 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -65,16 +65,16 @@ struct replica_info { std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; }; -std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id) { +std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id, std::shared_ptr< registry_manager > registry_mgr) { partition_peers_list_t peers; - auto registry = registry_manager::instance(); - if (auto peers_ptr = registry->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_id)); + if (auto peers_ptr = + registry_mgr->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_id)); peers_ptr) { peers = *peers_ptr; } std::vector< replica_info > replica_members; for (auto const& peer_id : peers) { - if (auto rinfo = registry->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, peer_id)); rinfo) { + if (auto rinfo = registry_mgr->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, peer_id)); rinfo) { replica_members.emplace_back(*rinfo); } else { LOGWARN("Peer {} not found in registry", boost::uuids::to_string(peer_id)); @@ -123,8 +123,12 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { LOGWARN("No replica config path provided, skipping replica initialization"); return; } - auto registry = registry_manager::instance(); - if (auto const rinfo = registry->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, ep_.id)); rinfo) { + if (!registry_mgr_) { + LOGWARN("Registry manager is not initialized, cannot initialize replica"); + return; + } + + if (auto const rinfo = registry_mgr_->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, ep_.id)); rinfo) { // recovery from registry return; } @@ -155,20 +159,17 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), m.at("tcp_port").get< uint16_t >(), id), }); - registry->put< raft_peer_t >(raft_service::peer_id_key(id), - std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); - registry->put< replica_info >(REGISTRY_KEY(replica_info_key_prefix, r->id), std::move(r)); + registry_mgr_->put< raft_peer_t >(raft_service::peer_id_key(id), + std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); + registry_mgr_->put< replica_info >(REGISTRY_KEY(replica_info_key_prefix, r->id), std::move(r)); } } void RaftReplica::raft_init() { - auto raft_inst = raft_service::instance(); - if (raft_inst->is_raft_enabled()) { + if (!raft_service_) { LOGINFO("RAFT already initialized for replica {}", boost::uuids::to_string(ep_.id)); return; } - raft_inst->start_raft_service(ep_.id); - if (!raft_inst->is_raft_enabled()) { return; } auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& partition_uuid_str) { auto const partition_uuid = boost::uuids::string_generator()(partition_uuid_str); auto const op_val = j.at("op").get< int >(); @@ -206,23 +207,26 @@ void RaftReplica::raft_init() { } }; - raft_inst->add_commit_cb(std::move(commit_cb)); + raft_service_->add_commit_cb(std::move(commit_cb)); LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), page_size_); } void RaftReplica::journal_init() { - auto registry = registry_manager::instance(); - if (auto existing = registry->get< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id)); !existing) { + if (!registry_mgr_) { + LOGWARN("Registry manager is not initialized, cannot initialize journal and index"); + return; + } + if (auto existing = registry_mgr_->get< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id)); !existing) { LOGINFO("No journal found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry->put< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id), journal_); + registry_mgr_->put< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id), journal_); } else { LOGINFO("Journal found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); journal_ = existing; } - if (auto existing = registry->get< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id)); !existing) { + if (auto existing = registry_mgr_->get< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id)); !existing) { LOGINFO("No index found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry->put< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id), index_); + registry_mgr_->put< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id), index_); } else { LOGINFO("Index found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); index_ = existing; @@ -230,10 +234,13 @@ void RaftReplica::journal_init() { } RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, - std::string const& replica_config_path) : + std::string const& replica_config_path, std::shared_ptr< registry_manager > registry_mgr, + bool init_raft_service) : MemCraftReplica{std::move(ep), page_size, nullptr}, max_tx_{max_tx}, - commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()} { + commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()}, + registry_mgr_{std::move(registry_mgr)}, + raft_service_{init_raft_service ? std::make_shared< raft_service >(ep_.id, registry_mgr_) : nullptr} { replica_init(replica_config_path); raft_init(); journal_init(); @@ -290,7 +297,7 @@ std::vector< int64_t > RaftReplica::get_missing_slots(int64_t watermark) { std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boost::uuids::uuid const& partition_uuid, int64_t watermark, uint64_t client_token, uint64_t term) { - auto const peers = peer_list(partition_uuid); + auto const peers = peer_list(partition_uuid, registry_mgr_); if (peers.empty()) { LOGERROR("resolve_and_apply[partition={}]: no peers found in registry", boost::uuids::to_string(partition_uuid)); @@ -363,10 +370,10 @@ result< void > RaftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& partiti boost::uuids::to_string(partition_uuid), stalled_lsn); return std::unexpected(make_error_condition(craft_error::INTERNAL)); } - if (auto const r = raft_service::instance()->propose(partition_uuid, - SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, - .client_token = client_token, - .empty_slots = std::move(empty_slots)}); + if (auto const r = raft_service_->propose(partition_uuid, + SyncRSCommitLSNMsg{.rs_commit_lsn = rs_commit_lsn, + .client_token = client_token, + .empty_slots = std::move(empty_slots)}); !r) { // TODO: any cleanup required? LOGERROR("sync_rs_commit_lsn[partition={}]: propose(SyncRSCommitLSN={}) failed: {}", @@ -380,14 +387,13 @@ result< void > RaftReplica::sync_rs_commit_lsn(boost::uuids::uuid const& partiti result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& partition_id, uint64_t client_token, uint64_t term) { - auto raft_service_inst = raft_service::instance(); // A note on dlsn: The login WATERMARK: the last dLSN already durable (-1 on a fresh replica), NOT the next one // to use -- the client derives next_dlsn_ = dlsn + 1 itself. This used to send last_append_lsn + 1, // which skipped slot 0 on a fresh cluster: every replica was then permanently Missing dLSN 0, // apply_up_to() stalled there forever, and commit_lsn pinned at -1 -- so no journal reclaimed and every read // walked the whole tail. See wire.hpp. - if (!raft_service_inst->is_raft_enabled()) { + if (!raft_service_) { // return cold path if raft service has not started std::lock_guard< std::mutex > g{mu_}; state_.term = term; @@ -403,9 +409,9 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& auto partition_uuid = craft::to_uuid(partition_id); LOGINFO("Login request, vol id {}, token {}, new session {}", boost::uuids::to_string(partition_uuid), client_token, term); - if (!raft_service_inst->is_leader(partition_uuid)) { + if (!raft_service_->is_leader(partition_uuid)) { LOGERROR("current replica not a raft leader"); - return LoginResult{{}, -1, 0, 0, 0, raft_service_inst->leader_id(partition_uuid)}; + return LoginResult{{}, -1, 0, 0, 0, raft_service_->leader_id(partition_uuid)}; } // 1.2 collect replica LSN state (non-RAFT broadcast) @@ -417,7 +423,7 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); } - auto const members = peer_list(partition_uuid); + auto const members = peer_list(partition_uuid, registry_mgr_); LOGDEBUG("apply_login[partition={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(partition_uuid), members.size()); for (auto const& m : members) { @@ -455,7 +461,7 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& std::lock_guard< std::mutex > lk(login_mu_); login_done_ = false; } - if (auto const r = raft_service_inst->propose( + if (auto const r = raft_service_->propose( partition_uuid, InternalLoginMsg{.client_token = client_token, .term = term, .rs_commit_lsn = rs_commit_lsn}); !r) { @@ -493,9 +499,8 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const& partition_id, std::vector< replica_endpoint > const& members) { auto const partition_uuid = craft::to_uuid(partition_id); - auto const& registry = registry_manager::instance(); // return success if the partition exists - if (auto p = registry->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid)); + if (auto p = registry_mgr_->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid)); p && !p->empty()) { LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; @@ -503,12 +508,17 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const LOGINFO("srv_create_partition[partition={}]: creating with {} member(s)", boost::uuids::to_string(partition_uuid), members.size()); - auto const r = raft_service::instance()->srv_create_partition(partition_uuid, members); + if (!raft_service_) { + LOGERROR("srv_create_partition[partition={}]: raft service not initialized", + boost::uuids::to_string(partition_uuid)); + return std::unexpected(make_error_condition(craft_error::INTERNAL)); + } + auto const r = raft_service_->srv_create_partition(partition_uuid, members); if (r) { auto view = members | std::views::transform(&replica_endpoint::id); std::vector< boost::uuids::uuid > peer_uuids(view.begin(), view.end()); - registry->put< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid), - std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); + registry_mgr_->put< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid), + std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { LOGERROR("srv_create_partition[partition={}]: FAILED: {}", boost::uuids::to_string(partition_uuid), @@ -531,7 +541,7 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCom } auto missing = get_missing_slots(m.rs_commit_lsn); - auto const peers = peer_list(partition_uuid); + auto const peers = peer_list(partition_uuid, registry_mgr_); if (peers.empty()) { LOGERROR("apply_sync[partition={}]: no peers found in registry", boost::uuids::to_string(partition_uuid)); } diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 95571dc..02abd3f 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -34,12 +34,16 @@ struct session_info { uint64_t client_token; }; +class registry_manager; +class raft_service; + struct SyncRSCommitLSNMsg; struct InternalLoginMsg; class RaftReplica final : public MemCraftReplica { public: - RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path); + RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path, + std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service = false); ~RaftReplica(); @@ -79,6 +83,8 @@ class RaftReplica final : public MemCraftReplica { class RaftCommitWorker; std::unique_ptr< RaftCommitWorker > commit_worker_; + std::shared_ptr< registry_manager > registry_mgr_; + std::shared_ptr< raft_service > raft_service_; }; } // namespace craft diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 0f35ac7..a61773d 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -23,24 +23,18 @@ static nuraft::ptr< nuraft::buffer > create_message(nlohmann::json const& j_obj) return buf; } -std::shared_ptr< raft_service > raft_service::instance() { - static std::shared_ptr< raft_service > instance{new raft_service()}; - return instance; -} - -consensus_handle raft_service::get_consensus() { return consensus_; } - -void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { - // raft global manager for commits - nuraft::nuraft_global_mgr::init(); +raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::shared_ptr< registry_manager > registry_mgr) : + server_uuid_(server_uuid), + registry_mgr_(std::move(registry_mgr)) { + nuraft::nuraft_global_mgr::init(); // raft global manager for commits std::call_once(raft_started_, [&] { - auto my_info = registry_manager::instance()->get< raft_peer_t >(raft_service::peer_id_key(server_uuid)); + auto my_info = registry_mgr_->get< raft_peer_t >(peer_id_key(server_uuid_)); if (!my_info) { LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid); return; } auto params = nuraft_mesg::manager::params{ - .server_uuid_ = server_uuid, + .server_uuid_ = server_uuid_, .mesg_port_ = my_info->second, .default_group_type_ = default_group_type_, }; @@ -51,7 +45,6 @@ void raft_service::start_raft_service(boost::uuids::uuid const& server_uuid) { .with_hb_interval(250) .with_rpc_failure_backoff(250); consensus_->register_mgr_type(default_group_type_, raft_params); - server_uuid_ = server_uuid; LOGINFO("Initialized raft_service for {} with raft consensus manager, port {}", params.server_uuid_, params.mesg_port_); }); @@ -95,8 +88,7 @@ result< void > raft_service::srv_create_partition(boost::uuids::uuid const& grou } std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { - if (auto peer_addr = registry_manager::instance()->get< raft_peer_t >(raft_service::peer_id_key(peer_id)); - peer_addr) { + if (auto peer_addr = registry_mgr_->get< raft_peer_t >(peer_id_key(peer_id)); peer_addr) { return fmt::format("{}:{}", peer_addr->first, peer_addr->second); } LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 015265e..b59c5df 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -19,6 +19,7 @@ using raft_peer_t = std::pair< std::string, uint16_t >; // namespace craft { class raft_state_mgr; +class registry_manager; // Process-wide bridge for the peer-to-peer consensus engine used by the TCP server and replica-side code. // The concrete nuraft_mesg::manager instance is installed once and then shared by anyone that needs to create @@ -29,12 +30,8 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab inline static const std::string peer_id_key(boost::uuids::uuid const& peer_id) { return fmt::format("raft_peer_{}", boost::uuids::to_string(peer_id)); } - + raft_service(boost::uuids::uuid const& server_uuid, std::shared_ptr< registry_manager > registry_mgr); virtual ~raft_service(); - static std::shared_ptr< raft_service > instance(); - bool is_raft_enabled() { return consensus_ != nullptr; } - consensus_handle get_consensus(); - void start_raft_service(boost::uuids::uuid const& server_uuid); result< void > srv_create_partition(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members); void add_commit_cb(raft_commit_cb_t cb); @@ -59,6 +56,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab std::shared_mutex mu_; std::map< nuraft_mesg::group_id_t, std::shared_ptr< raft_state_mgr > > state_mgrs_; raft_commit_cb_t commit_cb_; + std::shared_ptr< registry_manager > registry_mgr_; result< std::shared_ptr< raft_state_mgr > > get_state_mgr(nuraft_mesg::group_id_t const& group_id); void add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr); diff --git a/src/registry_mgr.cpp b/src/registry_mgr.cpp deleted file mode 100644 index 8171115..0000000 --- a/src/registry_mgr.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "registry_mgr.hpp" - -namespace craft { - -std::shared_ptr< registry_manager > registry_manager::instance() { - static std::shared_ptr< registry_manager > inst{new registry_manager()}; - return inst; -} - -} \ No newline at end of file diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index 91842e1..a8c0c47 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -40,6 +40,7 @@ #include #include "net/tcp_server.hpp" #include +#include "registry_mgr.hpp" // A 0 default means "unset" -> resolved in code (capacity to 1 GiB, max_tx to the single-sourced wire default), so // no size magic number is duplicated in a CLI string. @@ -116,7 +117,8 @@ int main(int argc, char** argv) { self.addr = fmt::format("127.0.0.1:{}", port); auto geo = craft::net::server_geometry{ .capacity = capacity, .lba_size = lba_size, .max_tx = max_tx, .member = std::move(self)}; - craft::net::craft_tcp_server server{std::move(geo), server_config_file}; + craft::net::craft_tcp_server server{std::move(geo), server_config_file, + std::make_shared< craft::registry_manager >(), true /*init_raft_service*/}; // sigaction WITHOUT SA_RESTART: glibc's signal() sets SA_RESTART, which auto-restarts the blocking accept() // after the handler runs, so the loop would never re-check g_stop and Ctrl-C could not stop the server. With From c89ed0020421d31f95df6d460aa9c0bc47c10f8d Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Wed, 19 Aug 2026 07:39:00 -0700 Subject: [PATCH 18/24] add raft persistence to registry manager --- src/helper.hpp | 4 ++ src/raft/raft_replica.cpp | 22 +++++------ src/raft/raft_service.cpp | 36 +++++++----------- src/raft/raft_service.hpp | 4 -- src/raft/raft_state_machine.hpp | 8 +++- src/raft/raft_state_manager.cpp | 65 +++++++++++++++++++++++---------- src/raft/raft_state_manager.hpp | 4 +- 7 files changed, 82 insertions(+), 61 deletions(-) diff --git a/src/helper.hpp b/src/helper.hpp index d645346..7012125 100644 --- a/src/helper.hpp +++ b/src/helper.hpp @@ -47,4 +47,8 @@ inline std::error_condition jsonObjectFromFile(std::string const& filename, json return std::error_condition(); } +inline std::string registry_key(std::string const& prefix, boost::uuids::uuid const& id) { + return fmt::format("{}_{}", prefix, boost::uuids::to_string(id)); +} + } // namespace craft diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 354f59e..f37e2e0 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -32,8 +32,6 @@ #include -#define REGISTRY_KEY(prefix, id) fmt::format("{}_{}", prefix, boost::uuids::to_string(id)) - namespace craft { namespace { @@ -68,13 +66,13 @@ struct replica_info { std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id, std::shared_ptr< registry_manager > registry_mgr) { partition_peers_list_t peers; if (auto peers_ptr = - registry_mgr->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_id)); + registry_mgr->get< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_id)); peers_ptr) { peers = *peers_ptr; } std::vector< replica_info > replica_members; for (auto const& peer_id : peers) { - if (auto rinfo = registry_mgr->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, peer_id)); rinfo) { + if (auto rinfo = registry_mgr->get< replica_info >(registry_key(replica_info_key_prefix, peer_id)); rinfo) { replica_members.emplace_back(*rinfo); } else { LOGWARN("Peer {} not found in registry", boost::uuids::to_string(peer_id)); @@ -128,7 +126,7 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { return; } - if (auto const rinfo = registry_mgr_->get< replica_info >(REGISTRY_KEY(replica_info_key_prefix, ep_.id)); rinfo) { + if (auto const rinfo = registry_mgr_->get< replica_info >(registry_key(replica_info_key_prefix, ep_.id)); rinfo) { // recovery from registry return; } @@ -161,7 +159,7 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { }); registry_mgr_->put< raft_peer_t >(raft_service::peer_id_key(id), std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); - registry_mgr_->put< replica_info >(REGISTRY_KEY(replica_info_key_prefix, r->id), std::move(r)); + registry_mgr_->put< replica_info >(registry_key(replica_info_key_prefix, r->id), std::move(r)); } } @@ -217,16 +215,16 @@ void RaftReplica::journal_init() { LOGWARN("Registry manager is not initialized, cannot initialize journal and index"); return; } - if (auto existing = registry_mgr_->get< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id)); !existing) { + if (auto existing = registry_mgr_->get< journal_t >(registry_key(journal_key_prefix, ep_.id)); !existing) { LOGINFO("No journal found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry_mgr_->put< journal_t >(REGISTRY_KEY(journal_key_prefix, ep_.id), journal_); + registry_mgr_->put< journal_t >(registry_key(journal_key_prefix, ep_.id), journal_); } else { LOGINFO("Journal found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); journal_ = existing; } - if (auto existing = registry_mgr_->get< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id)); !existing) { + if (auto existing = registry_mgr_->get< index_t >(registry_key(index_key_prefix, ep_.id)); !existing) { LOGINFO("No index found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry_mgr_->put< index_t >(REGISTRY_KEY(index_key_prefix, ep_.id), index_); + registry_mgr_->put< index_t >(registry_key(index_key_prefix, ep_.id), index_); } else { LOGINFO("Index found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); index_ = existing; @@ -500,7 +498,7 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const std::vector< replica_endpoint > const& members) { auto const partition_uuid = craft::to_uuid(partition_id); // return success if the partition exists - if (auto p = registry_mgr_->get< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid)); + if (auto p = registry_mgr_->get< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid)); p && !p->empty()) { LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; @@ -517,7 +515,7 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const if (r) { auto view = members | std::views::transform(&replica_endpoint::id); std::vector< boost::uuids::uuid > peer_uuids(view.begin(), view.end()); - registry_mgr_->put< partition_peers_list_t >(REGISTRY_KEY(partition_info_key_prefix, partition_uuid), + registry_mgr_->put< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid), std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index a61773d..10ffc38 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -9,6 +9,10 @@ #include #include +namespace { +std::string const state_mgr_key_prefix{"raft_state_mgr"}; +} + namespace craft { namespace { @@ -97,30 +101,18 @@ std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, nuraft_mesg::group_id_t const& group_id) { - auto result = get_state_mgr(group_id); + auto result = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (result) { LOGINFO("RAFT state manager for group_id={} already exists, returning existing instance", boost::uuids::to_string(group_id)); - return result.value(); + return result; } LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); - auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_); - add_state_mgr(group_id, mgr); + auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_, registry_mgr_); + registry_mgr_->put< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id), mgr); return mgr; } -result< std::shared_ptr< raft_state_mgr > > raft_service::get_state_mgr(nuraft_mesg::group_id_t const& group_id) { - std::shared_lock< std::shared_mutex > g{mu_}; - auto const it = state_mgrs_.find(group_id); - if (it == state_mgrs_.end()) return fail(craft_error::INTERNAL); - return it->second; -} - -void raft_service::add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr) { - std::lock_guard< std::shared_mutex > g{mu_}; - state_mgrs_[group_id] = std::move(mgr); -} - void raft_service::add_commit_cb(raft_commit_cb_t cb) { // we expect that this is called only once if (!consensus_) { @@ -135,12 +127,12 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { LOGERROR("Raft not enabled!"); return false; } - auto const state_mgr = get_state_mgr(group_id); + auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return false; } - auto* raft_ctx = state_mgr.value()->repl_ctx(); + auto* raft_ctx = state_mgr->repl_ctx(); return raft_ctx && raft_ctx->is_raft_leader(); } @@ -149,12 +141,12 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGERROR("Raft not enabled!"); return {}; } - auto const state_mgr = get_state_mgr(group_id); + auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return {}; } - auto* raft_ctx = state_mgr.value()->repl_ctx(); + auto* raft_ctx = state_mgr->repl_ctx(); if (!raft_ctx) { LOGWARN("No leader for the raft group {}", group_id); return {}; @@ -168,12 +160,12 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr template < typename MsgT > result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT const& payload) { - auto const state_mgr = get_state_mgr(group_id); + auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return std::unexpected(make_error_condition(craft_error::INTERNAL)); } - auto* raft_ctx = state_mgr.value()->repl_ctx(); + auto* raft_ctx = state_mgr->repl_ctx(); if (!raft_ctx) { LOGWARN("RAFT state manager context for group_id={} not found", boost::uuids::to_string(group_id)); return std::unexpected(make_error_condition(craft_error::INTERNAL)); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index b59c5df..3f0fe5f 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -54,12 +54,8 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab std::once_flag raft_started_; nlohmann::json server_config_; std::shared_mutex mu_; - std::map< nuraft_mesg::group_id_t, std::shared_ptr< raft_state_mgr > > state_mgrs_; raft_commit_cb_t commit_cb_; std::shared_ptr< registry_manager > registry_mgr_; - - result< std::shared_ptr< raft_state_mgr > > get_state_mgr(nuraft_mesg::group_id_t const& group_id); - void add_state_mgr(nuraft_mesg::group_id_t const& group_id, std::shared_ptr< raft_state_mgr > mgr); }; } // namespace craft diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 68e925f..1d32080 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -2,6 +2,7 @@ #include #include +#include "registry_mgr.hpp" namespace craft { @@ -59,10 +60,12 @@ using raft_commit_cb_t = std::function< void(uint64_t log_idx, nlohmann::json co class echo_state_machine : public nuraft::state_machine { public: - echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id) : + echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id, + std::shared_ptr< registry_manager > registry_mgr) : commit_cb_{cb}, group_id_{group_id}, - last_commit_idx_(0) {} + last_commit_idx_(0), + registry_mgr_(std::move(registry_mgr)) {} virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) override { nlohmann::json j; @@ -105,5 +108,6 @@ class echo_state_machine : public nuraft::state_machine { raft_commit_cb_t commit_cb_; std::string group_id_; nuraft::ulong last_commit_idx_; + std::shared_ptr< registry_manager > registry_mgr_; }; } // namespace craft diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index b525270..1582a3d 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -4,19 +4,36 @@ #include #include +#include #include "in_memory_log_store.hpp" #include "helper.hpp" +namespace { +std::string const log_store_key_prefix{"raft_log_store"}; +} + namespace craft { -std::error_condition loadConfigFile(json& config_map, std::string const& _group_id, int32_t const _srv_id) { - auto const config_file = fmt::format(FMT_STRING("{}_s{}/config.json"), _group_id, _srv_id); - return jsonObjectFromFile(config_file, config_map); +std::error_condition loadConfigFile(json& config_map, std::string const& _group_id, int32_t const _srv_id, std::shared_ptr< registry_manager >& _registry_mgr) { + auto const registry_key = fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id); + auto json_ptr = _registry_mgr->get< json >(registry_key); + if (!json_ptr) { + LOGWARN("Could not load config for group_id={} server_id={}", _group_id, _srv_id); + return std::make_error_condition(std::errc::no_such_file_or_directory); + } + config_map = *json_ptr; + return {}; } -std::error_condition loadStateFile(json& state_map, std::string const& _group_id, int32_t const _srv_id) { - auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); - return jsonObjectFromFile(state_file, state_map); +std::error_condition loadStateFile(json& state_map, std::string const& _group_id, int32_t const _srv_id, std::shared_ptr< registry_manager >& _registry_mgr) { + auto const registry_key = fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id); + auto json_ptr = _registry_mgr->get< json >(registry_key); + if (!json_ptr) { + LOGWARN("Could not load state for group_id={} server_id={}", _group_id, _srv_id); + return std::make_error_condition(std::errc::no_such_file_or_directory); + } + state_map = *json_ptr; + return {}; } nuraft::ptr< nuraft::srv_config > fromServer(json const& server) { @@ -59,31 +76,43 @@ nuraft::ptr< nuraft::cluster_config > fromClusterConfig(json const& cluster_conf } raft_state_mgr::raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, - nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb) : + nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb, + std::shared_ptr< registry_manager > registry_mgr) : nuraft_mesg::mesg_state_mgr(), _srv_id(srv_id), _srv_addr(to_string(srv_addr)), _group_id(to_string(group_id)), - _commit_cb(std::move(cb)) {} + _commit_cb(std::move(cb)), + _registry_mgr(std::move(registry_mgr)) {} nuraft::ptr< nuraft::cluster_config > raft_state_mgr::load_config() { LOGDEBUG("Loading config for [{}]", _group_id); json config_map; - if (auto err = loadConfigFile(config_map, _group_id, _srv_id); !err) { return fromClusterConfig(config_map); } + if (auto err = loadConfigFile(config_map, _group_id, _srv_id, _registry_mgr); !err) { return fromClusterConfig(config_map); } auto conf = nuraft::cs_new< nuraft::cluster_config >(); conf->get_servers().push_back(nuraft::cs_new< nuraft::srv_config >(_srv_id, _srv_addr)); return conf; } nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { - return nuraft::cs_new< nuraft::inmem_log_store >(); + auto log_store = _registry_mgr->get< nuraft::inmem_log_store >( + registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id))); + if (log_store) { + LOGDEBUG("RAFT log store for group_id={} already exists, returning existing instance", _group_id); + return log_store; + } + LOGDEBUG("Creating RAFT log store for group_id={}", _group_id); + log_store = std::make_shared< nuraft::inmem_log_store >(); + _registry_mgr->put< nuraft::inmem_log_store >( + registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id)), log_store); + return log_store; } nuraft::ptr< nuraft::srv_state > raft_state_mgr::read_state() { LOGDEBUG("Loading state for server: {}", _srv_id); json state_map; auto state = nuraft::cs_new< nuraft::srv_state >(); - if (auto err = loadStateFile(state_map, _group_id, _srv_id); !err) { + if (auto err = loadStateFile(state_map, _group_id, _srv_id, _registry_mgr); !err) { try { state->set_term(static_cast< uint64_t >(state_map["term"])); state->set_voted_for(static_cast< int >(state_map["voted_for"])); @@ -99,26 +128,22 @@ void raft_state_mgr::save_config(const nuraft::cluster_config& config) { {"eventual_consistency", config.is_async_replication()}, {"user_ctx", config.get_user_ctx()}, {"servers", toServers(const_cast< nuraft::cluster_config& >(config).get_servers())}}; - try { - std::ofstream ostrm(config_file, std::ios::binary); - if (ostrm.is_open()) { ostrm << json_obj; } - } catch (std::exception& e) { LOGERROR("Failed to write config values: {}", e.what()); } + _registry_mgr->put< json >(fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } void raft_state_mgr::save_state(const nuraft::srv_state& state) { auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); auto json_obj = json{{"term", state.get_term()}, {"voted_for", state.get_voted_for()}}; - try { - std::ofstream ostrm(state_file, std::ios::binary); - if (ostrm.is_open()) { ostrm << json_obj; } - } catch (std::exception& e) { LOGERROR("Failed to write config values: {}", e.what()); } + _registry_mgr->put< json >(fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } uint32_t raft_state_mgr::get_logstore_id() const { return 0; } std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { - return std::make_shared< echo_state_machine >(_commit_cb, _group_id); + return std::make_shared< echo_state_machine >(_commit_cb, _group_id, _registry_mgr); } void raft_state_mgr::permanent_destroy() {} diff --git a/src/raft/raft_state_manager.hpp b/src/raft/raft_state_manager.hpp index c9b1fca..373151a 100644 --- a/src/raft/raft_state_manager.hpp +++ b/src/raft/raft_state_manager.hpp @@ -7,11 +7,12 @@ namespace craft { class raft_service; +class registry_manager; class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { public: raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id, - raft_commit_cb_t cb); + raft_commit_cb_t cb, std::shared_ptr< registry_manager > registry_mgr); nuraft::ptr< nuraft::cluster_config > load_config() override; void save_config(const nuraft::cluster_config& config) override; @@ -32,6 +33,7 @@ class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { std::string const _srv_addr; std::string const _group_id; raft_commit_cb_t _commit_cb; + std::shared_ptr< registry_manager > _registry_mgr; }; } // namespace craft From 3bd3312c8f77cc6bb2697da991b46df9f87027cd Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Thu, 20 Aug 2026 13:56:30 +0530 Subject: [PATCH 19/24] use weak_ptr for registry_mgr to avoid cyclic destruction issue --- src/raft/raft_replica.cpp | 37 ++++++++++++++++++--------------- src/raft/raft_replica.hpp | 4 ++-- src/raft/raft_service.cpp | 17 ++++++++------- src/raft/raft_service.hpp | 4 ++-- src/raft/raft_state_machine.hpp | 4 ++-- src/raft/raft_state_manager.cpp | 23 +++++++++++--------- src/raft/raft_state_manager.hpp | 4 ++-- src/registry_mgr.hpp | 7 +++++++ 8 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index f37e2e0..5eef678 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -121,12 +121,13 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { LOGWARN("No replica config path provided, skipping replica initialization"); return; } - if (!registry_mgr_) { + auto reg = registry_mgr_.lock(); + if (!reg) { LOGWARN("Registry manager is not initialized, cannot initialize replica"); return; } - if (auto const rinfo = registry_mgr_->get< replica_info >(registry_key(replica_info_key_prefix, ep_.id)); rinfo) { + if (auto const rinfo = reg->get< replica_info >(registry_key(replica_info_key_prefix, ep_.id)); rinfo) { // recovery from registry return; } @@ -157,9 +158,9 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { : std::make_shared< net::CraftTcpPeer >(m.at("host").get< std::string >(), m.at("tcp_port").get< uint16_t >(), id), }); - registry_mgr_->put< raft_peer_t >(raft_service::peer_id_key(id), - std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); - registry_mgr_->put< replica_info >(registry_key(replica_info_key_prefix, r->id), std::move(r)); + reg->put< raft_peer_t >(raft_service::peer_id_key(id), + std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); + reg->put< replica_info >(registry_key(replica_info_key_prefix, r->id), std::move(r)); } } @@ -211,20 +212,21 @@ void RaftReplica::raft_init() { } void RaftReplica::journal_init() { - if (!registry_mgr_) { + auto reg = registry_mgr_.lock(); + if (!reg) { LOGWARN("Registry manager is not initialized, cannot initialize journal and index"); return; } - if (auto existing = registry_mgr_->get< journal_t >(registry_key(journal_key_prefix, ep_.id)); !existing) { + if (auto existing = reg->get< journal_t >(registry_key(journal_key_prefix, ep_.id)); !existing) { LOGINFO("No journal found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry_mgr_->put< journal_t >(registry_key(journal_key_prefix, ep_.id), journal_); + reg->put< journal_t >(registry_key(journal_key_prefix, ep_.id), journal_); } else { LOGINFO("Journal found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); journal_ = existing; } - if (auto existing = registry_mgr_->get< index_t >(registry_key(index_key_prefix, ep_.id)); !existing) { + if (auto existing = reg->get< index_t >(registry_key(index_key_prefix, ep_.id)); !existing) { LOGINFO("No index found in registry for replica {}", boost::uuids::to_string(ep_.id)); - registry_mgr_->put< index_t >(registry_key(index_key_prefix, ep_.id), index_); + reg->put< index_t >(registry_key(index_key_prefix, ep_.id), index_); } else { LOGINFO("Index found in registry for replica {}, loading into memory", boost::uuids::to_string(ep_.id)); index_ = existing; @@ -232,7 +234,7 @@ void RaftReplica::journal_init() { } RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, - std::string const& replica_config_path, std::shared_ptr< registry_manager > registry_mgr, + std::string const& replica_config_path, std::weak_ptr< registry_manager > registry_mgr, bool init_raft_service) : MemCraftReplica{std::move(ep), page_size, nullptr}, max_tx_{max_tx}, @@ -295,7 +297,7 @@ std::vector< int64_t > RaftReplica::get_missing_slots(int64_t watermark) { std::pair< std::vector< int64_t >, int64_t > RaftReplica::resolve_and_apply(boost::uuids::uuid const& partition_uuid, int64_t watermark, uint64_t client_token, uint64_t term) { - auto const peers = peer_list(partition_uuid, registry_mgr_); + auto const peers = peer_list(partition_uuid, lock_registry(registry_mgr_)); if (peers.empty()) { LOGERROR("resolve_and_apply[partition={}]: no peers found in registry", boost::uuids::to_string(partition_uuid)); @@ -421,7 +423,7 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& peer_resp.emplace_back(lsn_pair{state_.commit_lsn, state_.last_append_lsn}); } - auto const members = peer_list(partition_uuid, registry_mgr_); + auto const members = peer_list(partition_uuid, lock_registry(registry_mgr_)); LOGDEBUG("apply_login[partition={}]: polling {} member(s) for GetRSCommitLSN", boost::uuids::to_string(partition_uuid), members.size()); for (auto const& m : members) { @@ -498,7 +500,8 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const std::vector< replica_endpoint > const& members) { auto const partition_uuid = craft::to_uuid(partition_id); // return success if the partition exists - if (auto p = registry_mgr_->get< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid)); + auto reg = lock_registry(registry_mgr_); + if (auto p = reg->get< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid)); p && !p->empty()) { LOGINFO("Partition {} exists! Returning ok", boost::uuids::to_string(partition_uuid)); return {}; @@ -515,8 +518,8 @@ result< void > RaftReplica::srv_create_partition(std::array< uint8_t, 16 > const if (r) { auto view = members | std::views::transform(&replica_endpoint::id); std::vector< boost::uuids::uuid > peer_uuids(view.begin(), view.end()); - registry_mgr_->put< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid), - std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); + reg->put< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_uuid), + std::make_shared< partition_peers_list_t >(std::move(peer_uuids))); LOGINFO("srv_create_partition[partition={}]: SUCCESS", boost::uuids::to_string(partition_uuid)); } else { LOGERROR("srv_create_partition[partition={}]: FAILED: {}", boost::uuids::to_string(partition_uuid), @@ -539,7 +542,7 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCom } auto missing = get_missing_slots(m.rs_commit_lsn); - auto const peers = peer_list(partition_uuid, registry_mgr_); + auto const peers = peer_list(partition_uuid, lock_registry(registry_mgr_)); if (peers.empty()) { LOGERROR("apply_sync[partition={}]: no peers found in registry", boost::uuids::to_string(partition_uuid)); } diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 02abd3f..366a91b 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -43,7 +43,7 @@ struct InternalLoginMsg; class RaftReplica final : public MemCraftReplica { public: RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path, - std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service = false); + std::weak_ptr< registry_manager > registry_mgr, bool init_raft_service = false); ~RaftReplica(); @@ -83,7 +83,7 @@ class RaftReplica final : public MemCraftReplica { class RaftCommitWorker; std::unique_ptr< RaftCommitWorker > commit_worker_; - std::shared_ptr< registry_manager > registry_mgr_; + std::weak_ptr< registry_manager > registry_mgr_; std::shared_ptr< raft_service > raft_service_; }; diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 10ffc38..764e17e 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -27,12 +27,12 @@ static nuraft::ptr< nuraft::buffer > create_message(nlohmann::json const& j_obj) return buf; } -raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::shared_ptr< registry_manager > registry_mgr) : +raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr) : server_uuid_(server_uuid), registry_mgr_(std::move(registry_mgr)) { nuraft::nuraft_global_mgr::init(); // raft global manager for commits std::call_once(raft_started_, [&] { - auto my_info = registry_mgr_->get< raft_peer_t >(peer_id_key(server_uuid_)); + auto my_info = lock_registry(registry_mgr_)->get< raft_peer_t >(peer_id_key(server_uuid_)); if (!my_info) { LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid); return; @@ -92,7 +92,7 @@ result< void > raft_service::srv_create_partition(boost::uuids::uuid const& grou } std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { - if (auto peer_addr = registry_mgr_->get< raft_peer_t >(peer_id_key(peer_id)); peer_addr) { + if (auto peer_addr = lock_registry(registry_mgr_)->get< raft_peer_t >(peer_id_key(peer_id)); peer_addr) { return fmt::format("{}:{}", peer_addr->first, peer_addr->second); } LOGWARN("Peer {} not found in lookup map", boost::uuids::to_string(peer_id)); @@ -101,7 +101,8 @@ std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, nuraft_mesg::group_id_t const& group_id) { - auto result = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto reg = lock_registry(registry_mgr_); + auto result = reg->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (result) { LOGINFO("RAFT state manager for group_id={} already exists, returning existing instance", boost::uuids::to_string(group_id)); @@ -109,7 +110,7 @@ std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(in } LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_, registry_mgr_); - registry_mgr_->put< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id), mgr); + reg->put< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id), mgr); return mgr; } @@ -127,7 +128,7 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { LOGERROR("Raft not enabled!"); return false; } - auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return false; @@ -141,7 +142,7 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGERROR("Raft not enabled!"); return {}; } - auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return {}; @@ -160,7 +161,7 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr template < typename MsgT > result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT const& payload) { - auto const state_mgr = registry_mgr_->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return std::unexpected(make_error_condition(craft_error::INTERNAL)); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 3f0fe5f..30b7cd3 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -30,7 +30,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab inline static const std::string peer_id_key(boost::uuids::uuid const& peer_id) { return fmt::format("raft_peer_{}", boost::uuids::to_string(peer_id)); } - raft_service(boost::uuids::uuid const& server_uuid, std::shared_ptr< registry_manager > registry_mgr); + raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr); virtual ~raft_service(); result< void > srv_create_partition(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members); @@ -55,7 +55,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab nlohmann::json server_config_; std::shared_mutex mu_; raft_commit_cb_t commit_cb_; - std::shared_ptr< registry_manager > registry_mgr_; + std::weak_ptr< registry_manager > registry_mgr_; }; } // namespace craft diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 1d32080..88892c1 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -61,7 +61,7 @@ using raft_commit_cb_t = std::function< void(uint64_t log_idx, nlohmann::json co class echo_state_machine : public nuraft::state_machine { public: echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id, - std::shared_ptr< registry_manager > registry_mgr) : + std::weak_ptr< registry_manager > registry_mgr) : commit_cb_{cb}, group_id_{group_id}, last_commit_idx_(0), @@ -108,6 +108,6 @@ class echo_state_machine : public nuraft::state_machine { raft_commit_cb_t commit_cb_; std::string group_id_; nuraft::ulong last_commit_idx_; - std::shared_ptr< registry_manager > registry_mgr_; + std::weak_ptr< registry_manager > registry_mgr_; }; } // namespace craft diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index 1582a3d..27c8161 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -77,7 +77,7 @@ nuraft::ptr< nuraft::cluster_config > fromClusterConfig(json const& cluster_conf raft_state_mgr::raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id, raft_commit_cb_t cb, - std::shared_ptr< registry_manager > registry_mgr) : + std::weak_ptr< registry_manager > registry_mgr) : nuraft_mesg::mesg_state_mgr(), _srv_id(srv_id), _srv_addr(to_string(srv_addr)), @@ -88,14 +88,16 @@ raft_state_mgr::raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv nuraft::ptr< nuraft::cluster_config > raft_state_mgr::load_config() { LOGDEBUG("Loading config for [{}]", _group_id); json config_map; - if (auto err = loadConfigFile(config_map, _group_id, _srv_id, _registry_mgr); !err) { return fromClusterConfig(config_map); } + auto reg = lock_registry(_registry_mgr); + if (auto err = loadConfigFile(config_map, _group_id, _srv_id, reg); !err) { return fromClusterConfig(config_map); } auto conf = nuraft::cs_new< nuraft::cluster_config >(); conf->get_servers().push_back(nuraft::cs_new< nuraft::srv_config >(_srv_id, _srv_addr)); return conf; } nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { - auto log_store = _registry_mgr->get< nuraft::inmem_log_store >( + auto reg = lock_registry(_registry_mgr); + auto log_store = reg->get< nuraft::inmem_log_store >( registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id))); if (log_store) { LOGDEBUG("RAFT log store for group_id={} already exists, returning existing instance", _group_id); @@ -103,7 +105,7 @@ nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { } LOGDEBUG("Creating RAFT log store for group_id={}", _group_id); log_store = std::make_shared< nuraft::inmem_log_store >(); - _registry_mgr->put< nuraft::inmem_log_store >( + reg->put< nuraft::inmem_log_store >( registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id)), log_store); return log_store; } @@ -111,8 +113,9 @@ nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { nuraft::ptr< nuraft::srv_state > raft_state_mgr::read_state() { LOGDEBUG("Loading state for server: {}", _srv_id); json state_map; + auto reg = lock_registry(_registry_mgr); auto state = nuraft::cs_new< nuraft::srv_state >(); - if (auto err = loadStateFile(state_map, _group_id, _srv_id, _registry_mgr); !err) { + if (auto err = loadStateFile(state_map, _group_id, _srv_id, reg); !err) { try { state->set_term(static_cast< uint64_t >(state_map["term"])); state->set_voted_for(static_cast< int >(state_map["voted_for"])); @@ -128,22 +131,22 @@ void raft_state_mgr::save_config(const nuraft::cluster_config& config) { {"eventual_consistency", config.is_async_replication()}, {"user_ctx", config.get_user_ctx()}, {"servers", toServers(const_cast< nuraft::cluster_config& >(config).get_servers())}}; - _registry_mgr->put< json >(fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id), - std::make_shared< json >(std::move(json_obj))); + lock_registry(_registry_mgr)->put< json >(fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } void raft_state_mgr::save_state(const nuraft::srv_state& state) { auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); auto json_obj = json{{"term", state.get_term()}, {"voted_for", state.get_voted_for()}}; - _registry_mgr->put< json >(fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id), - std::make_shared< json >(std::move(json_obj))); + lock_registry(_registry_mgr)->put< json >(fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } uint32_t raft_state_mgr::get_logstore_id() const { return 0; } std::shared_ptr< nuraft::state_machine > raft_state_mgr::get_state_machine() { - return std::make_shared< echo_state_machine >(_commit_cb, _group_id, _registry_mgr); + return std::make_shared< echo_state_machine >(_commit_cb, _group_id, lock_registry(_registry_mgr)); } void raft_state_mgr::permanent_destroy() {} diff --git a/src/raft/raft_state_manager.hpp b/src/raft/raft_state_manager.hpp index 373151a..9463589 100644 --- a/src/raft/raft_state_manager.hpp +++ b/src/raft/raft_state_manager.hpp @@ -12,7 +12,7 @@ class registry_manager; class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { public: raft_state_mgr(int32_t srv_id, nuraft_mesg::peer_id_t const& srv_addr, nuraft_mesg::group_id_t const& group_id, - raft_commit_cb_t cb, std::shared_ptr< registry_manager > registry_mgr); + raft_commit_cb_t cb, std::weak_ptr< registry_manager > registry_mgr); nuraft::ptr< nuraft::cluster_config > load_config() override; void save_config(const nuraft::cluster_config& config) override; @@ -33,7 +33,7 @@ class raft_state_mgr : public nuraft_mesg::mesg_state_mgr { std::string const _srv_addr; std::string const _group_id; raft_commit_cb_t _commit_cb; - std::shared_ptr< registry_manager > _registry_mgr; + std::weak_ptr< registry_manager > _registry_mgr; }; } // namespace craft diff --git a/src/registry_mgr.hpp b/src/registry_mgr.hpp index 0832632..e96998c 100644 --- a/src/registry_mgr.hpp +++ b/src/registry_mgr.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace craft { @@ -30,4 +31,10 @@ class registry_manager { std::unordered_map component_store_; }; +inline std::shared_ptr< registry_manager > lock_registry(std::weak_ptr< registry_manager > const& w) { + auto r = w.lock(); + RELEASE_ASSERT(r, "registry destroyed before component"); + return r; +} + } \ No newline at end of file From e720e7df83777fe88658f6a5be39dcf5fcb74b80 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sun, 13 Sep 2026 05:37:59 -0700 Subject: [PATCH 20/24] Fix raft init order and the weak_from_init issue is raft service" --- conanfile.py | 2 +- src/raft/raft_replica.cpp | 12 ++++-------- src/raft/raft_service.cpp | 13 +++++++++++-- src/raft/raft_service.hpp | 9 ++++++++- tools/craft_reference_tcp_srv.cpp | 2 +- tools/craft_test_driver/cluster.py | 2 +- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/conanfile.py b/conanfile.py index 1dd4b65..d483a6b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,7 +10,7 @@ class CraftClientConan(ConanFile): name = "craft_client" - version = "0.4.1" + version = "0.4.2" description = ( "CRAFT reference client + wire protocol -- transport-agnostic, HomeStore-free" diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 8ca48c0..2d382da 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -160,15 +160,12 @@ void RaftReplica::replica_init(std::string const& replica_config_path) { }); reg->put< raft_peer_t >(raft_service::peer_id_key(id), std::make_shared< raft_peer_t >(std::make_pair(r->host, r->raft_port))); - reg->put< replica_info >(registry_key(replica_info_key_prefix, r->id), std::move(r)); + reg->put< replica_info >(registry_key(replica_info_key_prefix, id), std::move(r)); } } void RaftReplica::raft_init() { - if (!raft_service_) { - LOGINFO("RAFT already initialized for replica {}", boost::uuids::to_string(ep_.id)); - return; - } + raft_service_ = raft_service::create(ep_.id, registry_mgr_); auto commit_cb = [this](uint64_t log_idx, nlohmann::json const& j, std::string const& partition_uuid_str) { auto const partition_uuid = boost::uuids::string_generator()(partition_uuid_str); auto const op_val = j.at("op").get< int >(); @@ -239,10 +236,9 @@ RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_t MemCraftReplica{std::move(ep), page_size, nullptr}, max_tx_{max_tx}, commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()}, - registry_mgr_{std::move(registry_mgr)}, - raft_service_{init_raft_service ? std::make_shared< raft_service >(ep_.id, registry_mgr_) : nullptr} { + registry_mgr_{std::move(registry_mgr)} { replica_init(replica_config_path); - raft_init(); + if (init_raft_service) { raft_init(); } journal_init(); // set the watchdog diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 764e17e..e946243 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -29,12 +29,14 @@ static nuraft::ptr< nuraft::buffer > create_message(nlohmann::json const& j_obj) raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr) : server_uuid_(server_uuid), - registry_mgr_(std::move(registry_mgr)) { + registry_mgr_(std::move(registry_mgr)) {} + +void raft_service::init() { nuraft::nuraft_global_mgr::init(); // raft global manager for commits std::call_once(raft_started_, [&] { auto my_info = lock_registry(registry_mgr_)->get< raft_peer_t >(peer_id_key(server_uuid_)); if (!my_info) { - LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid); + LOGERROR("Could not start raft service, unrecognized replica uuid {}", server_uuid_); return; } auto params = nuraft_mesg::manager::params{ @@ -54,6 +56,13 @@ raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< }); } +std::shared_ptr< raft_service > raft_service::create(boost::uuids::uuid const& server_uuid, + std::weak_ptr< registry_manager > registry_mgr) { + auto service = std::shared_ptr< raft_service >(new raft_service(server_uuid, std::move(registry_mgr))); + service->init(); + return service; +} + raft_service::~raft_service() { if (consensus_) { consensus_.reset(); diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index 01024dd..abad669 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -30,7 +30,11 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab inline static const std::string peer_id_key(boost::uuids::uuid const& peer_id) { return fmt::format("raft_peer_{}", boost::uuids::to_string(peer_id)); } - raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr); + + // the constructor uses weak_from_this which requires the shared_ptr obj to be created. + static std::shared_ptr< raft_service > create(boost::uuids::uuid const& server_uuid, + std::weak_ptr< registry_manager > registry_mgr); + virtual ~raft_service(); result< void > srv_create_partition(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members); @@ -48,6 +52,9 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab nuraft_mesg::group_id_t const& group_id) override; private: + raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr); + void init(); + consensus_handle consensus_; nuraft_mesg::peer_id_t server_uuid_; std::once_flag raft_started_; diff --git a/tools/craft_reference_tcp_srv.cpp b/tools/craft_reference_tcp_srv.cpp index f44c118..c031684 100644 --- a/tools/craft_reference_tcp_srv.cpp +++ b/tools/craft_reference_tcp_srv.cpp @@ -81,7 +81,7 @@ int main(int argc, char** argv) { } sisl::logging::SetLogger(fmt::format("craft_tcp_srv_{}", SISL_OPTIONS["server_uuid"].as< std::string >())); std::string const s = SISL_OPTIONS.count("log_mods") ? SISL_OPTIONS["log_mods"].as< std::string >() : ""; - for (auto const* mod : {"nuraft_mesg", "grpc_server"}) { + for (auto const* mod : {"nuraft_mesg", "grpc_server", "craft", "base"}) { if (!s.contains(mod)) { sisl::logging::SetModuleLogLevel(mod, spdlog::level::info); } } diff --git a/tools/craft_test_driver/cluster.py b/tools/craft_test_driver/cluster.py index fc0631c..8843952 100644 --- a/tools/craft_test_driver/cluster.py +++ b/tools/craft_test_driver/cluster.py @@ -17,7 +17,7 @@ def __init__( self.config_path = config_path self.members = members lvl = log_level_dict[verbose] - self.log_args = f"base:{lvl},nuraft_mesg:info" + self.log_args = f"base:{lvl}" self.procs: dict[str, subprocess.Popen] = {} def start_all(self): From 073deeca96942a91e6432808312f436581f29e9e Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Sun, 13 Sep 2026 06:08:14 -0700 Subject: [PATCH 21/24] fix code format --- src/client_impl.hpp | 4 +--- src/dlsn_tracker.cpp | 2 +- src/local.cpp | 3 +-- src/mem/cluster.cpp | 6 +++--- src/mem/replica.hpp | 12 ++++++------ src/net/async_conn.cpp | 17 +++++++---------- src/net/conn.cpp | 5 +---- src/net/tcp_cluster.cpp | 3 +-- src/net/tcp_replica.cpp | 4 ++-- src/net/tcp_server.cpp | 15 ++++++++++----- src/net/tcp_server.hpp | 8 ++++---- src/raft/raft_replica.cpp | 31 +++++++++++++++---------------- src/raft/raft_replica.hpp | 16 ++++++++++++---- src/raft/raft_service.cpp | 12 +++++++----- src/raft/raft_state_machine.hpp | 5 +---- src/raft/raft_state_manager.cpp | 20 ++++++++++++-------- src/registry_mgr.hpp | 15 +++++++-------- 17 files changed, 91 insertions(+), 87 deletions(-) diff --git a/src/client_impl.hpp b/src/client_impl.hpp index ed1adbd..861353a 100644 --- a/src/client_impl.hpp +++ b/src/client_impl.hpp @@ -40,9 +40,7 @@ class craft_client { public: craft_client(std::vector< std::shared_ptr< craft_replica > > replicas, uint32_t leader = 0, uint32_t max_inflight = 128) : - replicas_(std::move(replicas)), - leader_(leader), - tracker_(std::make_shared< dlsn_tracker >(max_inflight)) {} + replicas_(std::move(replicas)), leader_(leader), tracker_(std::make_shared< dlsn_tracker >(max_inflight)) {} // Mid-session verbs carry the caller's queue ring (`q`, null = the blocking tier) straight through to every // backend leg they fan out -- one IO's whole leg chain rides one ring, so its resumptions all land back on diff --git a/src/dlsn_tracker.cpp b/src/dlsn_tracker.cpp index dd5c837..c0c4911 100644 --- a/src/dlsn_tracker.cpp +++ b/src/dlsn_tracker.cpp @@ -284,7 +284,7 @@ async_result< read_plan > dlsn_tracker::plan_read(uint64_t addr, uint64_t len) { gate_.leave(); break; } - co_await* ev; + co_await *ev; gate_.leave(); } if (p.degraded()) co_return std::unexpected(make_error_condition(craft_error::NO_QUORUM)); diff --git a/src/local.cpp b/src/local.cpp index 1841c77..aab5ed6 100644 --- a/src/local.cpp +++ b/src/local.cpp @@ -29,8 +29,7 @@ namespace craft { class local_cluster { public: explicit local_cluster(MemReplicaGroup&& group) : - group_{std::move(group)}, - backends_(group_.replicas.begin(), group_.replicas.end()) {} + group_{std::move(group)}, backends_(group_.replicas.begin(), group_.replicas.end()) {} // ~MemReplicaGroup (inside group_) drains the reference pools while it still owns every replica. std::vector< std::shared_ptr< craft_replica > > const& backends() const { return backends_; } diff --git a/src/mem/cluster.cpp b/src/mem/cluster.cpp index 9d01364..bd8a3fd 100644 --- a/src/mem/cluster.cpp +++ b/src/mem/cluster.cpp @@ -92,7 +92,7 @@ async_result< lsn_pair > MemTransport::send_write(std::shared_ptr< MemCraftRepli // submit. It resumes on one of THIS replica's server threads. Hold the awaitable in a local across the // suspension -- it is non-movable and that thread needs its address to stay put. auto ev = after(p.wait, id); - co_await* ev; + co_await *ev; if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); // Re-read: the peer may have gone down while this request was on the wire. @@ -112,7 +112,7 @@ async_result< read_result > MemTransport::send_read(std::shared_ptr< MemCraftRep auto const p = plan_delivery(rf->delay, op_timeout()); { auto ev = after(p.wait, id); // always suspends: the reply crosses the wire - co_await* ev; + co_await *ev; } if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); if (!to->fault_snapshot()->up) co_return fail(craft_error::REPLICA_DOWN); @@ -127,7 +127,7 @@ async_result< lsn_pair > MemTransport::send_keep_alive(std::shared_ptr< MemCraft auto const p = plan_delivery(rf->delay, op_timeout()); { auto ev = after(p.wait, id); // always suspends: the reply crosses the wire - co_await* ev; + co_await *ev; } if (p.timed_out) co_return std::unexpected(std::make_error_condition(std::errc::timed_out)); if (!to->fault_snapshot()->up) co_return fail(craft_error::REPLICA_DOWN); diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 837bf1f..8b3cd62 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -35,7 +35,7 @@ #include #include -#include // result types +#include // result types #include "craft_peer.hpp" // the PEER plane: craft_peer + JournalSlot + lba_t (this model is its only implementer) #include "craft_replica.hpp" // the CLIENT plane: the craft_replica interface @@ -269,15 +269,15 @@ class MemCraftReplica : public craft_replica, // resolution-round hooks used by MemTransport::run_resolution (each takes mu_). A fetched copy shares the // holder's bytes buffer (immutable once appended), so a fill copies no payload. std::optional< MemJournalSlot > peek_slot(int64_t dlsn); // copy of the slot, or nullopt if absent - - void cold_mark_empty(int64_t dlsn); // Empty verdict tombstone; overwrites held - // data (reconciliation: Empty beats data) - std::vector< int64_t > peek_empties(int64_t upto); // every is_empty dLSN <= upto + + void cold_mark_empty(int64_t dlsn); // Empty verdict tombstone; overwrites held + // data (reconciliation: Empty beats data) + std::vector< int64_t > peek_empties(int64_t upto); // every is_empty dLSN <= upto protected: void cold_apply_login(uint64_t client_token, uint64_t term); void cold_truncate_above(int64_t rs_commit_lsn); - void cold_install_slot(int64_t dlsn, MemJournalSlot s); // fill a hole; never overwrites an entry + void cold_install_slot(int64_t dlsn, MemJournalSlot s); // fill a hole; never overwrites an entry // Test observability: how many reads this replica actually served. Lets a test witness read routing // (e.g. round-robin distribution across members). Not part of the CRAFT surface. diff --git a/src/net/async_conn.cpp b/src/net/async_conn.cpp index fb13e63..bfbb8d0 100644 --- a/src/net/async_conn.cpp +++ b/src/net/async_conn.cpp @@ -40,10 +40,7 @@ constexpr std::size_t k_recv_chunk = 64 * 1024; // per-recv landing size; a mess } // namespace craft_async_conn::craft_async_conn(std::string host, uint16_t port, uint32_t max_tx, ::io_uring* ring) : - host_{std::move(host)}, - port_{port}, - max_tx_{max_tx}, - ring_{ring} {} + host_{std::move(host)}, port_{port}, max_tx_{max_tx}, ring_{ring} {} craft_async_conn::~craft_async_conn() { shutdown(); } @@ -111,20 +108,20 @@ sisl::async::light_task< int > craft_async_conn::ring_connect() { int const one = 1; ::setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); } - if (fd_ < 0) co_return - errno; + if (fd_ < 0) co_return -errno; sockaddr_in sa{}; sa.sin_family = AF_INET; sa.sin_port = htons(port_); - if (::inet_pton(AF_INET, host_.c_str(), &sa.sin_addr) != 1) co_return - EINVAL; // loopback / IP literal only (v1) + if (::inet_pton(AF_INET, host_.c_str(), &sa.sin_addr) != 1) co_return -EINVAL; // loopback / IP literal only (v1) sisl::async::cqe_awaitable ev; auto* sqe = acquire_sqe(); - if (nullptr == sqe) co_return - EAGAIN; + if (nullptr == sqe) co_return -EAGAIN; ::io_uring_prep_connect(sqe, fd_, reinterpret_cast< sockaddr* >(&sa), sizeof(sa)); // sa is frame-local, stable ::io_uring_sqe_set_data64(sqe, sisl::async::encode_managed_user_data(&ev)); int const res = co_await ev; - co_return(res < 0) ? res : 0; + co_return (res < 0) ? res : 0; } sisl::async::light_task< int > craft_async_conn::ring_send_all(std::span< uint8_t const > data) { @@ -132,11 +129,11 @@ sisl::async::light_task< int > craft_async_conn::ring_send_all(std::span< uint8_ while (off < data.size()) { sisl::async::cqe_awaitable ev; auto* sqe = acquire_sqe(); - if (nullptr == sqe) co_return - EAGAIN; + if (nullptr == sqe) co_return -EAGAIN; ::io_uring_prep_send(sqe, fd_, data.data() + off, data.size() - off, MSG_NOSIGNAL); ::io_uring_sqe_set_data64(sqe, sisl::async::encode_managed_user_data(&ev)); int const n = co_await ev; - if (n <= 0) co_return(n == 0 ? -EPIPE : n); // 0 = peer closed; <0 = error + if (n <= 0) co_return (n == 0 ? -EPIPE : n); // 0 = peer closed; <0 = error off += static_cast< std::size_t >(n); } co_return 0; diff --git a/src/net/conn.cpp b/src/net/conn.cpp index 9bd0a0d..bd7fe4d 100644 --- a/src/net/conn.cpp +++ b/src/net/conn.cpp @@ -53,10 +53,7 @@ void set_nodelay(int fd) { craft_conn::~craft_conn() { close_all(); } craft_conn::craft_conn(craft_conn&& o) noexcept : - fd_(o.fd_), - ring_(o.ring_), - ring_ready_(o.ring_ready_), - rx_(std::move(o.rx_)) { + fd_(o.fd_), ring_(o.ring_), ring_ready_(o.ring_ready_), rx_(std::move(o.rx_)) { // io_uring's pointers reference external mmap'd memory, so copying the struct and disarming the source // (so only we exit the ring) is a valid move. o.fd_ = -1; diff --git a/src/net/tcp_cluster.cpp b/src/net/tcp_cluster.cpp index a7b5d05..b21591d 100644 --- a/src/net/tcp_cluster.cpp +++ b/src/net/tcp_cluster.cpp @@ -32,8 +32,7 @@ class tcp_cluster { public: tcp_cluster(std::vector< std::shared_ptr< CraftTcpReplica > > proxies, std::vector< std::shared_ptr< craft_replica > > backends) : - proxies_{std::move(proxies)}, - backends_{std::move(backends)} {} + proxies_{std::move(proxies)}, backends_{std::move(backends)} {} ~tcp_cluster() { for (auto& p : proxies_) if (p) p->shutdown(); // drain each proxy from HERE (the destroying thread) before any of them drops diff --git a/src/net/tcp_replica.cpp b/src/net/tcp_replica.cpp index f869dc9..9561da2 100644 --- a/src/net/tcp_replica.cpp +++ b/src/net/tcp_replica.cpp @@ -234,7 +234,7 @@ std::optional< std::error_condition > CraftTcpReplica::ensure_bound(uint64_t ter async_result< LoginResult > CraftTcpReplica::login(uint64_t client_token) { auto ev = hop(); - co_await* ev; // now on the session-mgr thread + co_await *ev; // now on the session-mgr thread if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); auto r = conn_.login(vol_id_, client_token); // LOGIN names the volume, exactly as HELO does if (!r) co_return std::unexpected(on_net_fault(r.error())); @@ -266,7 +266,7 @@ async_result< LoginResult > CraftTcpReplica::login(uint64_t client_token) { async_status CraftTcpReplica::logout(client_hdr hdr) { auto ev = hop(); - co_await* ev; + co_await *ev; if (!ensure_connected()) co_return fail(craft_error::REPLICA_DOWN); (void)hdr; auto r = conn_.logout(); diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 617d537..9ae278b 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -41,13 +41,18 @@ std::span< uint8_t const > as_bytes(T const& v) { craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file, std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service) : - geo_{std::move(geo)}, - registry_mgr_{std::move(registry_mgr)}, - raft_enabled_{init_raft_service} { + geo_{std::move(geo)}, registry_mgr_{std::move(registry_mgr)}, raft_enabled_{init_raft_service} { auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); - replica_ = std::make_shared< RaftReplica >(std::move(ep), geo_.lba_size, geo_.max_tx, server_config_file, nullptr, - registry_mgr_, init_raft_service); + replica_ = std::make_shared< RaftReplica >(raft_replica_params{ + .ep = std::move(ep), + .page_size = geo_.lba_size, + .max_tx = geo_.max_tx, + .replica_config_path = server_config_file, + .watchdog = std::make_shared< Watchdog >(), + .registry_mgr = registry_mgr_, + .init_raft_service = init_raft_service, + }); } craft_tcp_server::~craft_tcp_server() = default; diff --git a/src/net/tcp_server.hpp b/src/net/tcp_server.hpp index cbe7f0b..554344d 100644 --- a/src/net/tcp_server.hpp +++ b/src/net/tcp_server.hpp @@ -69,10 +69,10 @@ class craft_tcp_server { private: server_geometry geo_; - std::shared_ptr< RaftReplica > replica_; // the real state; driven via its srv_* local-server seam - uint64_t next_term_ = 0; // monotonic term source; a fresh LOGIN takes ++next_term_ - uint64_t session_term_ = 0; // the current session's term, stamped on every IO - bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced + std::shared_ptr< RaftReplica > replica_; // the real state; driven via its srv_* local-server seam + uint64_t next_term_ = 0; // monotonic term source; a fresh LOGIN takes ++next_term_ + uint64_t session_term_ = 0; // the current session's term, stamped on every IO + bool session_active_ = false; // false before LOGIN / after LOGOUT -> IO is fenced std::shared_ptr< registry_manager > registry_mgr_ = nullptr; // the registry manager instance bool raft_enabled_ = false; diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 2d382da..282a934 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -63,7 +63,8 @@ struct replica_info { std::shared_ptr< net::CraftTcpPeer > peer_client{nullptr}; }; -std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id, std::shared_ptr< registry_manager > registry_mgr) { +std::vector< replica_info > peer_list(boost::uuids::uuid const& partition_id, + std::shared_ptr< registry_manager > registry_mgr) { partition_peers_list_t peers; if (auto peers_ptr = registry_mgr->get< partition_peers_list_t >(registry_key(partition_info_key_prefix, partition_id)); @@ -204,8 +205,7 @@ void RaftReplica::raft_init() { }; raft_service_->add_commit_cb(std::move(commit_cb)); - LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), - page_size_); + LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), page_size_); } void RaftReplica::journal_init() { @@ -230,23 +230,21 @@ void RaftReplica::journal_init() { } } -RaftReplica::RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, - std::string const& replica_config_path, std::shared_ptr< Watchdog > watchdog, - std::weak_ptr< registry_manager > registry_mgr, bool init_raft_service) : - MemCraftReplica{std::move(ep), page_size, nullptr}, - max_tx_{max_tx}, +RaftReplica::RaftReplica(raft_replica_params params) : + MemCraftReplica{std::move(params.ep), params.page_size, nullptr}, + max_tx_{params.max_tx}, commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()}, - registry_mgr_{std::move(registry_mgr)} { - replica_init(replica_config_path); - if (init_raft_service) { raft_init(); } + registry_mgr_{std::move(params.registry_mgr)} { + replica_init(params.replica_config_path); + if (params.init_raft_service) { raft_init(); } journal_init(); // set the watchdog - if (!watchdog) { + if (!params.watchdog) { LOGWARN("watchdog is not provided, using default watchdog"); watchdog_ = std::make_shared< Watchdog >(); } else { - watchdog_ = std::move(watchdog); + watchdog_ = std::move(params.watchdog); } } @@ -445,7 +443,9 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& boost::uuids::to_string(partition_uuid), members.size()); for (auto const& m : members) { if (m.id == ep_.id) { continue; } - if (auto r = sisl::async::sync_get(m.peer_client->get_rs_commit_lsn(term /* send proposed term */, true /* is_login */)); r) { + if (auto r = sisl::async::sync_get( + m.peer_client->get_rs_commit_lsn(term /* send proposed term */, true /* is_login */)); + r) { LOGDEBUG("apply_login[partition={}]: peer {} reported commit_lsn={} last_append_lsn={}", boost::uuids::to_string(partition_uuid), boost::uuids::to_string(m.id), r->commit_lsn, r->last_append_lsn); @@ -508,8 +508,7 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& } LOGINFO("apply_login[partition={}]: LOGIN SUCCESS term={} dLSN={} members={}", boost::uuids::to_string(partition_uuid), term, rs_commit_lsn, replicas.size()); - return LoginResult{.members = replicas, - .dLSN = rs_commit_lsn}; + return LoginResult{.members = replicas, .dLSN = rs_commit_lsn}; } // create peer raft group and add members to it. diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index acbfa84..5a3ea1e 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -41,11 +41,19 @@ class raft_service; struct SyncRSCommitLSNMsg; struct InternalLoginMsg; +struct raft_replica_params { + replica_endpoint ep; + uint32_t page_size; + uint32_t max_tx; + std::string replica_config_path; + std::shared_ptr< Watchdog > watchdog; + std::weak_ptr< registry_manager > registry_mgr; + bool init_raft_service{false}; +}; + class RaftReplica final : public MemCraftReplica { public: - RaftReplica(replica_endpoint ep, uint32_t page_size, uint32_t max_tx, std::string const& replica_config_path, - std::shared_ptr< Watchdog > watchdog = nullptr, std::weak_ptr< registry_manager > registry_mgr = {}, - bool init_raft_service = false); + RaftReplica(raft_replica_params params); ~RaftReplica(); @@ -84,7 +92,7 @@ class RaftReplica final : public MemCraftReplica { std::mutex login_mu_; std::condition_variable login_cv_; bool login_done_{false}; - + class RaftCommitWorker; std::unique_ptr< RaftCommitWorker > commit_worker_; std::optional< Watchdog::TimerId > pending_login_timer_; diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index e946243..8b511e7 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -28,8 +28,7 @@ static nuraft::ptr< nuraft::buffer > create_message(nlohmann::json const& j_obj) } raft_service::raft_service(boost::uuids::uuid const& server_uuid, std::weak_ptr< registry_manager > registry_mgr) : - server_uuid_(server_uuid), - registry_mgr_(std::move(registry_mgr)) {} + server_uuid_(server_uuid), registry_mgr_(std::move(registry_mgr)) {} void raft_service::init() { nuraft::nuraft_global_mgr::init(); // raft global manager for commits @@ -137,7 +136,8 @@ bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { LOGERROR("Raft not enabled!"); return false; } - auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = + lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return false; @@ -151,7 +151,8 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGERROR("Raft not enabled!"); return {}; } - auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = + lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return {}; @@ -170,7 +171,8 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr template < typename MsgT > result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT const& payload) { - auto const state_mgr = lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto const state_mgr = + lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return std::unexpected(make_error_condition(craft_error::INTERNAL)); diff --git a/src/raft/raft_state_machine.hpp b/src/raft/raft_state_machine.hpp index 88892c1..2f6c0e3 100644 --- a/src/raft/raft_state_machine.hpp +++ b/src/raft/raft_state_machine.hpp @@ -62,10 +62,7 @@ class echo_state_machine : public nuraft::state_machine { public: echo_state_machine(raft_commit_cb_t const& cb, std::string const& group_id, std::weak_ptr< registry_manager > registry_mgr) : - commit_cb_{cb}, - group_id_{group_id}, - last_commit_idx_(0), - registry_mgr_(std::move(registry_mgr)) {} + commit_cb_{cb}, group_id_{group_id}, last_commit_idx_(0), registry_mgr_(std::move(registry_mgr)) {} virtual nuraft::ptr< nuraft::buffer > commit(nuraft::ulong log_idx, nuraft::buffer& data) override { nlohmann::json j; diff --git a/src/raft/raft_state_manager.cpp b/src/raft/raft_state_manager.cpp index 27c8161..633c29f 100644 --- a/src/raft/raft_state_manager.cpp +++ b/src/raft/raft_state_manager.cpp @@ -14,7 +14,8 @@ std::string const log_store_key_prefix{"raft_log_store"}; namespace craft { -std::error_condition loadConfigFile(json& config_map, std::string const& _group_id, int32_t const _srv_id, std::shared_ptr< registry_manager >& _registry_mgr) { +std::error_condition loadConfigFile(json& config_map, std::string const& _group_id, int32_t const _srv_id, + std::shared_ptr< registry_manager >& _registry_mgr) { auto const registry_key = fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id); auto json_ptr = _registry_mgr->get< json >(registry_key); if (!json_ptr) { @@ -25,7 +26,8 @@ std::error_condition loadConfigFile(json& config_map, std::string const& _group_ return {}; } -std::error_condition loadStateFile(json& state_map, std::string const& _group_id, int32_t const _srv_id, std::shared_ptr< registry_manager >& _registry_mgr) { +std::error_condition loadStateFile(json& state_map, std::string const& _group_id, int32_t const _srv_id, + std::shared_ptr< registry_manager >& _registry_mgr) { auto const registry_key = fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id); auto json_ptr = _registry_mgr->get< json >(registry_key); if (!json_ptr) { @@ -105,8 +107,8 @@ nuraft::ptr< nuraft::log_store > raft_state_mgr::load_log_store() { } LOGDEBUG("Creating RAFT log store for group_id={}", _group_id); log_store = std::make_shared< nuraft::inmem_log_store >(); - reg->put< nuraft::inmem_log_store >( - registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id)), log_store); + reg->put< nuraft::inmem_log_store >(registry_key(log_store_key_prefix, boost::uuids::string_generator()(_group_id)), + log_store); return log_store; } @@ -131,16 +133,18 @@ void raft_state_mgr::save_config(const nuraft::cluster_config& config) { {"eventual_consistency", config.is_async_replication()}, {"user_ctx", config.get_user_ctx()}, {"servers", toServers(const_cast< nuraft::cluster_config& >(config).get_servers())}}; - lock_registry(_registry_mgr)->put< json >(fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id), - std::make_shared< json >(std::move(json_obj))); + lock_registry(_registry_mgr) + ->put< json >(fmt::format(FMT_STRING("{}_s{}_config"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } void raft_state_mgr::save_state(const nuraft::srv_state& state) { auto const state_file = fmt::format(FMT_STRING("{}_s{}/state.json"), _group_id, _srv_id); auto json_obj = json{{"term", state.get_term()}, {"voted_for", state.get_voted_for()}}; - lock_registry(_registry_mgr)->put< json >(fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id), - std::make_shared< json >(std::move(json_obj))); + lock_registry(_registry_mgr) + ->put< json >(fmt::format(FMT_STRING("{}_s{}_state"), _group_id, _srv_id), + std::make_shared< json >(std::move(json_obj))); } uint32_t raft_state_mgr::get_logstore_id() const { return 0; } diff --git a/src/registry_mgr.hpp b/src/registry_mgr.hpp index e96998c..797019c 100644 --- a/src/registry_mgr.hpp +++ b/src/registry_mgr.hpp @@ -10,25 +10,24 @@ namespace craft { class registry_manager { public: - template - void put(std::string const& key, std::shared_ptr value) { + template < typename T > + void put(std::string const& key, std::shared_ptr< T > value) { std::lock_guard lock(component_mutex_); component_store_[key] = std::move(value); } - template - std::shared_ptr get(std::string const& key) const { + template < typename T > + std::shared_ptr< T > get(std::string const& key) const { std::lock_guard lock(component_mutex_); auto it = component_store_.find(key); if (it == component_store_.end()) return nullptr; - auto* ptr = std::any_cast>(&it->second); + auto* ptr = std::any_cast< std::shared_ptr< T > >(&it->second); return ptr ? *ptr : nullptr; } - private: mutable std::mutex component_mutex_; - std::unordered_map component_store_; + std::unordered_map< std::string, std::any > component_store_; }; inline std::shared_ptr< registry_manager > lock_registry(std::weak_ptr< registry_manager > const& w) { @@ -37,4 +36,4 @@ inline std::shared_ptr< registry_manager > lock_registry(std::weak_ptr< registry return r; } -} \ No newline at end of file +} // namespace craft \ No newline at end of file From ece6698b148417df4855692255372b2875ac9dcc Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Tue, 15 Sep 2026 15:12:12 +0530 Subject: [PATCH 22/24] persist craft partition state --- src/mem/replica.cpp | 6 ++++++ src/mem/replica.hpp | 1 + src/net/tcp_server.cpp | 2 +- src/raft/raft_replica.cpp | 29 +++++++++++++++++++++++++++++ src/raft/raft_replica.hpp | 3 +++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/mem/replica.cpp b/src/mem/replica.cpp index 28ce839..0deb3d1 100644 --- a/src/mem/replica.cpp +++ b/src/mem/replica.cpp @@ -254,6 +254,7 @@ result< lsn_pair > MemCraftReplica::do_write(client_hdr hdr, int64_t dlsn, uint6 (*journal_)[dlsn] = std::move(slot); state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn); apply_up_to(hdr.commit_lsn); // piggybacked commit: advance the frontier best-effort, in dLSN order + on_state_changed(); // Piggyback the watermarks on the ack (the wire's write_rsp), so any round-trip refreshes the client. return lsn_pair{state_.commit_lsn, state_.last_append_lsn}; } @@ -292,6 +293,7 @@ status MemCraftReplica::do_truncate(int64_t lsn) { std::lock_guard< std::mutex > g{mu_}; journal_->erase(journal_->upper_bound(lsn), journal_->end()); state_.last_append_lsn = std::min(state_.last_append_lsn, lsn); + on_state_changed(); return ok(); } @@ -339,6 +341,7 @@ result< resolution_result > MemCraftReplica::do_resolve_local(client_hdr hdr, in } state_.last_append_lsn = std::max(state_.last_append_lsn, upto); apply_up_to(upto); + on_state_changed(); return out; } @@ -512,16 +515,19 @@ void MemCraftReplica::cold_apply_login(uint64_t client_token, uint64_t term) { std::lock_guard< std::mutex > g{mu_}; state_.client_token = client_token; state_.term = term; + on_state_changed(); } void MemCraftReplica::cold_apply_logout() { std::lock_guard< std::mutex > g{mu_}; state_.client_token = 0; state_.term = 0; // no active session; subsequent IOs with old term fail STALE_TERM + on_state_changed(); } void MemCraftReplica::cold_truncate_above(int64_t rs_commit_lsn) { std::lock_guard< std::mutex > g{mu_}; journal_->erase(journal_->upper_bound(rs_commit_lsn), journal_->end()); state_.last_append_lsn = std::min(state_.last_append_lsn, rs_commit_lsn); + on_state_changed(); } // ── resolution-round hooks (driven by MemTransport::run_resolution) ── diff --git a/src/mem/replica.hpp b/src/mem/replica.hpp index 8b3cd62..65409c3 100644 --- a/src/mem/replica.hpp +++ b/src/mem/replica.hpp @@ -275,6 +275,7 @@ class MemCraftReplica : public craft_replica, std::vector< int64_t > peek_empties(int64_t upto); // every is_empty dLSN <= upto protected: + virtual void on_state_changed() {} // called under mu_ after every state_ mutation; override to persist void cold_apply_login(uint64_t client_token, uint64_t term); void cold_truncate_above(int64_t rs_commit_lsn); void cold_install_slot(int64_t dlsn, MemJournalSlot s); // fill a hole; never overwrites an entry diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index 9ae278b..cb37460 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -41,7 +41,7 @@ std::span< uint8_t const > as_bytes(T const& v) { craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file, std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service) : - geo_{std::move(geo)}, registry_mgr_{std::move(registry_mgr)}, raft_enabled_{init_raft_service} { + geo_{std::move(geo)}, registry_mgr_{registry_mgr ? std::move(registry_mgr) : std::make_shared< registry_manager >()}, raft_enabled_{init_raft_service} { auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); replica_ = std::make_shared< RaftReplica >(raft_replica_params{ diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 282a934..425d9c3 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -53,6 +53,7 @@ std::string const replica_info_key_prefix{"replica_info"}; std::string const partition_info_key_prefix{"partition_info"}; std::string const journal_key_prefix{"journal"}; std::string const index_key_prefix{"index"}; +std::string const state_key_prefix{"craft_state"}; using partition_peers_list_t = std::vector< boost::uuids::uuid >; struct replica_info { @@ -230,6 +231,30 @@ void RaftReplica::journal_init() { } } +void RaftReplica::state_init() { + auto reg = registry_mgr_.lock(); + if (!reg) { + LOGWARN("state_init[{}]: registry not available", boost::uuids::to_string(ep_.id)); + return; + } + if (auto existing = reg->get< CraftPartitionState >(registry_key(state_key_prefix, ep_.id)); existing) { + state_ = *existing; + LOGINFO("state_init[{}]: recovered state term={} commit_lsn={} last_append_lsn={}", + boost::uuids::to_string(ep_.id), state_.term, state_.commit_lsn, state_.last_append_lsn); + } else { + reg->put< CraftPartitionState >(registry_key(state_key_prefix, ep_.id), + std::make_shared< CraftPartitionState >(state_)); + LOGINFO("state_init[{}]: initialized fresh state", boost::uuids::to_string(ep_.id)); + } +} + +void RaftReplica::persist_state() { + auto reg = registry_mgr_.lock(); + if (!reg) { return; } + reg->put< CraftPartitionState >(registry_key(state_key_prefix, ep_.id), + std::make_shared< CraftPartitionState >(state_)); +} + RaftReplica::RaftReplica(raft_replica_params params) : MemCraftReplica{std::move(params.ep), params.page_size, nullptr}, max_tx_{params.max_tx}, @@ -238,6 +263,7 @@ RaftReplica::RaftReplica(raft_replica_params params) : replica_init(params.replica_config_path); if (params.init_raft_service) { raft_init(); } journal_init(); + state_init(); // set the watchdog if (!params.watchdog) { @@ -414,6 +440,7 @@ result< LoginResult > RaftReplica::apply_login(std::array< uint8_t, 16 > const& std::lock_guard< std::mutex > g{mu_}; state_.term = term; state_.client_token = client_token; + on_state_changed(); LOGINFO("apply_login [id={}]: raft disabled, cold-path login OK, term={} token={}", boost::uuids::to_string(ep_.id), term, client_token); return LoginResult{.members = {ep_}, .dLSN = state_.last_append_lsn}; @@ -590,6 +617,7 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCom std::lock_guard< std::mutex > g{mu_}; apply_up_to(m.rs_commit_lsn); state_.commit_lsn = m.rs_commit_lsn; + on_state_changed(); LOGDEBUG("apply_sync[partition={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(partition_uuid), state_.commit_lsn, m.rs_commit_lsn); } @@ -604,6 +632,7 @@ void RaftReplica::internal_login(InternalLoginMsg m) { std::lock_guard< std::mutex > g{mu_}; state_.client_token = m.client_token; state_.term = m.term; + on_state_changed(); if (pending_login_timer_) { watchdog_->cancel(*pending_login_timer_); pending_login_timer_.reset(); diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 5a3ea1e..0e3bb3d 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -86,6 +86,9 @@ class RaftReplica final : public MemCraftReplica { void raft_init(); void replica_init(std::string const& replica_config_path); void journal_init(); + void state_init(); + void persist_state(); // called under mu_ + void on_state_changed() override { persist_state(); } uint32_t max_tx_; std::atomic< int64_t > rs_commit_lsn_{-1}; From 4f4c9dcd990c1b0b2af1cfd27a4ba5a77e8b1c00 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Tue, 15 Sep 2026 17:30:22 +0530 Subject: [PATCH 23/24] add partition recovery logic. Do not persist state manager in registry- the log store and state machine persistence is enough --- src/raft/raft_replica.cpp | 21 ++++++++++++-- src/raft/raft_replica.hpp | 1 + src/raft/raft_service.cpp | 59 ++++++++++++++++++++++----------------- src/raft/raft_service.hpp | 1 + src/registry_mgr.hpp | 11 ++++++++ 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/raft/raft_replica.cpp b/src/raft/raft_replica.cpp index 425d9c3..1279b22 100644 --- a/src/raft/raft_replica.cpp +++ b/src/raft/raft_replica.cpp @@ -209,6 +209,21 @@ void RaftReplica::raft_init() { LOGDEBUG("RaftReplica constructed [id={}] lba_size={}", boost::uuids::to_string(ep_.id), page_size_); } +void RaftReplica::recover_partitions() { + auto reg = registry_mgr_.lock(); + if (!reg) { return; } + auto const prefix = partition_info_key_prefix + "_"; + for (auto const& [key, _] : reg->get_prefix< partition_peers_list_t >(prefix)) { + auto const partition_uuid = boost::uuids::string_generator()(key.substr(prefix.size())); + LOGINFO("recover_partitions[{}]: rejoining partition {}", boost::uuids::to_string(ep_.id), + boost::uuids::to_string(partition_uuid)); + if (auto const r = raft_service_->srv_recover_partition(partition_uuid); !r) { + LOGERROR("recover_partitions[{}]: failed to rejoin partition {}", boost::uuids::to_string(ep_.id), + boost::uuids::to_string(partition_uuid)); + } + } +} + void RaftReplica::journal_init() { auto reg = registry_mgr_.lock(); if (!reg) { @@ -261,7 +276,10 @@ RaftReplica::RaftReplica(raft_replica_params params) : commit_worker_{std::make_unique< RaftReplica::RaftCommitWorker >()}, registry_mgr_{std::move(params.registry_mgr)} { replica_init(params.replica_config_path); - if (params.init_raft_service) { raft_init(); } + if (params.init_raft_service) { + raft_init(); + recover_partitions(); + } journal_init(); state_init(); @@ -616,7 +634,6 @@ void RaftReplica::apply_sync(boost::uuids::uuid const& partition_uuid, SyncRSCom std::lock_guard< std::mutex > g{mu_}; apply_up_to(m.rs_commit_lsn); - state_.commit_lsn = m.rs_commit_lsn; on_state_changed(); LOGDEBUG("apply_sync[partition={}]: done, commit_lsn now {} (target rs_commit_lsn={})", boost::uuids::to_string(partition_uuid), state_.commit_lsn, m.rs_commit_lsn); diff --git a/src/raft/raft_replica.hpp b/src/raft/raft_replica.hpp index 0e3bb3d..7fc43af 100644 --- a/src/raft/raft_replica.hpp +++ b/src/raft/raft_replica.hpp @@ -85,6 +85,7 @@ class RaftReplica final : public MemCraftReplica { void internal_login(InternalLoginMsg m); void raft_init(); void replica_init(std::string const& replica_config_path); + void recover_partitions(); void journal_init(); void state_init(); void persist_state(); // called under mu_ diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index 8b511e7..fd3db04 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -9,9 +9,7 @@ #include #include -namespace { -std::string const state_mgr_key_prefix{"raft_state_mgr"}; -} +namespace {} namespace craft { @@ -109,17 +107,11 @@ std::string raft_service::lookup_peer(nuraft_mesg::peer_id_t const& peer_id) { std::shared_ptr< nuraft_mesg::mesg_state_mgr > raft_service::create_state_mgr(int32_t const srv_id, nuraft_mesg::group_id_t const& group_id) { - auto reg = lock_registry(registry_mgr_); - auto result = reg->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); - if (result) { - LOGINFO("RAFT state manager for group_id={} already exists, returning existing instance", - boost::uuids::to_string(group_id)); - return result; - } + // Always create fresh — never cache the state manager. The commit_cb_ captures a live `this`; a cached + // instance from a prior run holds a stale pointer. Log store, cluster config, and srv_state survive in + // their own registry keys and are loaded by the fresh instance's methods. LOGINFO("Creating RAFT state manager for server_id={} group_id={}", srv_id, boost::uuids::to_string(group_id)); - auto mgr = std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_, registry_mgr_); - reg->put< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id), mgr); - return mgr; + return std::make_shared< raft_state_mgr >(srv_id, server_uuid_, group_id, commit_cb_, registry_mgr_); } void raft_service::add_commit_cb(raft_commit_cb_t cb) { @@ -131,18 +123,22 @@ void raft_service::add_commit_cb(raft_commit_cb_t cb) { commit_cb_ = std::move(cb); } +static raft_state_mgr* lookup_mgr(consensus_handle const& consensus, nuraft_mesg::group_id_t const& group_id) { + auto base = consensus->lookup_state_manager(group_id); + return base ? static_cast< raft_state_mgr* >(base.get()) : nullptr; +} + bool raft_service::is_leader(nuraft_mesg::group_id_t const& group_id) { if (!consensus_) { LOGERROR("Raft not enabled!"); return false; } - auto const state_mgr = - lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); - if (!state_mgr) { + auto* mgr = lookup_mgr(consensus_, group_id); + if (!mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return false; } - auto* raft_ctx = state_mgr->repl_ctx(); + auto* raft_ctx = mgr->repl_ctx(); return raft_ctx && raft_ctx->is_raft_leader(); } @@ -151,19 +147,18 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGERROR("Raft not enabled!"); return {}; } - auto const state_mgr = - lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); - if (!state_mgr) { + auto* mgr = lookup_mgr(consensus_, group_id); + if (!mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return {}; } - auto* raft_ctx = state_mgr->repl_ctx(); + auto* raft_ctx = mgr->repl_ctx(); if (!raft_ctx) { LOGWARN("No leader for the raft group {}", group_id); return {}; } - if (auto const leader_id = raft_ctx->raft_leader_id(); !leader_id.empty()) { - return boost::uuids::string_generator()(raft_ctx->raft_leader_id()); + if (auto const lid = raft_ctx->raft_leader_id(); !lid.empty()) { + return boost::uuids::string_generator()(lid); } LOGWARN("No leader for the raft group {}", group_id); return {}; @@ -171,8 +166,7 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr template < typename MsgT > result< void > raft_service::propose(boost::uuids::uuid const& group_id, MsgT const& payload) { - auto const state_mgr = - lock_registry(registry_mgr_)->get< raft_state_mgr >(registry_key(state_mgr_key_prefix, group_id)); + auto* state_mgr = lookup_mgr(consensus_, group_id); if (!state_mgr) { LOGWARN("RAFT state manager for group_id={} not found", boost::uuids::to_string(group_id)); return std::unexpected(make_error_condition(craft_error::INTERNAL)); @@ -194,4 +188,19 @@ template result< void > raft_service::propose< SyncRSCommitLSNMsg >(boost::uuids SyncRSCommitLSNMsg const&); template result< void > raft_service::propose< InternalLoginMsg >(boost::uuids::uuid const&, InternalLoginMsg const&); +result< void > raft_service::srv_recover_partition(boost::uuids::uuid const& group_id) { + if (!consensus_) { + LOGERROR("srv_recover_partition[{}]: raft not enabled", boost::uuids::to_string(group_id)); + return fail(craft_error::INTERNAL); + } + auto mgr = std::make_shared< raft_state_mgr >(nuraft_mesg::to_server_id(server_uuid_), server_uuid_, group_id, + commit_cb_, registry_mgr_); + if (auto const r = consensus_->join_group(group_id, default_group_type_, mgr); !r) { + LOGERROR("srv_recover_partition[{}]: join_group failed", boost::uuids::to_string(group_id)); + return fail(craft_error::INTERNAL); + } + LOGINFO("srv_recover_partition[{}]: rejoined raft group OK", boost::uuids::to_string(group_id)); + return {}; +} + } // namespace craft diff --git a/src/raft/raft_service.hpp b/src/raft/raft_service.hpp index abad669..d627fce 100644 --- a/src/raft/raft_service.hpp +++ b/src/raft/raft_service.hpp @@ -38,6 +38,7 @@ class raft_service : public nuraft_mesg::messaging_application, public std::enab virtual ~raft_service(); result< void > srv_create_partition(boost::uuids::uuid const& group_id, std::vector< replica_endpoint > const& members); + result< void > srv_recover_partition(boost::uuids::uuid const& group_id); void add_commit_cb(raft_commit_cb_t cb); bool is_leader(nuraft_mesg::group_id_t const& group_id); nuraft_mesg::peer_id_t leader_id(nuraft_mesg::group_id_t const& group_id); diff --git a/src/registry_mgr.hpp b/src/registry_mgr.hpp index 797019c..7e20f24 100644 --- a/src/registry_mgr.hpp +++ b/src/registry_mgr.hpp @@ -25,6 +25,17 @@ class registry_manager { return ptr ? *ptr : nullptr; } + template < typename T > + std::vector< std::pair< std::string, std::shared_ptr< T > > > get_prefix(std::string const& prefix) const { + std::lock_guard lock(component_mutex_); + std::vector< std::pair< std::string, std::shared_ptr< T > > > result; + for (auto const& [key, val] : component_store_) { + if (!key.starts_with(prefix)) continue; + if (auto const* ptr = std::any_cast< std::shared_ptr< T > >(&val)) result.emplace_back(key, *ptr); + } + return result; + } + private: mutable std::mutex component_mutex_; std::unordered_map< std::string, std::any > component_store_; From a09fb790d3bc051f59287eede50cfa6abd30f146 Mon Sep 17 00:00:00 2001 From: Ravi Nagarjun Akella Date: Tue, 15 Sep 2026 05:09:14 -0700 Subject: [PATCH 24/24] fix code style --- src/net/tcp_server.cpp | 4 +++- src/raft/raft_service.cpp | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp index cb37460..3195e10 100644 --- a/src/net/tcp_server.cpp +++ b/src/net/tcp_server.cpp @@ -41,7 +41,9 @@ std::span< uint8_t const > as_bytes(T const& v) { craft_tcp_server::craft_tcp_server(server_geometry geo, std::string const& server_config_file, std::shared_ptr< registry_manager > registry_mgr, bool init_raft_service) : - geo_{std::move(geo)}, registry_mgr_{registry_mgr ? std::move(registry_mgr) : std::make_shared< registry_manager >()}, raft_enabled_{init_raft_service} { + geo_{std::move(geo)}, + registry_mgr_{registry_mgr ? std::move(registry_mgr) : std::make_shared< registry_manager >()}, + raft_enabled_{init_raft_service} { auto ep = replica_endpoint{.id = to_uuid(geo_.member.id), .addr = geo_.member.addr}; LOGINFO("craft_tcp_server: starting [id={}] config_file='{}'", boost::uuids::to_string(ep.id), server_config_file); replica_ = std::make_shared< RaftReplica >(raft_replica_params{ diff --git a/src/raft/raft_service.cpp b/src/raft/raft_service.cpp index fd3db04..388829f 100644 --- a/src/raft/raft_service.cpp +++ b/src/raft/raft_service.cpp @@ -157,9 +157,7 @@ nuraft_mesg::peer_id_t raft_service::leader_id(nuraft_mesg::group_id_t const& gr LOGWARN("No leader for the raft group {}", group_id); return {}; } - if (auto const lid = raft_ctx->raft_leader_id(); !lid.empty()) { - return boost::uuids::string_generator()(lid); - } + if (auto const lid = raft_ctx->raft_leader_id(); !lid.empty()) { return boost::uuids::string_generator()(lid); } LOGWARN("No leader for the raft group {}", group_id); return {}; } @@ -194,7 +192,7 @@ result< void > raft_service::srv_recover_partition(boost::uuids::uuid const& gro return fail(craft_error::INTERNAL); } auto mgr = std::make_shared< raft_state_mgr >(nuraft_mesg::to_server_id(server_uuid_), server_uuid_, group_id, - commit_cb_, registry_mgr_); + commit_cb_, registry_mgr_); if (auto const r = consensus_->join_group(group_id, default_group_type_, mgr); !r) { LOGERROR("srv_recover_partition[{}]: join_group failed", boost::uuids::to_string(group_id)); return fail(craft_error::INTERNAL);