libdpf/test/tests/net_control_test.cpp

1093 lines
37 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "dpf/app_flow.hpp"
#include "dpf/compose.hpp"
#include "dpf/compose_async.hpp"
#include "dpf/launch.hpp"
#include "dpf/net/async_round_sink.hpp"
#include "dpf/net/async_sctp_stream_array.hpp"
#include "dpf/net/async_stream_array.hpp"
#include "dpf/net/connect.hpp"
#include "dpf/net/party_session.hpp"
#include "dpf/net/policy.hpp"
#include "dpf/net/round_lane.hpp"
#include "dpf/net/stream_array.hpp"
#include "dpf/net/sync_stream_array.hpp"
#include "dpf/online_session.hpp"
#include "dpf/party_run.hpp"
#include "dpf/party_runner.hpp"
#include "dpf/run_config.hpp"
namespace
{
using dpf::protocol::domain;
using clock_type = std::chrono::steady_clock;
void put(std::vector<std::vector<std::uint8_t>> & values, dpf::protocol::node n,
std::uint64_t v)
{
if (values.size() <= n.id)
values.resize(n.id + 1);
values[n.id].assign(8, 0);
std::memcpy(values[n.id].data(), &v, 8);
}
std::uint64_t get(const std::vector<std::vector<std::uint8_t>> & values,
dpf::protocol::node n, std::size_t lane = 0)
{
std::uint64_t v = 0;
std::memcpy(&v, values[n.id].data() + 8 * lane, 8);
return v;
}
/// Pump `io` until `n` of the counted completions arrive or 5 s pass.
void pump(asio::io_context & io, std::atomic<int> & left)
{
const auto deadline = clock_type::now() + std::chrono::seconds(5);
while (left.load() > 0 && clock_type::now() < deadline)
{
if (io.stopped())
io.restart();
io.run_one_for(std::chrono::milliseconds(10));
}
}
void step(asio::io_context & io)
{
if (io.stopped())
io.restart();
io.run_one_for(std::chrono::milliseconds(5));
}
unsigned short free_port()
{
asio::io_context io;
asio::ip::tcp::acceptor a(io);
dpf::net::open_listener(a, 0);
return a.local_endpoint().port();
}
/// Two parties: x0 + x1 opened, then `k` dependent steps (+1 and reopen).
struct chain
{
dpf::protocol::plan plan;
dpf::protocol::node x;
dpf::protocol::node out;
};
constexpr std::uint32_t k_inc = 94001;
chain make_chain(std::size_t party, int steps)
{
dpf::protocol::composer c(party);
chain ch;
ch.x = c.input(domain::a, 8);
auto cur = ch.x;
for (int i = 0; i < steps; ++i)
{
auto e = c.exchange(cur);
cur = c.compute(k_inc, {e}, domain::a, 8);
}
ch.out = cur;
ch.plan = c.schedule();
return ch;
}
dpf::protocol::kernel_fn inc_kernel(std::chrono::milliseconds sleep = {})
{
return [sleep](std::uint32_t, const std::vector<dpf::protocol::node> &,
const std::vector<dpf::protocol::block_span> & inputs,
dpf::protocol::block_span output, std::size_t lanes) {
if (sleep.count() > 0)
std::this_thread::sleep_for(sleep);
for (std::size_t l = 0; l < lanes; ++l)
{
std::uint64_t v = 0;
std::memcpy(&v, inputs[0].at(l), 8);
v += 1;
std::memcpy(output.at(l), &v, 8);
}
};
}
/// Expected result of `make_chain` from inputs a, b.
std::uint64_t chain_value(std::uint64_t a, std::uint64_t b, int steps)
{
// Each step opens (both parties hold the prior value) and adds 1.
std::uint64_t v = a + b + 1;
for (int i = 1; i < steps; ++i)
v = 2 * v + 1;
return v;
}
} // namespace
// ---------------------------------------------------------------------------
// Explicit configuration
// ---------------------------------------------------------------------------
TEST(RunConfig, EnvAndArgsShareKeys)
{
::setenv("DPF_LANES", "4", 1);
::setenv("DPF_WIRE_WINDOW", "65536", 1);
auto cfg = dpf::app::run_config::from_env();
::unsetenv("DPF_LANES");
::unsetenv("DPF_WIRE_WINDOW");
EXPECT_EQ(cfg.n_lanes, 4u);
EXPECT_EQ(cfg.policy.window_bytes, 65536u);
const char * argv[] = {"x", "--transport=parallel", "--framing=always",
"--instances=3", "--sndbuf=262144", "--trials=5", "positional"};
const auto rest = cfg.apply_args(7, const_cast<char **>(argv));
ASSERT_EQ(rest.size(), 1u);
EXPECT_EQ(rest[0], "positional");
EXPECT_EQ(cfg.kind, dpf::net::transport::parallel);
EXPECT_EQ(cfg.framing, dpf::net::framing_mode::always);
EXPECT_EQ(cfg.instances, 3u);
EXPECT_EQ(cfg.policy.socket.send_buffer, 262144);
EXPECT_EQ(cfg.trials, 5u);
EXPECT_NE(cfg.summary().find("transport=parallel"), std::string::npos);
EXPECT_THROW(cfg.set("lanse", "3"), std::invalid_argument);
EXPECT_THROW(cfg.set("lanes", "three"), std::invalid_argument);
EXPECT_THROW(cfg.set("transport", "udp"), std::invalid_argument);
EXPECT_THROW(cfg.set("chunk", std::to_string(std::size_t{64} << 20)),
std::invalid_argument);
}
TEST(RoundLane, FramingModesAndLimits)
{
dpf::net::round_lane_map a(4, 3, dpf::net::framing_mode::always);
EXPECT_TRUE(a.framed);
EXPECT_EQ(a.lane(2), 2u);
dpf::net::round_lane_map b(2, 6);
EXPECT_TRUE(b.framed);
EXPECT_EQ(b.lane(5), 1u);
EXPECT_THROW(dpf::net::round_lane_map(2, 6, dpf::net::framing_mode::never),
std::invalid_argument);
EXPECT_THROW(dpf::net::round_lane_map(1, 70000), std::invalid_argument);
EXPECT_EQ(dpf::net::lane_count_for_rounds(20), 8u);
EXPECT_EQ(dpf::net::lane_count_for_rounds(20, dpf::net::lanes_one_per_round),
20u);
EXPECT_EQ(dpf::net::lane_count_for_rounds(3, 8), 3u);
}
// ---------------------------------------------------------------------------
// Sink hello and framing
// ---------------------------------------------------------------------------
namespace
{
std::string sink_mismatch(std::vector<std::size_t> s0, std::vector<std::size_t> s1,
std::size_t c0, std::size_t c1, dpf::net::framing_mode f0,
dpf::net::framing_mode f1)
{
asio::io_context io;
auto ends = dpf::net::make_async_memory_stream_pair(io, 4);
dpf::net::sink_options o0, o1;
o0.framing = f0;
o1.framing = f1;
dpf::net::async_round_sink a(ends.first, s0, c0, o0);
dpf::net::async_round_sink b(ends.second, s1, c1, o1);
const auto deadline = clock_type::now() + std::chrono::seconds(2);
while (clock_type::now() < deadline)
{
try
{
(void)a.peer_ready(0, 0);
(void)b.peer_ready(0, 0);
}
catch (const std::exception & e)
{
return e.what();
}
step(io);
}
return {};
}
} // namespace
TEST(Sink, HelloNamesTheDisagreement)
{
using fm = dpf::net::framing_mode;
auto rounds = sink_mismatch({8, 8}, {8}, 1, 1, fm::automatic, fm::automatic);
EXPECT_NE(rounds.find("rounds"), std::string::npos) << rounds;
auto widths = sink_mismatch({8, 8}, {8, 16}, 1, 1, fm::automatic, fm::automatic);
EXPECT_NE(widths.find("slot widths"), std::string::npos) << widths;
auto inst = sink_mismatch({8}, {8}, 2, 1, fm::automatic, fm::automatic);
EXPECT_NE(inst.find("instances 1 vs 2"), std::string::npos) << inst;
auto frame = sink_mismatch({8}, {8}, 1, 1, fm::always, fm::automatic);
EXPECT_NE(frame.find("framing"), std::string::npos) << frame;
}
TEST(Sink, FramedPartialPrefixReadyPerInstance)
{
asio::io_context io;
auto ends = dpf::net::make_async_memory_stream_pair(io, 1);
dpf::net::sink_options so;
so.framing = dpf::net::framing_mode::always;
dpf::net::async_round_sink a(ends.first, {8}, 4, so);
dpf::net::async_round_sink b(ends.second, {8}, 4, so);
std::uint8_t slot[8] = {1};
a.submit(0, 0, slot, 8);
a.submit(0, 1, slot, 8);
a.flush_round(0);
const auto deadline = clock_type::now() + std::chrono::seconds(2);
while (!b.peer_ready(0, 1) && clock_type::now() < deadline)
step(io);
EXPECT_TRUE(b.peer_ready(0, 0));
EXPECT_TRUE(b.peer_ready(0, 1));
EXPECT_FALSE(b.peer_ready(0, 2));
a.submit(0, 2, slot, 8);
a.submit(0, 3, slot, 8);
a.flush_round(0);
while (!b.peer_ready(0, 3) && clock_type::now() < deadline)
step(io);
EXPECT_TRUE(b.peer_ready(0, 3));
EXPECT_TRUE(a.framed());
}
TEST(Sink, PeerFailureSurfacesImmediately)
{
asio::io_context io;
auto ends = dpf::net::make_async_memory_stream_pair(io, 2);
dpf::net::async_round_sink a(ends.first, {8, 8}, 1);
{
dpf::net::async_round_sink b(ends.second, {8, 8}, 1);
for (int i = 0; i < 50; ++i)
io.poll();
}
ends.second.close();
const auto t0 = clock_type::now();
bool threw = false;
try
{
for (int i = 0; i < 1000 && !threw; ++i)
{
(void)a.peer_ready(0, 0);
a.wait_io_for(std::chrono::milliseconds(5));
}
}
catch (const std::system_error &)
{
threw = true;
}
EXPECT_TRUE(threw);
EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(2));
}
// ---------------------------------------------------------------------------
// Drive-loop budgets
// ---------------------------------------------------------------------------
TEST(Drive, HealthyPlanLongerThanWaitTimeout)
{
const int steps = 6;
auto c0 = make_chain(0, steps);
auto c1 = make_chain(1, steps);
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{
{k_inc, inc_kernel(std::chrono::milliseconds(25))}};
std::vector<std::vector<std::uint8_t>> v0(c0.plan.nodes().size()),
v1(c1.plan.nodes().size());
put(v0, c0.x, 1);
put(v1, c1.x, 2);
dpf::protocol::drive_options opt;
opt.wait_timeout = std::chrono::milliseconds(100);
const auto t0 = clock_type::now();
dpf::protocol::drive_both_on_async_streams(c0.plan, c1.plan, v0, v1, k, 1, opt);
EXPECT_GT(clock_type::now() - t0, std::chrono::milliseconds(120));
EXPECT_EQ(get(v0, c0.out), chain_value(1, 2, steps));
}
TEST(Drive, EdgeAndRoundBudgetsNameTheWait)
{
auto c0 = make_chain(0, 2);
asio::io_context io0, io1;
auto ends = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 2);
std::vector<std::vector<std::uint8_t>> v0(c0.plan.nodes().size());
put(v0, c0.x, 1);
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_inc, inc_kernel()}};
dpf::protocol::drive_options edge;
edge.wait_timeout = std::chrono::seconds(20);
edge.edge_timeout[dpf::net::edge_peer] = std::chrono::milliseconds(40);
auto t0 = clock_type::now();
try
{
dpf::protocol::drive_plan_on_async_streams(c0.plan, ends.first, v0, k, 0, 1,
edge);
FAIL() << "one-sided drive should time out";
}
catch (const std::runtime_error & e)
{
const std::string what = e.what();
EXPECT_NE(what.find("peer"), std::string::npos) << what;
EXPECT_NE(what.find("budget 40"), std::string::npos) << what;
}
EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(5));
auto ends2 = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 2);
std::vector<std::vector<std::uint8_t>> v1(c0.plan.nodes().size());
put(v1, c0.x, 1);
dpf::protocol::drive_options round;
round.wait_timeout = std::chrono::seconds(20);
round.round_timeout[0] = std::chrono::milliseconds(30);
t0 = clock_type::now();
EXPECT_THROW(dpf::protocol::drive_plan_on_async_streams(c0.plan, ends2.first,
v1, k, 0, 1, round),
std::runtime_error);
EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(5));
}
// ---------------------------------------------------------------------------
// Backends: windows, stats, scheduling, graceful close
// ---------------------------------------------------------------------------
TEST(Memory, WindowSetterAndGracefulClose)
{
asio::io_context io;
auto ends = dpf::net::make_async_memory_stream_pair(io, 1);
ends.first.set_window_bytes(100);
EXPECT_EQ(ends.first.window_bytes(), 100u);
std::vector<std::uint8_t> a(300, 1), b(300, 2);
std::atomic<int> left{2};
ends.first.async_write(0, a.data(), a.size(), [&](auto) { --left; });
ends.first.async_write(0, b.data(), b.size(), [&](auto) { --left; });
EXPECT_EQ(ends.first.buffered_bytes(), 600u);
ends.first.close();
std::vector<std::uint8_t> got(600);
std::atomic<int> rd{1};
std::error_code rec;
ends.second.async_read(0, got.data(), got.size(), [&](const std::error_code & ec) {
rec = ec;
--rd;
});
pump(io, rd);
EXPECT_FALSE(rec);
EXPECT_EQ(got[0], 1);
EXPECT_EQ(got[599], 2);
const auto st = ends.first.stats();
EXPECT_EQ(st.payload_out, 600u);
EXPECT_TRUE(st.closed);
}
namespace
{
/// Localhost mux pair, both ends on `io`.
struct mux_pair
{
std::unique_ptr<dpf::net::async_mux_stream_array> a;
std::unique_ptr<dpf::net::async_mux_stream_array> b;
};
mux_pair make_mux_pair(asio::io_context & io, std::size_t lanes,
const dpf::net::wire_policy & pol = {})
{
asio::ip::tcp::acceptor acc(io);
dpf::net::open_listener(acc, 0);
asio::ip::tcp::socket s0(io), s1(io);
std::thread t([&] {
dpf::net::connect_until(s1, "127.0.0.1", acc.local_endpoint().port(),
std::chrono::seconds(5));
});
dpf::net::accept_until(acc, s0, std::chrono::seconds(5));
t.join();
mux_pair out;
out.a = std::make_unique<dpf::net::async_mux_stream_array>(io, std::move(s0),
0, 1, lanes, pol);
out.b = std::make_unique<dpf::net::async_mux_stream_array>(io, std::move(s1),
1, 0, lanes, pol);
return out;
}
} // namespace
TEST(Mux, StatsCountHeaders)
{
asio::io_context io;
auto m = make_mux_pair(io, 2);
std::vector<std::uint8_t> out(100, 7), in(100);
std::atomic<int> left{2};
m.a->async_write(0, out.data(), out.size(), [&](auto) { --left; });
m.b->async_read(0, in.data(), in.size(), [&](auto) { --left; });
pump(io, left);
const auto s = m.a->stats();
EXPECT_EQ(s.payload_out, 100u);
EXPECT_EQ(s.bytes_out, 110u);
EXPECT_EQ(s.frames_out, 1u);
EXPECT_EQ(in, out);
}
TEST(Mux, SmallWriteIsNotQueuedBehindLargeWrite)
{
asio::io_context io;
dpf::net::wire_policy pol;
pol.chunk_bytes = 16 << 10;
pol.window_bytes = 0;
auto m = make_mux_pair(io, 2, pol);
std::vector<std::uint8_t> big(8u << 20, 1), small(8, 2);
std::vector<std::uint8_t> big_in(big.size()), small_in(8);
std::atomic<int> left{2};
std::atomic<int> order{0};
int big_at = 0, small_at = 0;
m.a->async_write(0, big.data(), big.size(), [](auto) {});
m.a->async_write(1, small.data(), small.size(), [](auto) {});
m.b->async_read(0, big_in.data(), big_in.size(), [&](auto) {
big_at = ++order;
--left;
});
m.b->async_read(1, small_in.data(), small_in.size(), [&](auto) {
small_at = ++order;
--left;
});
pump(io, left);
EXPECT_EQ(small_at, 1);
EXPECT_EQ(big_at, 2);
EXPECT_EQ(small_in, small);
EXPECT_EQ(big_in, big);
EXPECT_GT(m.a->stats().frames_out, 100u);
}
TEST(Mux, DestroyAfterWriteStillDelivers)
{
asio::io_context io;
auto m = make_mux_pair(io, 1);
std::vector<std::uint8_t> out(4u << 20, 9), in(out.size());
m.a->async_write(0, out.data(), out.size(), [](auto) {});
m.a.reset();
std::atomic<int> left{1};
std::error_code rec;
m.b->async_read(0, in.data(), in.size(), [&](const std::error_code & ec) {
rec = ec;
--left;
});
pump(io, left);
EXPECT_FALSE(rec);
EXPECT_EQ(in, out);
}
TEST(Parallel, PerLaneWindows)
{
asio::io_context io;
std::atomic<unsigned short> port{0};
std::vector<asio::ip::tcp::socket> a, b;
std::thread t([&] { a = dpf::net::accept_parallel_tcp(io, port, 2); });
while (port.load() == 0)
std::this_thread::yield();
b = dpf::net::connect_parallel_tcp(io, "127.0.0.1", port, 2);
t.join();
dpf::net::wire_policy pol;
pol.window_bytes = 1000;
dpf::net::async_parallel_stream_array pa(io, std::move(a), pol);
dpf::net::async_parallel_stream_array pb(io, std::move(b), pol);
EXPECT_EQ(pa.lane_window_bytes(0), 1000u);
EXPECT_EQ(pa.window_bytes(), 2000u);
pa.set_window_bytes(500);
EXPECT_EQ(pa.lane_window_bytes(1), 500u);
std::vector<std::uint8_t> out(64, 3), in(64);
std::atomic<int> left{2};
pa.async_write(1, out.data(), out.size(), [&](auto) { --left; });
pb.async_read(1, in.data(), in.size(), [&](auto) { --left; });
pump(io, left);
EXPECT_EQ(in, out);
EXPECT_EQ(pa.stats().bytes_out, 68u);
}
// ---------------------------------------------------------------------------
// Synchronous mux: same wire as async mux, no flush deadlock
// ---------------------------------------------------------------------------
TEST(SyncMux, InteroperatesWithAsyncMux)
{
// Blocking sync faces need one side to read first so the peer's write can
// complete (the old fd mux pumped both directions while blocked).
std::atomic<unsigned short> port{0};
std::vector<std::uint8_t> from_sync(3000, 5), from_async(5000, 6);
std::vector<std::uint8_t> got_sync(from_async.size()), got_async(from_sync.size());
std::exception_ptr err;
std::mutex err_mu;
auto note = [&](std::exception_ptr e) {
std::lock_guard<std::mutex> lock(err_mu);
if (!err)
err = std::move(e);
};
std::thread sync_side([&] {
try
{
dpf::run::tcp_pair_mux(1, "127.0.0.1", port, 2,
[&](unsigned, dpf::net::mux_stream_array & mux) {
mux.read(0, got_sync.data(), got_sync.size());
mux.write(1, from_sync.data(), from_sync.size());
mux.flush(1);
});
}
catch (...)
{
note(std::current_exception());
}
});
try
{
dpf::run::tcp_pair_mux(0, "127.0.0.1", port, 2,
[&](unsigned, dpf::net::mux_stream_array & mux) {
mux.write(0, from_async.data(), from_async.size());
mux.flush(0);
mux.read(1, got_async.data(), got_async.size());
});
}
catch (...)
{
note(std::current_exception());
}
sync_side.join();
if (err)
std::rethrow_exception(err);
EXPECT_EQ(got_async, from_sync);
EXPECT_EQ(got_sync, from_async);
}
TEST(SyncMux, LargeCrossFlushDoesNotDeadlock)
{
std::atomic<unsigned short> port{0};
std::vector<std::uint8_t> big(4u << 20);
for (std::size_t i = 0; i < big.size(); ++i)
big[i] = static_cast<std::uint8_t>(i);
std::atomic<int> ok{0};
auto side = [&](unsigned party) {
dpf::run::tcp_pair_mux(party, "127.0.0.1", port, 1,
[&](unsigned, dpf::net::mux_stream_array & mux) {
mux.write(0, big.data(), big.size());
mux.flush(0);
std::vector<std::uint8_t> in(big.size());
mux.read(0, in.data(), in.size());
if (in == big)
++ok;
});
};
std::thread t0([&] { side(0); });
std::thread t1([&] { side(1); });
t0.join();
t1.join();
EXPECT_EQ(ok.load(), 2);
}
// ---------------------------------------------------------------------------
// Sessions: static tables, deadlines, per-edge transport, reconnect
// ---------------------------------------------------------------------------
TEST(Session, ConnectHasADeadline)
{
asio::io_context io;
asio::ip::tcp::socket s(io);
const auto port = free_port();
const auto t0 = clock_type::now();
EXPECT_THROW(dpf::net::connect_until(s, "127.0.0.1", port,
std::chrono::milliseconds(200)),
std::system_error);
EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(2));
}
TEST(Session, StaticTableAnyStartOrder)
{
const std::vector<dpf::net::peer_address> table = {
{"127.0.0.1", free_port()}, {"127.0.0.1", free_port()}};
std::atomic<int> ok{0};
auto run = [&](unsigned me, std::chrono::milliseconds delay) {
std::this_thread::sleep_for(delay);
asio::io_context io;
dpf::net::party_session s(io, me, 2, std::size_t{1});
s.join(table);
std::uint64_t mine = me + 10, got = 0;
std::atomic<int> left{2};
s.peer(1 - me).async_write(0, &mine, 8, [&](auto) { --left; });
s.peer(1 - me).async_read(0, &got, 8, [&](auto) { --left; });
pump(io, left);
if (got == 11u - me)
++ok;
};
std::thread late([&] { run(0, std::chrono::milliseconds(300)); });
std::thread early([&] { run(1, std::chrono::milliseconds(0)); });
late.join();
early.join();
EXPECT_EQ(ok.load(), 2);
}
TEST(Session, TransportMismatchIsNamed)
{
auto ports = dpf::net::make_mesh_ports(2);
std::string e0, e1;
auto run = [&](unsigned me, dpf::net::transport t, std::string & err) {
try
{
asio::io_context io;
dpf::net::session_options so;
so.n_lanes = 2;
so.limits.accept = std::chrono::milliseconds(2000);
dpf::net::party_session s(io, me, 2, so);
s.set_edge_transport(1 - me, t);
s.join("127.0.0.1", ports);
}
catch (const std::exception & e)
{
err = e.what();
}
};
std::thread t0([&] { run(0, dpf::net::transport::mux, e0); });
std::thread t1([&] { run(1, dpf::net::transport::parallel, e1); });
t0.join();
t1.join();
const std::string both = e0 + " | " + e1;
EXPECT_NE(both.find("transport"), std::string::npos) << both;
}
TEST(Session, ParallelAndSctpEdges)
{
std::vector<dpf::net::transport> kinds = {dpf::net::transport::parallel};
if (dpf::net::sctp_available())
kinds.push_back(dpf::net::transport::sctp);
for (auto kind : kinds)
{
auto ports = dpf::net::make_mesh_ports(2);
std::atomic<int> ok{0};
std::string err;
std::mutex mu;
auto run = [&](unsigned me) {
try
{
asio::io_context io;
dpf::net::session_options so;
so.n_lanes = 3;
so.kind = kind;
// SCTP links cannot be encrypted.
so.security.encrypt = kind != dpf::net::transport::sctp;
dpf::net::party_session s(io, me, 2, so);
s.join("127.0.0.1", ports);
std::vector<std::uint8_t> out(1000, static_cast<std::uint8_t>(me + 1));
std::vector<std::uint8_t> in(1000);
std::atomic<int> left{2};
s.peer(1 - me).async_write(2, out.data(), out.size(), [&](auto) { --left; });
s.peer(1 - me).async_read(2, in.data(), in.size(), [&](auto) { --left; });
pump(io, left);
if (in[0] == static_cast<std::uint8_t>(2 - me)
&& s.edge_stats(1 - me).payload_out == 1000u)
++ok;
}
catch (const std::exception & e)
{
std::lock_guard<std::mutex> lock(mu);
err = e.what();
}
};
std::thread t0([&] { run(0); });
std::thread t1([&] { run(1); });
t0.join();
t1.join();
EXPECT_EQ(ok.load(), 2) << dpf::net::transport_name(kind) << ": " << err;
}
}
TEST(Session, ReconnectResumesMidPlan)
{
const int steps = 5;
auto c0 = make_chain(0, steps);
auto c1 = make_chain(1, steps);
auto ports = dpf::net::make_mesh_ports(2);
std::uint64_t result[2] = {0, 0};
std::uint64_t resumes[2] = {0, 0};
std::string err;
std::mutex mu;
auto run = [&](unsigned me, const chain & ch) {
try
{
asio::io_context io;
dpf::net::party_session s(io, me, 2, std::size_t{steps});
s.join("127.0.0.1", ports);
int calls = 0;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{
{k_inc,
[&, base = inc_kernel()](std::uint32_t op,
const std::vector<dpf::protocol::node> & ns,
const std::vector<dpf::protocol::block_span> & in,
dpf::protocol::block_span out, std::size_t lanes) {
if (me == 0 && ++calls == 2)
s.peer(1).close();
base(op, ns, in, out, lanes);
}}};
dpf::net::sink_options so;
so.reconnect = s.reconnector(1 - me);
dpf::net::async_round_sink sink(s.peer(1 - me), ch.plan.slot_bytes_all(),
1, so);
std::vector<std::vector<std::uint8_t>> v(ch.plan.nodes().size());
put(v, ch.x, me + 1);
dpf::protocol::drive_via_schedule(ch.plan, sink, v, k, me);
result[me] = get(v, ch.out);
resumes[me] = sink.stats().resumes;
}
catch (const std::exception & e)
{
std::lock_guard<std::mutex> lock(mu);
err += std::string(e.what()) + "; ";
}
};
std::thread t0([&] { run(0, c0); });
std::thread t1([&] { run(1, c1); });
t0.join();
t1.join();
ASSERT_TRUE(err.empty()) << err;
EXPECT_EQ(result[0], chain_value(1, 2, steps));
EXPECT_EQ(result[1], chain_value(1, 2, steps));
EXPECT_EQ(resumes[0], 1u);
EXPECT_EQ(resumes[1], 1u);
}
TEST(Session, DealerLinkHasItsOwnEpoch)
{
asio::io_context dio, pio;
std::atomic<unsigned short> port{0};
std::string err;
std::mutex mu;
std::thread dealer([&] {
try
{
dpf::net::dealer_session d(dio, 1);
port.store(d.listen());
d.accept_parties();
std::uint64_t v = 5;
std::atomic<int> left{1};
d.party(0).async_write(0, &v, 8, [&](auto) { --left; });
pump(dio, left);
d.reconnect(0);
v = 6;
left = 1;
d.party(0).async_write(0, &v, 8, [&](auto) { --left; });
pump(dio, left);
}
catch (const std::exception & e)
{
std::lock_guard<std::mutex> lock(mu);
err += e.what();
}
});
while (port.load() == 0)
std::this_thread::yield();
dpf::net::party_session p(pio, 0, 2, std::size_t{1});
p.connect_dealer("127.0.0.1", port.load());
std::uint64_t got = 0;
std::atomic<int> left{1};
p.dealer().async_read(0, &got, 8, [&](auto) { --left; });
pump(pio, left);
EXPECT_EQ(got, 5u);
p.reconnect_dealer();
left = 1;
p.dealer().async_read(0, &got, 8, [&](auto) { --left; });
pump(pio, left);
dealer.join();
EXPECT_TRUE(err.empty()) << err;
EXPECT_EQ(got, 6u);
}
// ---------------------------------------------------------------------------
// N-party runner and harness
// ---------------------------------------------------------------------------
TEST(Runner, TwoPartyOnEveryTransport)
{
std::vector<dpf::net::transport> kinds = {dpf::net::transport::async_memory,
dpf::net::transport::local, dpf::net::transport::mux,
dpf::net::transport::parallel};
if (dpf::net::sctp_available())
kinds.push_back(dpf::net::transport::sctp);
const int steps = 4;
for (auto kind : kinds)
{
auto c0 = make_chain(0, steps);
auto c1 = make_chain(1, steps);
dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size());
put(v0, c0.x, 3);
put(v1, c1.x, 4);
dpf::app::run_config cfg;
cfg.kind = kind;
cfg.n_lanes = 2;
cfg.security.encrypt = kind != dpf::net::transport::sctp;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_inc, inc_kernel()}};
const auto r = dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg);
EXPECT_EQ(get(v0, c0.out), chain_value(3, 4, steps))
<< dpf::net::transport_name(kind);
EXPECT_EQ(get(v1, c1.out), chain_value(3, 4, steps))
<< dpf::net::transport_name(kind);
EXPECT_GT(r.wire[0].payload_out, 0u) << dpf::net::transport_name(kind);
if (kind == dpf::net::transport::async_memory || kind == dpf::net::transport::sctp)
EXPECT_EQ(r.wire[0].bytes_out, r.wire[0].payload_out);
else
EXPECT_GT(r.wire[0].bytes_out, r.wire[0].payload_out)
<< dpf::net::transport_name(kind);
}
}
TEST(Runner, InstancesAndComputePool)
{
const int steps = 3;
auto c0 = make_chain(0, steps);
auto c1 = make_chain(1, steps);
dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size());
v0[c0.x.id].assign(8 * 4, 0);
v1[c1.x.id].assign(8 * 4, 0);
for (std::size_t l = 0; l < 4; ++l)
{
std::uint64_t a = l, b = 10;
std::memcpy(v0[c0.x.id].data() + 8 * l, &a, 8);
std::memcpy(v1[c1.x.id].data() + 8 * l, &b, 8);
}
dpf::app::run_config cfg;
cfg.instances = 4;
cfg.compute_threads = 2;
cfg.framing = dpf::net::framing_mode::always;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_inc, inc_kernel()}};
(void)dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg);
for (std::size_t l = 0; l < 4; ++l)
EXPECT_EQ(get(v0, c0.out, l), chain_value(l, 10, steps));
}
TEST(Runner, StarOnEveryTransport)
{
std::vector<dpf::net::transport> kinds = {dpf::net::transport::async_memory,
dpf::net::transport::local, dpf::net::transport::mux,
dpf::net::transport::parallel};
if (dpf::net::sctp_available())
kinds.push_back(dpf::net::transport::sctp);
// pirsona_bitmore_fetch(L) is a star over 2^L servers.
constexpr std::size_t L = 2;
constexpr std::size_t n = std::size_t{1} << L;
constexpr std::size_t query = 16 * L;
for (auto kind : kinds)
{
auto seeds = std::make_shared<std::vector<std::vector<std::uint8_t>>>(n);
auto answers = std::make_shared<std::vector<std::vector<std::uint8_t>>>(n);
for (std::size_t i = 0; i < n; ++i)
{
(*seeds)[i].assign(query, static_cast<std::uint8_t>(i + 1));
(*answers)[i].assign(8, static_cast<std::uint8_t>(0x40 + i));
}
auto client = dpf::protocol::pirsona_bitmore_fetch(L, 16, 8, seeds, answers);
dpf::app::run_config cfg;
cfg.kind = kind;
cfg.n_lanes = 1;
cfg.wait_timeout = std::chrono::seconds(10);
cfg.security.encrypt = kind != dpf::net::transport::sctp;
EXPECT_NO_THROW(dpf::session::drive_async_star(n, {query, 8u},
std::move(client),
[&](std::size_t i) {
return dpf::protocol::star_server_reply_rounds(query, 8, (*answers)[i]);
},
cfg))
<< dpf::net::transport_name(kind);
}
}
TEST(Runner, ShortInputIsRejectedNotZeroed)
{
auto c0 = make_chain(0, 1);
auto c1 = make_chain(1, 1);
dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size());
put(v0, c0.x, 1);
put(v1, c1.x, 2);
dpf::app::run_config cfg;
cfg.instances = 4;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_inc, inc_kernel()}};
try
{
(void)dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg);
FAIL() << "8-byte input for 4 instances should be rejected";
}
catch (const std::exception & e)
{
EXPECT_NE(std::string(e.what()).find("input node"), std::string::npos)
<< e.what();
}
}
TEST(Runner, ThreePartyDealerRingAndPeer)
{
struct built
{
dpf::protocol::plan plan;
dpf::protocol::node y, ring, pad, mask, x, open;
};
auto make = [](std::size_t party) {
dpf::protocol::composer c(party);
built b;
b.y = c.input(domain::y, 8);
b.ring = c.rss_from_y(b.y);
b.pad = c.input(domain::a, 8);
b.mask = c.dealer_deliver(b.pad);
b.x = c.input(domain::a, 8);
b.open = c.exchange(b.x);
b.plan = c.schedule();
return b;
};
for (auto kind : {dpf::net::transport::async_memory, dpf::net::transport::mux})
{
built b[3] = {make(0), make(1), make(2)};
std::vector<dpf::app::party_values> values(3);
for (int i = 0; i < 3; ++i)
{
values[i].resize(b[i].plan.nodes().size());
put(values[i], b[i].y, 100 + i);
put(values[i], b[i].pad, i == 2 ? 77 : 0);
put(values[i], b[i].x, i == 0 ? 20 : 22);
}
dpf::app::run_config cfg;
cfg.kind = kind;
(void)dpf::app::run_parties({b[0].plan, b[1].plan, b[2].plan}, values, {},
cfg);
for (int i = 0; i < 3; ++i)
{
std::uint64_t own = 0, next = 0;
std::memcpy(&own, values[i][b[i].ring.id].data(), 8);
std::memcpy(&next, values[i][b[i].ring.id].data() + 8, 8);
EXPECT_EQ(own, 100u + i) << dpf::net::transport_name(kind);
EXPECT_EQ(next, 100u + (i + 1) % 3) << dpf::net::transport_name(kind);
}
for (int i = 0; i < 2; ++i)
{
EXPECT_EQ(get(values[i], b[i].mask), 77u);
EXPECT_EQ(get(values[i], b[i].open), 42u);
}
}
}
TEST(Runner, SeparateProcessesFromAStaticTable)
{
const std::vector<std::string> peers = {
"127.0.0.1:" + std::to_string(free_port()),
"127.0.0.1:" + std::to_string(free_port())};
const std::string list = peers[0] + "," + peers[1];
const int steps = 3;
pid_t kids[2];
for (int me = 0; me < 2; ++me)
{
const pid_t pid = ::fork();
ASSERT_GE(pid, 0);
if (pid == 0)
{
int code = 1;
try
{
const std::string party = "--party=" + std::to_string(me);
const std::string pl = "--peers=" + list;
const char * argv[] = {"node", party.c_str(), pl.c_str(),
"--transport=mux", "--lanes=2"};
const auto args = dpf::app::parse_node_args(5,
const_cast<char **>(argv), dpf::app::run_config{});
auto ch = make_chain(static_cast<std::size_t>(me), steps);
dpf::app::party_values v(ch.plan.nodes().size());
put(v, ch.x, me == 0 ? 5 : 6);
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{
{k_inc, inc_kernel()}};
(void)dpf::app::run_node(args, ch.plan, v, k);
code = get(v, ch.out) == chain_value(5, 6, steps) ? 0 : 2;
}
catch (const std::exception & e)
{
std::fprintf(stderr, "party %d: %s\n", me, e.what());
code = 3;
}
::_exit(code);
}
kids[me] = pid;
}
for (int me = 0; me < 2; ++me)
{
int status = 0;
ASSERT_EQ(::waitpid(kids[me], &status, 0), kids[me]);
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0) << "party " << me;
}
}
TEST(Harness, TrialsConfigAndWireRecorded)
{
auto c0 = make_chain(0, 2);
auto c1 = make_chain(1, 2);
std::vector<dpf::app::party_values> inputs(2);
inputs[0].resize(c0.plan.nodes().size());
inputs[1].resize(c1.plan.nodes().size());
put(inputs[0], c0.x, 1);
put(inputs[1], c1.x, 2);
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_inc, inc_kernel()}};
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::mux;
cfg.warmup = 1;
cfg.trials = 3;
dpf::experiment ex("chain", "p0");
const auto cost =
dpf::app::exercise_parties({c0.plan, c1.plan}, inputs, k, &ex, cfg);
EXPECT_EQ(ex.trials().size(), 3u);
EXPECT_GT(cost.wall_ns, 0u);
EXPECT_GT(cost.wire_out, cost.bytes);
EXPECT_EQ(ex.wire().bytes_out, cost.wire_out);
bool has_transport = false;
for (const auto & kv : ex.config())
has_transport = has_transport || (kv.first == "transport" && kv.second == "mux");
EXPECT_TRUE(has_transport);
const std::string dir = "/tmp/libdpf_net_control_" + std::to_string(::getpid());
ex.write_csv(dir);
for (const char * f : {"/config.csv", "/trials.csv", "/wire.csv"})
{
std::ifstream in(dir + f);
EXPECT_TRUE(static_cast<bool>(in)) << f;
}
(void)::system(("rm -rf '" + dir + "'").c_str());
}
TEST(Prep, ShippedOverADealerSession)
{
dpf::prep::demand d;
d.ring_triples = 2;
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::mux;
auto shipped = dpf::session::ship_prep(d, cfg);
std::uint8_t a0[8], b0[8], c0[8], a1[8], b1[8], c1[8];
shipped.party0.take_ring(a0, b0, c0);
shipped.party1.take_ring(a1, b1, c1);
std::uint64_t ta[2], tb[2], tc[2];
std::memcpy(&ta[0], a0, 8);
std::memcpy(&tb[0], b0, 8);
std::memcpy(&tc[0], c0, 8);
std::memcpy(&ta[1], a1, 8);
std::memcpy(&tb[1], b1, 8);
std::memcpy(&tc[1], c1, 8);
EXPECT_EQ((ta[0] + ta[1]) * (tb[0] + tb[1]), tc[0] + tc[1]);
}
TEST(Sink, WindowGatesPipelining)
{
asio::io_context io0, io1;
auto ends = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 1, 64);
dpf::net::sink_options so;
so.drain_timeout = std::chrono::milliseconds(300);
dpf::net::async_round_sink a(ends.first, {256}, 1, so);
std::vector<std::uint8_t> slot(256, 1);
a.submit(0, 0, slot.data(), slot.size());
EXPECT_THROW(a.flush_round(0), std::runtime_error);
EXPECT_FALSE(a.can_send_ahead());
}