/// @file dpf/net/edge_mesh.hpp /// @brief N-edge RoundSink mesh for star / dealer / 4PC topologies. #ifndef LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__ #define LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__ #include #include #include #include #include #include #include #include "dpf/net/memory_sink.hpp" #include "dpf/net/round_sink.hpp" namespace dpf { namespace net { /// @brief Index of a duplex link in an `edge_mesh`. using edge_id = std::uint16_t; /// @brief Named edges for the common 2PC / RSS / dealer trio (also mesh ids 0..2). /// @details For the dealer (party 2), `edge_dealer` is its link to party 0 and /// `edge_dealer_p1` its link to party 1. inline constexpr edge_id edge_peer = 0; inline constexpr edge_id edge_rss_next = 1; inline constexpr edge_id edge_dealer = 2; inline constexpr edge_id edge_dealer_p1 = 3; inline std::string edge_name(edge_id e) { switch (e) { case edge_peer: return "peer"; case edge_rss_next: return "rss_next"; case edge_dealer: return "dealer"; case edge_dealer_p1: return "dealer->p1"; default: return "edge " + std::to_string(e); } } /// @brief Collection of duplex RoundSinks keyed by `edge_id`. struct edge_mesh { std::vector sinks; RoundSink & at(edge_id id) const { if (static_cast(id) >= sinks.size() || sinks[id] == nullptr) throw std::logic_error("edge_mesh: edge not bound"); return *sinks[id]; } bool has(edge_id id) const noexcept { return static_cast(id) < sinks.size() && sinks[id] != nullptr; } std::size_t size() const noexcept { return sinks.size(); } void flush_all() { for (auto * s : sinks) { if (s == nullptr) continue; s->flush(); s->poll(); } } /// @brief Sum of `progress()` over distinct bound sinks. std::uint64_t progress_total() const noexcept { std::uint64_t total = 0; for (std::size_t i = 0; i < sinks.size(); ++i) { const RoundSink * s = sinks[i]; if (s == nullptr) continue; bool seen = false; for (std::size_t j = 0; j < i && !seen; ++j) seen = sinks[j] == s; if (!seen) total += s->progress(); } return total; } /// @brief Block until at least one bound sink makes I/O progress. /// @details Returns true if any sink's `wait_io()` ran a real event (an /// async sink slept in `epoll`); false if none did (memory sinks), /// so the caller can fall back to its spin guard. bool wait_io_all() { bool progressed = false; for (auto * s : sinks) { if (s == nullptr) continue; if (s->wait_io()) progressed = true; } return progressed; } }; /// @brief In-process star: one client edge per server, matching server ends. struct memory_star { std::size_t servers = 0; std::vector> hubs; std::vector client; ///< client side of edge i std::vector server; ///< server i side of edge i /// @brief Client mesh: edges `[0, servers)`. edge_mesh client_mesh() { edge_mesh m; m.sinks.resize(client.size()); for (std::size_t i = 0; i < client.size(); ++i) m.sinks[i] = &client[i]; return m; } /// @brief Server `i` mesh with a single live edge at `edge_id{i}` (sparse). /// Prefer `server_edge(i)` when the schedule uses `edge_id{0}` locally. edge_mesh server_mesh_at(std::size_t i) { if (i >= server.size()) throw std::out_of_range("memory_star server"); edge_mesh m; m.sinks.assign(server.size(), nullptr); m.sinks[i] = &server[i]; return m; } /// @brief Server `i` as a one-edge mesh (`edge_id` 0 → that duplex). edge_mesh server_edge(std::size_t i) { if (i >= server.size()) throw std::out_of_range("memory_star server"); return edge_mesh{{&server[i]}}; } }; /// @brief Build an in-process client↔N-server star. /// @param slot_bytes Round widths shared by every edge (same schedule shape). inline memory_star make_memory_star(std::size_t n_servers, std::size_t count, std::vector slot_bytes) { if (n_servers < 2) throw std::invalid_argument("make_memory_star needs >= 2 servers"); memory_star star; star.servers = n_servers; star.hubs.reserve(n_servers); star.client.reserve(n_servers); star.server.reserve(n_servers); for (std::size_t i = 0; i < n_servers; ++i) { auto hub = std::make_shared(count, slot_bytes); star.hubs.push_back(hub); star.client.emplace_back(hub, true); star.server.emplace_back(hub, false); } return star; } /// @brief Fully connected memory clique of `n` roles (every unordered pair). /// @details Edge id for ordered pair (a,b) with a> hubs; /// hubs[edge], ends[edge].first = lower role, .second = higher role std::vector> ends; static edge_id pair_edge(std::size_t a, std::size_t b, std::size_t n) { if (a == b || a >= n || b >= n) throw std::invalid_argument("memory_clique pair"); if (a > b) std::swap(a, b); // Index among pairs (i,j) with i(e + (n - 1 - i)); e = static_cast(e + (b - a - 1)); return e; } RoundSink & end(std::size_t role, std::size_t peer) { const auto e = pair_edge(role, peer, roles); if (role < peer) return ends[e].first; return ends[e].second; } edge_mesh mesh_for(std::size_t role) { edge_mesh m; m.sinks.assign(ends.size(), nullptr); for (std::size_t p = 0; p < roles; ++p) { if (p == role) continue; m.sinks[pair_edge(role, p, roles)] = &end(role, p); } return m; } }; inline memory_clique make_memory_clique(std::size_t n_roles, std::size_t count, std::vector slot_bytes) { if (n_roles < 2) throw std::invalid_argument("make_memory_clique needs >= 2 roles"); memory_clique c; c.roles = n_roles; const std::size_t n_edges = n_roles * (n_roles - 1) / 2; c.hubs.reserve(n_edges); c.ends.reserve(n_edges); for (std::size_t e = 0; e < n_edges; ++e) { auto hub = std::make_shared(count, slot_bytes); c.hubs.push_back(hub); c.ends.emplace_back(memory_sink(hub, true), memory_sink(hub, false)); } return c; } } // namespace net } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__