877 lines
33 KiB
C++
877 lines
33 KiB
C++
|
|
/// @file dpf/net/async_round_sink.hpp
|
||
|
|
/// @brief Event-driven `RoundSink` over an `async_stream_array`.
|
||
|
|
/// @details Both ends first exchange a hello on lane 0 carrying the plan shape
|
||
|
|
/// (rounds, slot widths, lanes, instances, framing) and an epoch. A
|
||
|
|
/// mismatch fails both ends with a message that names the field.
|
||
|
|
/// Unframed lanes carry one round each (round == lane). Framed lanes
|
||
|
|
/// carry `{round, nbytes}` headers; payloads are read straight into the
|
||
|
|
/// round's inbox, and a partial prefix of instances is ready as soon
|
||
|
|
/// as it lands. Every outbound round stays in memory, so after a
|
||
|
|
/// transport error the sink can take a replacement link from
|
||
|
|
/// `sink_options::reconnect`, exchange what each side received, and
|
||
|
|
/// resend only the missing bytes. The schedule above never rewinds.
|
||
|
|
#ifndef LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|
||
|
|
#define LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <array>
|
||
|
|
#include <atomic>
|
||
|
|
#include <chrono>
|
||
|
|
#include <cstddef>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <cstring>
|
||
|
|
#include <functional>
|
||
|
|
#include <memory>
|
||
|
|
#include <mutex>
|
||
|
|
#include <stdexcept>
|
||
|
|
#include <string>
|
||
|
|
#include <system_error>
|
||
|
|
#include <utility>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "dpf/net/asio_ns.hpp"
|
||
|
|
|
||
|
|
#include "dpf/net/async_stream_array.hpp"
|
||
|
|
#include "dpf/net/policy.hpp"
|
||
|
|
#include "dpf/net/round_lane.hpp"
|
||
|
|
#include "dpf/net/round_sink.hpp"
|
||
|
|
|
||
|
|
namespace dpf
|
||
|
|
{
|
||
|
|
namespace net
|
||
|
|
{
|
||
|
|
|
||
|
|
/// @brief Construction options for `async_round_sink`.
|
||
|
|
struct sink_options
|
||
|
|
{
|
||
|
|
framing_mode framing = framing_mode::automatic;
|
||
|
|
/// Exchange the plan-shape hello. Both ends must agree.
|
||
|
|
bool hello = true;
|
||
|
|
/// Bound on waiting for a full write window to drain.
|
||
|
|
std::chrono::milliseconds drain_timeout{30000};
|
||
|
|
/// Bound on the hello exchange after a reconnect.
|
||
|
|
std::chrono::milliseconds handshake_timeout{30000};
|
||
|
|
/// On a transport error, return a replacement link (the peer must replace
|
||
|
|
/// its end too) or nullptr to fail. Runs on the drive thread.
|
||
|
|
std::function<async_stream_array *(const std::error_code &)> reconnect;
|
||
|
|
unsigned max_reconnects = 3;
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief Per-sink counters.
|
||
|
|
struct sink_stats
|
||
|
|
{
|
||
|
|
std::uint64_t bytes_sent = 0;
|
||
|
|
std::uint64_t bytes_received = 0;
|
||
|
|
std::uint64_t flushes = 0;
|
||
|
|
std::uint64_t window_waits = 0;
|
||
|
|
std::uint64_t window_wait_ns = 0;
|
||
|
|
std::uint64_t resumes = 0;
|
||
|
|
std::uint64_t resent_bytes = 0;
|
||
|
|
bool framed = false;
|
||
|
|
std::size_t lanes = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief `RoundSink` whose I/O completes through an `io_context`, never spins.
|
||
|
|
class async_round_sink : public RoundSink
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
async_round_sink(async_stream_array & streams,
|
||
|
|
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
|
||
|
|
sink_options opt = {})
|
||
|
|
: core_(std::make_shared<core>(streams, streams,
|
||
|
|
std::move(slot_bytes_per_round), count, std::move(opt)))
|
||
|
|
{
|
||
|
|
core_->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Send on `out`, receive on `in` (a ring edge: to the previous
|
||
|
|
/// party, from the next). Both must run on the same `io_context`.
|
||
|
|
/// Reconnect resume needs a single link.
|
||
|
|
async_round_sink(async_stream_array & out, async_stream_array & in,
|
||
|
|
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
|
||
|
|
sink_options opt = {})
|
||
|
|
: core_(std::make_shared<core>(out, in, std::move(slot_bytes_per_round),
|
||
|
|
count, std::move(opt)))
|
||
|
|
{
|
||
|
|
core_->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
async_round_sink(const async_round_sink &) = delete;
|
||
|
|
async_round_sink & operator=(const async_round_sink &) = delete;
|
||
|
|
|
||
|
|
~async_round_sink() override
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
core_->dead = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::size_t count() const noexcept override { return core_->count; }
|
||
|
|
std::size_t rounds() const noexcept override { return core_->slots.size(); }
|
||
|
|
bool framed() const noexcept { return core_->map.framed; }
|
||
|
|
std::size_t lanes() const noexcept { return core_->map.n_lanes; }
|
||
|
|
|
||
|
|
std::size_t slot_bytes(std::uint16_t round) const override
|
||
|
|
{
|
||
|
|
if (round >= core_->slots.size())
|
||
|
|
throw std::out_of_range("async_round_sink round");
|
||
|
|
return core_->slots[round];
|
||
|
|
}
|
||
|
|
|
||
|
|
asio::io_context & context() noexcept { return *core_->io; }
|
||
|
|
async_stream_array & link() noexcept { return *core_->streams; }
|
||
|
|
async_stream_array & in_link() noexcept { return *core_->in; }
|
||
|
|
|
||
|
|
sink_stats stats() const
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
sink_stats s = core_->st;
|
||
|
|
s.framed = core_->map.framed;
|
||
|
|
s.lanes = core_->map.n_lanes;
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
void submit(std::uint16_t round, std::size_t index,
|
||
|
|
const std::uint8_t * bytes, std::size_t n) override
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
core_->window(round).submit(index, bytes, n);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||
|
|
{
|
||
|
|
{
|
||
|
|
// Bytes that already arrived stay readable after a link error.
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
if (core_->ready_locked(round, index))
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
core_->raise_if_failed();
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
return core_->ready_locked(round, index);
|
||
|
|
}
|
||
|
|
|
||
|
|
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||
|
|
std::size_t n) const override
|
||
|
|
{
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
if (round < core_->slots.size() && core_->ready_locked(round, index))
|
||
|
|
{
|
||
|
|
if (n != core_->slots[round])
|
||
|
|
throw std::invalid_argument("async_round_sink read size");
|
||
|
|
if (n != 0)
|
||
|
|
std::memcpy(out, core_->inbox[round].data() + index * n, n);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
core_->raise_if_failed();
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
if (round >= core_->slots.size())
|
||
|
|
throw std::out_of_range("async_round_sink read_peer round");
|
||
|
|
if (!core_->ready_locked(round, index))
|
||
|
|
throw std::logic_error("async_round_sink: peer not ready");
|
||
|
|
if (n != core_->slots[round])
|
||
|
|
throw std::invalid_argument("async_round_sink read size");
|
||
|
|
if (n != 0)
|
||
|
|
std::memcpy(out, core_->inbox[round].data() + index * n, n);
|
||
|
|
}
|
||
|
|
|
||
|
|
void flush() override
|
||
|
|
{
|
||
|
|
for (std::uint16_t r = 0; r < core_->slots.size(); ++r)
|
||
|
|
{
|
||
|
|
bool due = false;
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(core_->mu);
|
||
|
|
const auto & w = core_->win[r];
|
||
|
|
due = w.next_unwritten() > w.flushed()
|
||
|
|
|| (core_->slots[r] == 0 && !core_->announced[r]);
|
||
|
|
}
|
||
|
|
if (due)
|
||
|
|
flush_round(r);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void flush_round(std::uint16_t round) override
|
||
|
|
{
|
||
|
|
core_->raise_if_failed();
|
||
|
|
const std::size_t lane = core_->flush_one(round);
|
||
|
|
core_->await_window(lane);
|
||
|
|
}
|
||
|
|
|
||
|
|
void poll() override
|
||
|
|
{
|
||
|
|
core_->restart_if_stopped();
|
||
|
|
core_->io->poll();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool wait_io() override
|
||
|
|
{
|
||
|
|
core_->raise_if_failed();
|
||
|
|
core_->restart_if_stopped();
|
||
|
|
const bool ran = core_->io->run_one() > 0;
|
||
|
|
core_->raise_if_failed();
|
||
|
|
return ran;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool wait_io_for(std::chrono::milliseconds budget) override
|
||
|
|
{
|
||
|
|
core_->raise_if_failed();
|
||
|
|
core_->restart_if_stopped();
|
||
|
|
const bool ran = budget.count() <= 0 ? core_->io->poll_one() > 0
|
||
|
|
: core_->io->run_one_for(budget) > 0;
|
||
|
|
core_->raise_if_failed();
|
||
|
|
return ran;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool can_block() const noexcept override { return true; }
|
||
|
|
|
||
|
|
bool can_send_ahead() const noexcept override
|
||
|
|
{
|
||
|
|
for (std::size_t l = 0; l < core_->map.n_lanes; ++l)
|
||
|
|
{
|
||
|
|
const std::size_t w = core_->streams->lane_window_bytes(l);
|
||
|
|
if (w != 0 && core_->streams->lane_buffered_bytes(l) > w)
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::uint64_t progress() const noexcept override
|
||
|
|
{
|
||
|
|
return core_->progress.load(std::memory_order_relaxed);
|
||
|
|
}
|
||
|
|
|
||
|
|
const void * wait_domain() const noexcept override { return core_->io; }
|
||
|
|
|
||
|
|
private:
|
||
|
|
static constexpr std::uint32_t k_magic = 0x48535044u; // 'DPSH'
|
||
|
|
static constexpr std::uint16_t k_version = 1;
|
||
|
|
static constexpr std::size_t k_hello_fixed = 36;
|
||
|
|
static constexpr std::uint32_t k_zero_seen = 1;
|
||
|
|
|
||
|
|
struct core : std::enable_shared_from_this<core>
|
||
|
|
{
|
||
|
|
core(async_stream_array & s, async_stream_array & rx,
|
||
|
|
std::vector<std::size_t> slots_, std::size_t count_, sink_options opt_)
|
||
|
|
: streams(&s),
|
||
|
|
in(&rx),
|
||
|
|
io(&s.context()),
|
||
|
|
count(count_),
|
||
|
|
slots(std::move(slots_)),
|
||
|
|
map(std::min(s.size(), rx.size()), slots.size(), opt_.framing),
|
||
|
|
opt(std::move(opt_))
|
||
|
|
{
|
||
|
|
if (count == 0)
|
||
|
|
throw std::invalid_argument("async_round_sink: count 0");
|
||
|
|
if (s.size() == 0 || rx.size() == 0)
|
||
|
|
throw std::invalid_argument("async_round_sink: empty streams");
|
||
|
|
if (&s.context() != &rx.context())
|
||
|
|
throw std::invalid_argument(
|
||
|
|
"async_round_sink: out and in links need one io_context");
|
||
|
|
if (&s != &rx && opt.reconnect)
|
||
|
|
throw std::invalid_argument(
|
||
|
|
"async_round_sink: reconnect needs a single link");
|
||
|
|
const std::size_t nr = slots.size();
|
||
|
|
win.reserve(nr);
|
||
|
|
inbox.resize(nr);
|
||
|
|
in_filled.assign(nr, 0);
|
||
|
|
zero_seen.assign(nr, false);
|
||
|
|
announced.assign(nr, false);
|
||
|
|
legacy_started.assign(nr, false);
|
||
|
|
legacy_wanted.assign(nr, false);
|
||
|
|
for (std::size_t k = 0; k < nr; ++k)
|
||
|
|
{
|
||
|
|
win.emplace_back(count, slots[k]);
|
||
|
|
inbox[k].assign(count * slots[k], 0);
|
||
|
|
}
|
||
|
|
fingerprint = slots_fingerprint(slots);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- lifecycle -------------------------------------------------
|
||
|
|
void start()
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (opt.hello)
|
||
|
|
begin_hello_locked();
|
||
|
|
else
|
||
|
|
hello_done_locked();
|
||
|
|
}
|
||
|
|
|
||
|
|
void restart_if_stopped()
|
||
|
|
{
|
||
|
|
if (io->stopped())
|
||
|
|
io->restart();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- hello ------------------------------------------------------
|
||
|
|
std::shared_ptr<std::vector<std::uint8_t>> build_hello_locked() const
|
||
|
|
{
|
||
|
|
const std::size_t r = slots.size();
|
||
|
|
auto buf = acquire_buffer(k_hello_fixed + 4 * r);
|
||
|
|
auto * p = buf->data();
|
||
|
|
std::memset(p, 0, buf->size());
|
||
|
|
detail::put_u32(p + 0, k_magic);
|
||
|
|
p[4] = static_cast<std::uint8_t>(k_version & 0xffu);
|
||
|
|
p[5] = static_cast<std::uint8_t>(k_version >> 8);
|
||
|
|
p[6] = map.framed ? 1 : 0;
|
||
|
|
detail::put_u32(p + 8, static_cast<std::uint32_t>(r));
|
||
|
|
detail::put_u32(p + 12, static_cast<std::uint32_t>(map.n_lanes));
|
||
|
|
detail::put_u32(p + 16, static_cast<std::uint32_t>(count));
|
||
|
|
detail::put_u32(p + 20, gen);
|
||
|
|
for (int i = 0; i < 8; ++i)
|
||
|
|
p[24 + i] = static_cast<std::uint8_t>((fingerprint >> (8 * i)) & 0xffu);
|
||
|
|
for (std::size_t k = 0; k < r; ++k)
|
||
|
|
{
|
||
|
|
const std::uint32_t got = slots[k] == 0
|
||
|
|
? (zero_seen[k] ? k_zero_seen : 0)
|
||
|
|
: static_cast<std::uint32_t>(in_filled[k]);
|
||
|
|
detail::put_u32(p + k_hello_fixed + 4 * k, got);
|
||
|
|
}
|
||
|
|
return buf;
|
||
|
|
}
|
||
|
|
|
||
|
|
void begin_hello_locked()
|
||
|
|
{
|
||
|
|
hello_ok = false;
|
||
|
|
auto self = shared_from_this();
|
||
|
|
const std::uint32_t g = gen;
|
||
|
|
streams->async_write_owned(0, build_hello_locked(),
|
||
|
|
[self, g](const std::error_code & ec) { self->on_write(g, ec); });
|
||
|
|
hello_in = std::make_shared<std::vector<std::uint8_t>>(
|
||
|
|
k_hello_fixed + 4 * slots.size());
|
||
|
|
auto buf = hello_in;
|
||
|
|
in->async_read(0, buf->data(), buf->size(),
|
||
|
|
[self, g, buf](const std::error_code & ec) {
|
||
|
|
self->on_hello(g, buf, ec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void on_hello(std::uint32_t g,
|
||
|
|
const std::shared_ptr<std::vector<std::uint8_t>> & buf,
|
||
|
|
const std::error_code & ec)
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (dead || g != gen)
|
||
|
|
return;
|
||
|
|
if (ec)
|
||
|
|
{
|
||
|
|
fail_locked(ec, "hello read", false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const auto * p = buf->data();
|
||
|
|
if (detail::get_u32(p) != k_magic)
|
||
|
|
{
|
||
|
|
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||
|
|
"peer did not send a sink hello (is it an async_round_sink "
|
||
|
|
"or stream_array_sink with hello enabled?)", true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const std::uint16_t ver = static_cast<std::uint16_t>(p[4] | (p[5] << 8));
|
||
|
|
const bool peer_framed = (p[6] & 1) != 0;
|
||
|
|
const std::uint32_t pr = detail::get_u32(p + 8);
|
||
|
|
const std::uint32_t pl = detail::get_u32(p + 12);
|
||
|
|
const std::uint32_t pc = detail::get_u32(p + 16);
|
||
|
|
const std::uint32_t pg = detail::get_u32(p + 20);
|
||
|
|
std::uint64_t pf = 0;
|
||
|
|
for (int i = 0; i < 8; ++i)
|
||
|
|
pf |= static_cast<std::uint64_t>(p[24 + i]) << (8 * i);
|
||
|
|
std::string why;
|
||
|
|
if (ver != k_version)
|
||
|
|
why += " version " + std::to_string(ver) + " vs "
|
||
|
|
+ std::to_string(k_version) + ";";
|
||
|
|
if (pr != slots.size())
|
||
|
|
why += " rounds " + std::to_string(pr) + " vs "
|
||
|
|
+ std::to_string(slots.size()) + ";";
|
||
|
|
else if (pf != fingerprint)
|
||
|
|
why += " slot widths differ;";
|
||
|
|
if (pl != map.n_lanes)
|
||
|
|
why += " lanes " + std::to_string(pl) + " vs "
|
||
|
|
+ std::to_string(map.n_lanes) + ";";
|
||
|
|
if (pc != count)
|
||
|
|
why += " instances " + std::to_string(pc) + " vs "
|
||
|
|
+ std::to_string(count) + ";";
|
||
|
|
if (peer_framed != map.framed)
|
||
|
|
why += std::string(" framing ") + (peer_framed ? "on" : "off")
|
||
|
|
+ " vs " + (map.framed ? "on" : "off") + ";";
|
||
|
|
if (pg != gen)
|
||
|
|
why += " epoch " + std::to_string(pg) + " vs "
|
||
|
|
+ std::to_string(gen) + ";";
|
||
|
|
if (!why.empty())
|
||
|
|
{
|
||
|
|
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||
|
|
"peer sink disagrees (peer vs this side):" + why, true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
std::vector<std::uint32_t> peer_got(slots.size());
|
||
|
|
for (std::size_t k = 0; k < slots.size(); ++k)
|
||
|
|
peer_got[k] = detail::get_u32(p + k_hello_fixed + 4 * k);
|
||
|
|
if (gen != 0 && !resend_locked(peer_got))
|
||
|
|
return;
|
||
|
|
hello_done_locked();
|
||
|
|
}
|
||
|
|
|
||
|
|
void hello_done_locked()
|
||
|
|
{
|
||
|
|
hello_ok = true;
|
||
|
|
if (map.framed)
|
||
|
|
{
|
||
|
|
hdr_bufs.clear();
|
||
|
|
for (std::size_t lane = 0; lane < map.n_lanes; ++lane)
|
||
|
|
{
|
||
|
|
hdr_bufs.push_back(
|
||
|
|
std::make_shared<std::array<std::uint8_t, round_lane_hdr::size>>());
|
||
|
|
read_lane_hdr_locked(lane);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
for (std::size_t r = 0; r < slots.size(); ++r)
|
||
|
|
if (legacy_wanted[r])
|
||
|
|
issue_legacy_locked(static_cast<std::uint16_t>(r));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief After a reconnect: send what the peer reported missing.
|
||
|
|
bool resend_locked(const std::vector<std::uint32_t> & peer_got)
|
||
|
|
{
|
||
|
|
auto self = shared_from_this();
|
||
|
|
const std::uint32_t g = gen;
|
||
|
|
for (std::size_t k = 0; k < slots.size(); ++k)
|
||
|
|
{
|
||
|
|
const std::size_t sb = slots[k];
|
||
|
|
const auto round = static_cast<std::uint16_t>(k);
|
||
|
|
const std::size_t lane = map.lane(round);
|
||
|
|
if (sb == 0)
|
||
|
|
{
|
||
|
|
if (map.framed && announced[k] && peer_got[k] != k_zero_seen)
|
||
|
|
{
|
||
|
|
auto frame = acquire_buffer(round_lane_hdr::size);
|
||
|
|
round_lane_hdr{round, 0}.pack(frame->data());
|
||
|
|
streams->async_write_owned(lane, std::move(frame),
|
||
|
|
[self, g](const std::error_code & ec) {
|
||
|
|
self->on_write(g, ec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const std::size_t sent = win[k].flushed() * sb;
|
||
|
|
const std::size_t got = peer_got[k];
|
||
|
|
if (got > sent || got % sb != 0)
|
||
|
|
{
|
||
|
|
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||
|
|
"resume: peer reports " + std::to_string(got)
|
||
|
|
+ " bytes of round " + std::to_string(k)
|
||
|
|
+ ", this side sent " + std::to_string(sent),
|
||
|
|
true);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (got == sent)
|
||
|
|
continue;
|
||
|
|
const std::size_t nbytes = sent - got;
|
||
|
|
const std::uint8_t * src = win[k].out_at(got / sb);
|
||
|
|
st.resent_bytes += nbytes;
|
||
|
|
if (map.framed)
|
||
|
|
{
|
||
|
|
auto frame = acquire_buffer(round_lane_hdr::size + nbytes);
|
||
|
|
round_lane_hdr{round, static_cast<std::uint32_t>(nbytes)}.pack(
|
||
|
|
frame->data());
|
||
|
|
std::memcpy(frame->data() + round_lane_hdr::size, src, nbytes);
|
||
|
|
streams->async_write_owned(lane, std::move(frame),
|
||
|
|
[self, g](const std::error_code & ec) {
|
||
|
|
self->on_write(g, ec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
streams->async_write(lane, src, nbytes,
|
||
|
|
[self, g](const std::error_code & ec) {
|
||
|
|
self->on_write(g, ec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- errors and resume ------------------------------------------
|
||
|
|
void fail_locked(const std::error_code & ec, const std::string & what,
|
||
|
|
bool is_fatal)
|
||
|
|
{
|
||
|
|
if (fail)
|
||
|
|
return;
|
||
|
|
fail = ec;
|
||
|
|
fail_what = "async_round_sink: " + what;
|
||
|
|
fatal = is_fatal;
|
||
|
|
}
|
||
|
|
|
||
|
|
void on_write(std::uint32_t g, const std::error_code & ec)
|
||
|
|
{
|
||
|
|
if (!ec)
|
||
|
|
return;
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (dead || g != gen)
|
||
|
|
return;
|
||
|
|
fail_locked(ec, "write", false);
|
||
|
|
}
|
||
|
|
|
||
|
|
void raise_if_failed()
|
||
|
|
{
|
||
|
|
std::error_code ec;
|
||
|
|
std::string what;
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (!fail)
|
||
|
|
return;
|
||
|
|
if (fatal || !opt.reconnect || reconnects >= opt.max_reconnects)
|
||
|
|
throw std::system_error(fail, fail_what);
|
||
|
|
ec = fail;
|
||
|
|
what = fail_what;
|
||
|
|
}
|
||
|
|
async_stream_array * next = nullptr;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
next = opt.reconnect(ec);
|
||
|
|
}
|
||
|
|
catch (const std::exception & e)
|
||
|
|
{
|
||
|
|
throw std::system_error(ec, what + "; reconnect failed: " + e.what());
|
||
|
|
}
|
||
|
|
if (next == nullptr)
|
||
|
|
throw std::system_error(ec, what + "; no replacement link");
|
||
|
|
resume(*next);
|
||
|
|
}
|
||
|
|
|
||
|
|
void resume(async_stream_array & next)
|
||
|
|
{
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (next.size() < map.n_lanes)
|
||
|
|
throw std::invalid_argument(
|
||
|
|
"async_round_sink resume: replacement link has fewer lanes");
|
||
|
|
++gen;
|
||
|
|
++reconnects;
|
||
|
|
++st.resumes;
|
||
|
|
fail.clear();
|
||
|
|
fail_what.clear();
|
||
|
|
fatal = false;
|
||
|
|
streams = &next;
|
||
|
|
in = &next;
|
||
|
|
io = &next.context();
|
||
|
|
for (std::size_t r = 0; r < slots.size(); ++r)
|
||
|
|
{
|
||
|
|
if (legacy_started[r] && in_filled[r] < count * slots[r])
|
||
|
|
{
|
||
|
|
legacy_started[r] = false;
|
||
|
|
legacy_wanted[r] = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
begin_hello_locked();
|
||
|
|
}
|
||
|
|
const auto deadline = std::chrono::steady_clock::now()
|
||
|
|
+ opt.handshake_timeout;
|
||
|
|
for (;;)
|
||
|
|
{
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (hello_ok)
|
||
|
|
return;
|
||
|
|
if (fail)
|
||
|
|
throw std::system_error(fail, fail_what + " (during resume)");
|
||
|
|
}
|
||
|
|
const auto now = std::chrono::steady_clock::now();
|
||
|
|
if (now >= deadline)
|
||
|
|
throw std::system_error(std::make_error_code(std::errc::timed_out),
|
||
|
|
"async_round_sink: resume hello timed out");
|
||
|
|
restart_if_stopped();
|
||
|
|
io->run_one_for(std::min<std::chrono::milliseconds>(
|
||
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||
|
|
deadline - now),
|
||
|
|
std::chrono::milliseconds(10)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- reads ------------------------------------------------------
|
||
|
|
/// @brief Every round's peer bytes (and zero-width announcements) are in.
|
||
|
|
bool complete_locked() const
|
||
|
|
{
|
||
|
|
for (std::size_t r = 0; r < slots.size(); ++r)
|
||
|
|
{
|
||
|
|
const std::size_t sb = slots[r];
|
||
|
|
if (sb == 0 ? (map.framed && !zero_seen[r])
|
||
|
|
: in_filled[r] < count * sb)
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ready_locked(std::uint16_t round, std::size_t index)
|
||
|
|
{
|
||
|
|
if (round >= slots.size())
|
||
|
|
return false;
|
||
|
|
const std::size_t sb = slots[round];
|
||
|
|
if (map.framed)
|
||
|
|
{
|
||
|
|
if (sb == 0)
|
||
|
|
return zero_seen[round];
|
||
|
|
return in_filled[round] >= (index + 1) * sb;
|
||
|
|
}
|
||
|
|
if (sb == 0)
|
||
|
|
return true;
|
||
|
|
if (!legacy_started[round])
|
||
|
|
{
|
||
|
|
if (hello_ok)
|
||
|
|
issue_legacy_locked(round);
|
||
|
|
else
|
||
|
|
legacy_wanted[round] = true;
|
||
|
|
}
|
||
|
|
return in_filled[round] == count * sb;
|
||
|
|
}
|
||
|
|
|
||
|
|
void issue_legacy_locked(std::uint16_t round)
|
||
|
|
{
|
||
|
|
if (legacy_started[round])
|
||
|
|
return;
|
||
|
|
legacy_started[round] = true;
|
||
|
|
legacy_wanted[round] = false;
|
||
|
|
const std::size_t bytes = count * slots[round];
|
||
|
|
if (bytes == 0)
|
||
|
|
{
|
||
|
|
in_filled[round] = 0;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
auto self = shared_from_this();
|
||
|
|
const std::uint32_t g = gen;
|
||
|
|
this->in->async_read(round, inbox[round].data(), bytes,
|
||
|
|
[self, g, round, bytes](const std::error_code & ec) {
|
||
|
|
std::lock_guard<std::mutex> lock(self->mu);
|
||
|
|
if (self->dead || g != self->gen)
|
||
|
|
return;
|
||
|
|
if (ec)
|
||
|
|
{
|
||
|
|
self->fail_locked(ec, "read round "
|
||
|
|
+ std::to_string(round), false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
self->in_filled[round] = bytes;
|
||
|
|
self->st.bytes_received += bytes;
|
||
|
|
self->progress.fetch_add(bytes, std::memory_order_relaxed);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void read_lane_hdr_locked(std::size_t lane)
|
||
|
|
{
|
||
|
|
auto self = shared_from_this();
|
||
|
|
const std::uint32_t g = gen;
|
||
|
|
auto buf = hdr_bufs[lane];
|
||
|
|
in->async_read(lane, buf->data(), buf->size(),
|
||
|
|
[self, g, lane, buf](const std::error_code & ec) {
|
||
|
|
self->on_lane_hdr(g, lane, buf, ec);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void on_lane_hdr(std::uint32_t g, std::size_t lane,
|
||
|
|
const std::shared_ptr<std::array<std::uint8_t, round_lane_hdr::size>> & buf,
|
||
|
|
const std::error_code & ec)
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
if (dead || g != gen)
|
||
|
|
return;
|
||
|
|
if (ec)
|
||
|
|
{
|
||
|
|
// A peer that closes after its last frame finished cleanly.
|
||
|
|
if (!complete_locked())
|
||
|
|
fail_locked(ec, "read lane " + std::to_string(lane), false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const auto hdr = round_lane_hdr::unpack(buf->data());
|
||
|
|
if (hdr.round >= slots.size() || map.lane(hdr.round) != lane)
|
||
|
|
{
|
||
|
|
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||
|
|
"frame for round " + std::to_string(hdr.round) + " on lane "
|
||
|
|
+ std::to_string(lane),
|
||
|
|
true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const std::size_t sb = slots[hdr.round];
|
||
|
|
const std::size_t cap = count * sb;
|
||
|
|
if (hdr.nbytes == 0)
|
||
|
|
{
|
||
|
|
if (sb == 0 && !zero_seen[hdr.round])
|
||
|
|
{
|
||
|
|
zero_seen[hdr.round] = true;
|
||
|
|
progress.fetch_add(1, std::memory_order_relaxed);
|
||
|
|
}
|
||
|
|
read_lane_hdr_locked(lane);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (sb == 0 || hdr.nbytes % sb != 0
|
||
|
|
|| in_filled[hdr.round] + hdr.nbytes > cap)
|
||
|
|
{
|
||
|
|
fail_locked(std::make_error_code(std::errc::message_size),
|
||
|
|
"round " + std::to_string(hdr.round) + " frame of "
|
||
|
|
+ std::to_string(hdr.nbytes) + " bytes does not fit",
|
||
|
|
true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const std::uint16_t round = hdr.round;
|
||
|
|
const std::size_t nbytes = hdr.nbytes;
|
||
|
|
auto self = shared_from_this();
|
||
|
|
in->async_read(lane, inbox[round].data() + in_filled[round], nbytes,
|
||
|
|
[self, g, lane, round, nbytes](const std::error_code & xec) {
|
||
|
|
std::lock_guard<std::mutex> lk(self->mu);
|
||
|
|
if (self->dead || g != self->gen)
|
||
|
|
return;
|
||
|
|
if (xec)
|
||
|
|
{
|
||
|
|
self->fail_locked(xec, "read round "
|
||
|
|
+ std::to_string(round), false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
self->in_filled[round] += nbytes;
|
||
|
|
self->st.bytes_received += nbytes;
|
||
|
|
self->progress.fetch_add(nbytes, std::memory_order_relaxed);
|
||
|
|
self->read_lane_hdr_locked(lane);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- writes -----------------------------------------------------
|
||
|
|
round_window & window(std::uint16_t round)
|
||
|
|
{
|
||
|
|
if (round >= win.size())
|
||
|
|
throw std::out_of_range("async_round_sink round");
|
||
|
|
return win[round];
|
||
|
|
}
|
||
|
|
|
||
|
|
std::size_t flush_one(std::uint16_t round)
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
auto & w = window(round);
|
||
|
|
std::size_t begin = 0;
|
||
|
|
std::size_t nslots = 0;
|
||
|
|
const std::uint8_t * pend = w.pending_out(begin, nslots);
|
||
|
|
const std::size_t sb = slots[round];
|
||
|
|
const std::size_t nbytes = nslots * sb;
|
||
|
|
const std::size_t lane = map.lane(round);
|
||
|
|
auto self = shared_from_this();
|
||
|
|
const std::uint32_t g = gen;
|
||
|
|
auto done = [self, g](const std::error_code & ec) {
|
||
|
|
self->on_write(g, ec);
|
||
|
|
};
|
||
|
|
if (map.framed)
|
||
|
|
{
|
||
|
|
if (nslots != 0 || (sb == 0 && !announced[round]))
|
||
|
|
{
|
||
|
|
auto frame = acquire_buffer(round_lane_hdr::size + nbytes);
|
||
|
|
round_lane_hdr{round, static_cast<std::uint32_t>(nbytes)}.pack(
|
||
|
|
frame->data());
|
||
|
|
if (nbytes != 0)
|
||
|
|
std::memcpy(frame->data() + round_lane_hdr::size, pend,
|
||
|
|
nbytes);
|
||
|
|
streams->async_write_owned(lane, std::move(frame), done);
|
||
|
|
if (sb == 0)
|
||
|
|
announced[round] = true;
|
||
|
|
++st.flushes;
|
||
|
|
st.bytes_sent += nbytes;
|
||
|
|
}
|
||
|
|
if (nslots != 0)
|
||
|
|
w.mark_flushed(nslots);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (nslots != 0 && sb != 0)
|
||
|
|
{
|
||
|
|
streams->async_write(lane, pend, nbytes, done);
|
||
|
|
++st.flushes;
|
||
|
|
st.bytes_sent += nbytes;
|
||
|
|
}
|
||
|
|
if (nslots != 0)
|
||
|
|
w.mark_flushed(nslots);
|
||
|
|
if (!legacy_started[round])
|
||
|
|
{
|
||
|
|
if (hello_ok)
|
||
|
|
issue_legacy_locked(round);
|
||
|
|
else
|
||
|
|
legacy_wanted[round] = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return lane;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Stall the producer while lane `lane`'s window is full.
|
||
|
|
void await_window(std::size_t lane)
|
||
|
|
{
|
||
|
|
const std::size_t w = streams->lane_window_bytes(lane);
|
||
|
|
if (w == 0 || streams->lane_buffered_bytes(lane) <= w)
|
||
|
|
return;
|
||
|
|
const auto t0 = std::chrono::steady_clock::now();
|
||
|
|
auto since = t0;
|
||
|
|
std::size_t last = streams->lane_buffered_bytes(lane);
|
||
|
|
for (;;)
|
||
|
|
{
|
||
|
|
raise_if_failed();
|
||
|
|
const std::size_t now_buf = streams->lane_buffered_bytes(lane);
|
||
|
|
if (now_buf <= streams->lane_window_bytes(lane))
|
||
|
|
break;
|
||
|
|
const auto now = std::chrono::steady_clock::now();
|
||
|
|
if (now_buf < last)
|
||
|
|
{
|
||
|
|
last = now_buf;
|
||
|
|
since = now;
|
||
|
|
}
|
||
|
|
else if (now - since > opt.drain_timeout)
|
||
|
|
{
|
||
|
|
throw std::runtime_error("async_round_sink: lane "
|
||
|
|
+ std::to_string(lane) + " did not drain for "
|
||
|
|
+ std::to_string(opt.drain_timeout.count()) + " ms ("
|
||
|
|
+ std::to_string(now_buf) + " bytes buffered, window "
|
||
|
|
+ std::to_string(w) + ")");
|
||
|
|
}
|
||
|
|
restart_if_stopped();
|
||
|
|
io->run_one_for(std::chrono::milliseconds(10));
|
||
|
|
}
|
||
|
|
const auto dt = std::chrono::steady_clock::now() - t0;
|
||
|
|
std::lock_guard<std::mutex> lock(mu);
|
||
|
|
++st.window_waits;
|
||
|
|
st.window_wait_ns += static_cast<std::uint64_t>(
|
||
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(dt).count());
|
||
|
|
}
|
||
|
|
|
||
|
|
async_stream_array * streams = nullptr;
|
||
|
|
async_stream_array * in = nullptr;
|
||
|
|
asio::io_context * io = nullptr;
|
||
|
|
std::size_t count = 1;
|
||
|
|
std::vector<std::size_t> slots;
|
||
|
|
round_lane_map map;
|
||
|
|
sink_options opt;
|
||
|
|
std::uint64_t fingerprint = 0;
|
||
|
|
|
||
|
|
std::mutex mu;
|
||
|
|
std::vector<round_window> win;
|
||
|
|
std::vector<std::vector<std::uint8_t>> inbox;
|
||
|
|
std::vector<std::size_t> in_filled;
|
||
|
|
std::vector<bool> zero_seen;
|
||
|
|
std::vector<bool> announced;
|
||
|
|
std::vector<bool> legacy_started;
|
||
|
|
std::vector<bool> legacy_wanted;
|
||
|
|
std::vector<std::shared_ptr<std::array<std::uint8_t, round_lane_hdr::size>>>
|
||
|
|
hdr_bufs;
|
||
|
|
std::shared_ptr<std::vector<std::uint8_t>> hello_in;
|
||
|
|
std::uint32_t gen = 0;
|
||
|
|
bool hello_ok = false;
|
||
|
|
std::error_code fail;
|
||
|
|
std::string fail_what;
|
||
|
|
bool fatal = false;
|
||
|
|
unsigned reconnects = 0;
|
||
|
|
bool dead = false;
|
||
|
|
std::atomic<std::uint64_t> progress{0};
|
||
|
|
sink_stats st;
|
||
|
|
};
|
||
|
|
|
||
|
|
std::shared_ptr<core> core_;
|
||
|
|
};
|
||
|
|
|
||
|
|
} // namespace net
|
||
|
|
} // namespace dpf
|
||
|
|
|
||
|
|
#endif // LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|