libdpf/include/dpf/net/async_stream_array.hpp

2070 lines
68 KiB
C++
Raw Permalink Normal View History

/// @file dpf/net/async_stream_array.hpp
/// @brief Truly asynchronous, overlapped indexed byte streams.
/// @details Every backend completes through an `asio::io_context` and never
/// spins on a `peer_ready` flag or blocks the calling thread. The
/// calling thread of `async_read` only registers a waiter and returns;
/// the completion handler fires later on an `io_context` thread.
/// * `async_dual_memory_stream_array` — paired in-process ends.
/// * `async_mux_stream_array` — N lanes on ONE TCP socket.
/// * `async_parallel_stream_array` — one socket PER lane.
/// Writes are split into `wire_policy::chunk_bytes` frames. The mux
/// sends chunks round-robin across lanes, so a large write on one lane
/// does not queue small writes on another lane behind it. When a reader
/// is already waiting, socket payloads land directly in its buffer.
#ifndef LIBDPF_INCLUDE_DPF_NET_ASYNC_STREAM_ARRAY_HPP__
#define LIBDPF_INCLUDE_DPF_NET_ASYNC_STREAM_ARRAY_HPP__
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
#include <vector>
#include "dpf/net/asio_ns.hpp"
#include "hedley/hedley.h"
#include "dpf/net/buffer_pool.hpp"
#include "dpf/net/connect.hpp"
#include "dpf/net/policy.hpp"
#include "dpf/net/socket_tune.hpp"
#include "dpf/net/stream_array.hpp" // stream_frame_hdr
namespace dpf
{
namespace net
{
/// @brief Completion handler for a single async I/O operation.
using async_handler = std::function<void(const std::error_code &)>;
/// @brief Counters for one stream array end.
/// @details `bytes_*` include framing; `payload_*` are caller bytes. Times are
/// `steady_clock` nanoseconds since epoch (0 = never).
struct stream_stats
{
std::uint64_t bytes_out = 0;
std::uint64_t bytes_in = 0;
std::uint64_t payload_out = 0;
std::uint64_t payload_in = 0;
std::uint64_t frames_out = 0;
std::uint64_t frames_in = 0;
std::uint64_t write_calls = 0;
std::size_t buffered = 0;
std::size_t unread = 0;
std::int64_t last_write_ns = 0;
std::int64_t last_read_ns = 0;
/// TCP payload bytes the kernel reports sent (acknowledged) and received
/// on this link's sockets, so TLS records and handshakes included; 0 for
/// in-process, unix, and SCTP links, and once the link is closed.
std::uint64_t socket_bytes_out = 0;
std::uint64_t socket_bytes_in = 0;
std::error_code error;
bool closed = false;
};
namespace detail
{
inline std::int64_t steady_ns() noexcept
{
return static_cast<std::int64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
}
/// @brief Completion shared by the chunks of one write.
struct write_op
{
async_handler h;
std::size_t left = 0;
std::error_code ec;
};
struct io_counters
{
std::atomic<std::uint64_t> bytes_out{0};
std::atomic<std::uint64_t> bytes_in{0};
std::atomic<std::uint64_t> payload_out{0};
std::atomic<std::uint64_t> payload_in{0};
std::atomic<std::uint64_t> frames_out{0};
std::atomic<std::uint64_t> frames_in{0};
std::atomic<std::uint64_t> write_calls{0};
std::atomic<std::int64_t> last_write_ns{0};
std::atomic<std::int64_t> last_read_ns{0};
void wrote(std::uint64_t wire, std::uint64_t payload, std::uint64_t frames)
{
bytes_out.fetch_add(wire, std::memory_order_relaxed);
payload_out.fetch_add(payload, std::memory_order_relaxed);
frames_out.fetch_add(frames, std::memory_order_relaxed);
write_calls.fetch_add(1, std::memory_order_relaxed);
last_write_ns.store(steady_ns(), std::memory_order_relaxed);
}
void read(std::uint64_t wire, std::uint64_t payload, std::uint64_t frames)
{
bytes_in.fetch_add(wire, std::memory_order_relaxed);
payload_in.fetch_add(payload, std::memory_order_relaxed);
frames_in.fetch_add(frames, std::memory_order_relaxed);
last_read_ns.store(steady_ns(), std::memory_order_relaxed);
}
void fill(stream_stats & s) const
{
s.bytes_out = bytes_out.load(std::memory_order_relaxed);
s.bytes_in = bytes_in.load(std::memory_order_relaxed);
s.payload_out = payload_out.load(std::memory_order_relaxed);
s.payload_in = payload_in.load(std::memory_order_relaxed);
s.frames_out = frames_out.load(std::memory_order_relaxed);
s.frames_in = frames_in.load(std::memory_order_relaxed);
s.write_calls = write_calls.load(std::memory_order_relaxed);
s.last_write_ns = last_write_ns.load(std::memory_order_relaxed);
s.last_read_ns = last_read_ns.load(std::memory_order_relaxed);
}
};
/// @brief `(offset, length)` pieces of a `len`-byte write.
inline std::vector<std::pair<std::size_t, std::size_t>> split_chunks(
std::size_t len, std::size_t chunk)
{
std::vector<std::pair<std::size_t, std::size_t>> out;
if (len == 0)
return out;
const std::size_t step = chunk == 0 ? len : chunk;
for (std::size_t off = 0; off < len; off += step)
out.emplace_back(off, std::min(step, len - off));
return out;
}
/// @brief One reader waiting for `n` bytes on a stream.
struct stream_waiter
{
bool active = false;
bool reading = false; ///< a socket read is filling `dst` directly
void * dst = nullptr;
std::size_t n = 0;
std::size_t filled = 0;
async_handler h;
std::error_code abort;
};
/// @brief Byte inbox with a consumed-prefix cursor.
struct stream_inbox
{
std::vector<std::uint8_t> bytes;
std::size_t pos = 0;
std::size_t avail() const noexcept { return bytes.size() - pos; }
std::size_t take(void * dst, std::size_t n, std::size_t compact_at)
{
const std::size_t k = std::min(n, avail());
if (k != 0)
std::memcpy(dst, bytes.data() + pos, k);
pos += k;
if (pos == bytes.size())
{
bytes.clear();
pos = 0;
}
else if (compact_at != 0 && pos > compact_at)
{
bytes.erase(bytes.begin(),
bytes.begin() + static_cast<std::ptrdiff_t>(pos));
pos = 0;
}
return k;
}
};
inline void post_handler(asio::io_context & io, async_handler h,
std::error_code ec)
{
if (h)
asio::post(io, [h = std::move(h), ec]() { h(ec); });
}
/// @brief One contiguous copy of a gather list of `bytes` total.
inline std::shared_ptr<std::vector<std::uint8_t>> flatten(
const std::vector<asio::const_buffer> & bufs, std::size_t bytes)
{
auto flat = acquire_buffer(bytes);
std::size_t at = 0;
for (const auto & b : bufs)
{
if (b.size() != 0)
std::memcpy(flat->data() + at, b.data(), b.size());
at += b.size();
}
return flat;
}
/// @brief Add the kernel's TCP byte counters for `fd` (no-op off Linux or
/// for non-TCP sockets).
inline void add_socket_bytes(int fd, stream_stats & s)
{
#if defined(__linux__) && defined(TCP_INFO)
tcp_info ti{};
socklen_t len = sizeof(ti);
if (fd >= 0 && ::getsockopt(fd, IPPROTO_TCP, TCP_INFO, &ti, &len) == 0
&& len >= offsetof(tcp_info, tcpi_bytes_received) + sizeof(ti.tcpi_bytes_received))
{
s.socket_bytes_out += ti.tcpi_bytes_acked;
s.socket_bytes_in += ti.tcpi_bytes_received;
}
#else
(void)fd;
(void)s;
#endif
}
/// @brief A TLS peer that closes without `close_notify` reads as a truncated
/// stream. Frames already say what is owed, so that is an ordinary end
/// of stream.
inline std::error_code end_of_stream(const std::error_code & ec)
{
if (ec.value() == 1 && std::strcmp(ec.category().name(), "asio.ssl.stream") == 0)
return asio::error::eof;
return ec;
}
} // namespace detail
/// @brief Virtual base for N duplex streams with event-driven completion.
/// @details `async_write` / `async_read` are non-blocking initiators. The
/// handler is invoked exactly once, on a thread running `context()`
/// (never inline on the caller's thread). A short read or write is
/// reported through the handler's `error_code`. After `close()`, no
/// backend copies into a caller buffer from its inbox again; pending
/// reads complete with `operation_aborted`.
class async_stream_array
{
public:
virtual ~async_stream_array() = default;
virtual std::size_t size() const noexcept = 0;
virtual asio::io_context & context() noexcept = 0;
/// @brief Write `n` bytes on stream `i`. Backends copy `src` before return.
virtual void async_write(std::size_t i, const void * src, std::size_t n,
async_handler h) = 0;
/// @brief Read exactly `n` bytes on stream `i` into `dst`. `dst` must stay
/// valid until `h` fires. One outstanding read per stream.
virtual void async_read(std::size_t i, void * dst, std::size_t n,
async_handler h) = 0;
/// @brief Write an owned buffer without copying the payload again.
virtual void async_write_owned(std::size_t i,
std::shared_ptr<std::vector<std::uint8_t>> buf, async_handler h)
{
const std::size_t n = buf ? buf->size() : 0;
const void * p = (buf && n) ? buf->data() : nullptr;
async_write(i, p, n, [buf = std::move(buf), h = std::move(h)](
const std::error_code & ec) {
if (h)
h(ec);
});
}
/// @brief Bytes accepted by `async_write*` and not yet on the wire (or not
/// yet read, for in-process backends).
virtual std::size_t buffered_bytes() const noexcept { return 0; }
/// @brief High-water mark for `buffered_bytes` (0 = unlimited).
virtual std::size_t window_bytes() const noexcept { return 0; }
virtual void set_window_bytes(std::size_t) {}
/// @brief Backpressure that applies to lane `i` (whole connection on mux
/// and SCTP, that lane's socket on parallel).
virtual std::size_t lane_buffered_bytes(std::size_t) const noexcept
{
return buffered_bytes();
}
virtual std::size_t lane_window_bytes(std::size_t) const noexcept
{
return window_bytes();
}
virtual stream_stats stats() const { return {}; }
/// @brief Abort all I/O: pending reads and queued writes fail with
/// `operation_aborted`; the peer sees EOF. Destroying a backend is
/// graceful instead: accepted writes still reach the peer.
virtual void close() noexcept {}
};
// ---------------------------------------------------------------------------
// Lane-range view
// ---------------------------------------------------------------------------
/// @brief Lanes `[first, first + count)` of another array, re-indexed from 0.
/// @details Lets one connection carry several logical edges (peer, ring,
/// dealer) on disjoint lanes. Does not own or close the base.
class async_stream_view final : public async_stream_array
{
public:
async_stream_view(async_stream_array & base, std::size_t first,
std::size_t count)
: base_(&base), first_(first), count_(count)
{
if (count == 0 || first + count > base.size())
throw std::invalid_argument("async_stream_view: lane range");
}
std::size_t size() const noexcept override { return count_; }
asio::io_context & context() noexcept override { return base_->context(); }
void async_write(std::size_t i, const void * src, std::size_t n,
async_handler h) override
{
base_->async_write(map(i), src, n, std::move(h));
}
void async_read(std::size_t i, void * dst, std::size_t n,
async_handler h) override
{
base_->async_read(map(i), dst, n, std::move(h));
}
void async_write_owned(std::size_t i,
std::shared_ptr<std::vector<std::uint8_t>> buf, async_handler h) override
{
base_->async_write_owned(map(i), std::move(buf), std::move(h));
}
std::size_t buffered_bytes() const noexcept override
{
return base_->buffered_bytes();
}
std::size_t window_bytes() const noexcept override
{
return base_->window_bytes();
}
void set_window_bytes(std::size_t b) override { base_->set_window_bytes(b); }
std::size_t lane_buffered_bytes(std::size_t i) const noexcept override
{
return base_->lane_buffered_bytes(first_ + i);
}
std::size_t lane_window_bytes(std::size_t i) const noexcept override
{
return base_->lane_window_bytes(first_ + i);
}
stream_stats stats() const override { return base_->stats(); }
private:
std::size_t map(std::size_t i) const
{
if (i >= count_)
throw std::out_of_range("async_stream_view index");
return first_ + i;
}
async_stream_array * base_ = nullptr;
std::size_t first_ = 0;
std::size_t count_ = 0;
};
// ---------------------------------------------------------------------------
// In-process backend (paired ends; each end may own its own io_context)
// ---------------------------------------------------------------------------
/// @brief Shared state for a pair of in-process async ends.
/// @details Direction `d` carries bytes written by side `d` to side `1 - d`.
/// A write that would push a direction's unread bytes past that
/// side's window waits (FIFO) until the reader consumes. Completions
/// post to the owning side's `io_context`. Closing one side fails its
/// pending I/O and gives the other side EOF once its data runs out.
class async_dual_memory_hub
{
public:
async_dual_memory_hub(asio::io_context & io_a, asio::io_context & io_b,
std::size_t streams, std::size_t window = default_wire_window())
: n_(streams)
{
if (streams == 0)
throw std::invalid_argument("async memory hub: empty");
io_[0] = &io_a;
io_[1] = &io_b;
for (int d = 0; d < 2; ++d)
{
in_[d].resize(streams);
wait_[d].resize(streams);
window_[d] = window;
}
}
async_dual_memory_hub(asio::io_context & io, std::size_t streams)
: async_dual_memory_hub(io, io, streams)
{
}
std::size_t size() const noexcept { return n_; }
asio::io_context & context(int side) noexcept { return *io_[side]; }
std::size_t window(int writer_side) const
{
std::lock_guard<std::mutex> lock(mu_);
return window_[writer_side];
}
void set_window(int writer_side, std::size_t bytes)
{
std::lock_guard<std::mutex> lock(mu_);
window_[writer_side] = bytes;
release_locked(writer_side);
}
/// @brief Bytes `writer_side` wrote that its peer has not read yet.
std::size_t unread_side(int writer_side) const
{
std::lock_guard<std::mutex> lock(mu_);
return unread_[writer_side] + pending_bytes_[writer_side];
}
stream_stats stats(int side) const
{
stream_stats s;
std::lock_guard<std::mutex> lock(mu_);
s.bytes_out = s.payload_out = out_bytes_[side];
s.bytes_in = s.payload_in = in_bytes_[side];
s.frames_out = s.write_calls = out_frames_[side];
s.frames_in = in_frames_[side];
s.buffered = unread_[side] + pending_bytes_[side];
s.unread = unread_[1 - side];
s.last_write_ns = last_write_[side];
s.last_read_ns = last_read_[side];
s.closed = closed_[side];
return s;
}
void do_write(int ws, std::size_t i, const void * src, std::size_t n,
async_handler h)
{
check(i);
std::lock_guard<std::mutex> lock(mu_);
if (closed_[ws])
{
detail::post_handler(*io_[ws], std::move(h),
asio::error::operation_aborted);
return;
}
if (closed_[1 - ws])
{
detail::post_handler(*io_[ws], std::move(h), asio::error::broken_pipe);
return;
}
if (n == 0)
{
detail::post_handler(*io_[ws], std::move(h), {});
return;
}
const auto * p = static_cast<const std::uint8_t *>(src);
if (!pending_[ws].empty() || !fits_locked(ws, n))
{
pending_[ws].push_back(
pending_write{i, std::vector<std::uint8_t>(p, p + n), std::move(h)});
pending_bytes_[ws] += n;
return;
}
deliver_locked(ws, i, p, n);
detail::post_handler(*io_[ws], std::move(h), {});
release_locked(ws);
}
void do_read(int rs, std::size_t i, void * dst, std::size_t n,
async_handler h)
{
check(i);
const int d = 1 - rs;
std::lock_guard<std::mutex> lock(mu_);
if (closed_[rs])
{
detail::post_handler(*io_[rs], std::move(h),
asio::error::operation_aborted);
return;
}
detail::stream_waiter & w = wait_[d][i];
if (w.active)
throw std::logic_error("async memory stream: overlapping read on "
"stream " + std::to_string(i));
const std::size_t k = in_[d][i].take(dst, n, compact_);
consumed_locked(d, rs, k);
if (k == n)
{
detail::post_handler(*io_[rs], std::move(h), {});
release_locked(d);
return;
}
if (closed_[d] && pending_[d].empty())
{
detail::post_handler(*io_[rs], std::move(h), asio::error::eof);
return;
}
w.active = true;
w.dst = dst;
w.n = n;
w.filled = k;
w.h = std::move(h);
release_locked(d);
}
void close_side(int s)
{
std::lock_guard<std::mutex> lock(mu_);
if (closed_[s])
return;
closed_[s] = true;
// Reads by `s` fail.
for (auto & w : wait_[1 - s])
{
if (!w.active)
continue;
w.active = false;
detail::post_handler(*io_[s], std::move(w.h),
asio::error::operation_aborted);
}
// Accepted writes by `s` still reach the peer (window no longer applies).
while (!pending_[s].empty())
{
pending_write pw = std::move(pending_[s].front());
pending_[s].pop_front();
pending_bytes_[s] -= pw.bytes.size();
deliver_locked(s, pw.i, pw.bytes.data(), pw.bytes.size());
detail::post_handler(*io_[s], std::move(pw.h), {});
}
// Peer reads that the delivered bytes cannot satisfy end with EOF.
for (auto & w : wait_[s])
{
if (!w.active)
continue;
w.active = false;
detail::post_handler(*io_[1 - s], std::move(w.h), asio::error::eof);
}
}
private:
struct pending_write
{
std::size_t i = 0;
std::vector<std::uint8_t> bytes;
async_handler h;
};
void check(std::size_t i) const
{
if (i >= n_)
throw std::out_of_range("async memory stream index "
+ std::to_string(i));
}
bool fits_locked(int ws, std::size_t n) const
{
const std::size_t w = window_[ws];
return w == 0 || unread_[ws] == 0 || unread_[ws] + n <= w;
}
void deliver_locked(int d, std::size_t i, const std::uint8_t * p,
std::size_t n)
{
auto & box = in_[d][i];
box.bytes.insert(box.bytes.end(), p, p + n);
unread_[d] += n;
out_bytes_[d] += n;
++out_frames_[d];
last_write_[d] = detail::steady_ns();
satisfy_locked(d, i);
}
void consumed_locked(int d, int rs, std::size_t k)
{
if (k == 0)
return;
unread_[d] -= std::min(unread_[d], k);
in_bytes_[rs] += k;
++in_frames_[rs];
last_read_[rs] = detail::steady_ns();
}
void satisfy_locked(int d, std::size_t i)
{
detail::stream_waiter & w = wait_[d][i];
if (!w.active)
return;
const int rs = 1 - d;
const std::size_t k = in_[d][i].take(
static_cast<std::uint8_t *>(w.dst) + w.filled, w.n - w.filled,
compact_);
consumed_locked(d, rs, k);
w.filled += k;
if (w.filled == w.n)
{
w.active = false;
detail::post_handler(*io_[rs], std::move(w.h), {});
}
}
void release_locked(int d)
{
while (!pending_[d].empty() && !closed_[1 - d]
&& fits_locked(d, pending_[d].front().bytes.size()))
{
pending_write pw = std::move(pending_[d].front());
pending_[d].pop_front();
pending_bytes_[d] -= pw.bytes.size();
deliver_locked(d, pw.i, pw.bytes.data(), pw.bytes.size());
detail::post_handler(*io_[d], std::move(pw.h), {});
}
}
asio::io_context * io_[2] = {nullptr, nullptr};
std::size_t n_ = 0;
std::size_t compact_ = wire_policy{}.compact_bytes;
mutable std::mutex mu_;
std::vector<detail::stream_inbox> in_[2];
std::vector<detail::stream_waiter> wait_[2];
std::deque<pending_write> pending_[2];
std::size_t pending_bytes_[2] = {0, 0};
std::size_t unread_[2] = {0, 0};
std::size_t window_[2] = {0, 0};
bool closed_[2] = {false, false};
std::uint64_t out_bytes_[2] = {0, 0};
std::uint64_t in_bytes_[2] = {0, 0};
std::uint64_t out_frames_[2] = {0, 0};
std::uint64_t in_frames_[2] = {0, 0};
std::int64_t last_write_[2] = {0, 0};
std::int64_t last_read_[2] = {0, 0};
};
/// @brief One end of an in-process async pair. Destroying it closes its side.
class async_dual_memory_stream_array final : public async_stream_array
{
public:
async_dual_memory_stream_array(std::shared_ptr<async_dual_memory_hub> hub,
bool side_a)
: hub_(std::move(hub)), side_(side_a ? 0 : 1)
{
if (!hub_)
throw std::invalid_argument("async memory stream needs a hub");
}
async_dual_memory_stream_array(async_dual_memory_stream_array && o) noexcept
: hub_(std::move(o.hub_)), side_(o.side_)
{
}
async_dual_memory_stream_array & operator=(
async_dual_memory_stream_array && o) noexcept
{
if (this != &o)
{
if (hub_)
hub_->close_side(side_);
hub_ = std::move(o.hub_);
side_ = o.side_;
}
return *this;
}
async_dual_memory_stream_array(const async_dual_memory_stream_array &) = delete;
async_dual_memory_stream_array & operator=(
const async_dual_memory_stream_array &) = delete;
~async_dual_memory_stream_array() override
{
if (hub_)
hub_->close_side(side_);
}
std::size_t size() const noexcept override { return hub_->size(); }
asio::io_context & context() noexcept override
{
return hub_->context(side_);
}
void async_write(std::size_t i, const void * src, std::size_t n,
async_handler h) override
{
hub_->do_write(side_, i, src, n, std::move(h));
}
void async_read(std::size_t i, void * dst, std::size_t n,
async_handler h) override
{
hub_->do_read(side_, i, dst, n, std::move(h));
}
std::size_t buffered_bytes() const noexcept override
{
return hub_->unread_side(side_);
}
std::size_t window_bytes() const noexcept override
{
return hub_->window(side_);
}
void set_window_bytes(std::size_t bytes) override
{
hub_->set_window(side_, bytes);
}
stream_stats stats() const override { return hub_->stats(side_); }
void close() noexcept override
{
if (hub_)
hub_->close_side(side_);
}
private:
std::shared_ptr<async_dual_memory_hub> hub_;
int side_ = 0;
};
using async_memory_hub = async_dual_memory_hub;
using async_memory_stream_array = async_dual_memory_stream_array;
/// @brief Two ends sharing one `io_context`.
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<async_memory_stream_array, async_memory_stream_array>
make_async_memory_stream_pair(asio::io_context & io, std::size_t streams)
{
auto hub = std::make_shared<async_dual_memory_hub>(io, streams);
return {async_memory_stream_array(hub, true),
async_memory_stream_array(hub, false)};
}
/// @brief Two ends, each completing on its own `io_context`.
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<async_dual_memory_stream_array, async_dual_memory_stream_array>
make_async_dual_memory_stream_pair(asio::io_context & io_a,
asio::io_context & io_b, std::size_t streams,
std::size_t window = default_wire_window())
{
auto hub = std::make_shared<async_dual_memory_hub>(io_a, io_b, streams,
window);
return {async_dual_memory_stream_array(hub, true),
async_dual_memory_stream_array(hub, false)};
}
// ---------------------------------------------------------------------------
// Mux backend (N lanes on ONE TCP socket)
// ---------------------------------------------------------------------------
/// @brief N lanes multiplexed on one TCP socket, fully overlapped.
/// @details Wire frame: `u32 total` (little-endian) `|| stream_frame_hdr`
/// (`u16 index`, `u32 length`) `|| payload`, with
/// `total = sizeof(stream_frame_hdr) + length`. The synchronous
/// `mux_stream_array` writes the same frames, so the two interoperate.
/// All socket operations run on one strand. The window covers the
/// whole connection: every lane shares one TCP byte stream.
template <typename Stream>
class basic_async_mux_stream_array final : public async_stream_array
{
public:
basic_async_mux_stream_array(asio::io_context & io, Stream sock,
unsigned self_id, unsigned peer_id, std::size_t nstreams,
const wire_policy & pol = {})
{
if (nstreams == 0)
throw std::invalid_argument("async_mux_stream_array empty");
if (nstreams > 0xffffu)
throw std::invalid_argument("async_mux_stream_array: lane ids are u16");
if (self_id == peer_id)
throw std::invalid_argument("async_mux_stream_array roles");
pol.validate();
tune_stream(sock, pol.socket);
impl_ = std::make_shared<impl>(io, std::move(sock), nstreams, pol);
impl_->start();
}
basic_async_mux_stream_array(const basic_async_mux_stream_array &) = delete;
basic_async_mux_stream_array & operator=(const basic_async_mux_stream_array &)
= delete;
~basic_async_mux_stream_array() override
{
if (impl_)
impl_->close_graceful();
}
std::size_t size() const noexcept override { return impl_->n; }
asio::io_context & context() noexcept override { return impl_->io; }
void async_write(std::size_t i, const void * src, std::size_t n,
async_handler h) override
{
impl_->write(i, n == 0 ? nullptr : copy_buffer(src, n), std::move(h));
}
void async_write_owned(std::size_t i,
std::shared_ptr<std::vector<std::uint8_t>> buf, async_handler h) override
{
impl_->write(i, std::move(buf), std::move(h));
}
void async_read(std::size_t i, void * dst, std::size_t n,
async_handler h) override
{
impl_->read(i, dst, n, std::move(h));
}
std::size_t buffered_bytes() const noexcept override
{
return impl_->buffered.load(std::memory_order_relaxed);
}
std::size_t window_bytes() const noexcept override
{
return impl_->window.load(std::memory_order_relaxed);
}
void set_window_bytes(std::size_t bytes) override
{
impl_->window.store(bytes, std::memory_order_relaxed);
}
stream_stats stats() const override { return impl_->stats(); }
void close() noexcept override
{
impl_->close(asio::error::operation_aborted);
}
private:
struct impl : std::enable_shared_from_this<impl>
{
static constexpr std::size_t hdr_wire = 4 + sizeof(stream_frame_hdr);
struct chunk
{
std::uint32_t total_le = 0;
stream_frame_hdr hdr{};
std::shared_ptr<std::vector<std::uint8_t>> payload;
std::size_t off = 0;
std::size_t len = 0;
std::shared_ptr<detail::write_op> op;
};
impl(asio::io_context & io_, Stream sock_,
std::size_t nstreams, const wire_policy & pol_)
: io(io_),
sock(std::move(sock_)),
strand(asio::make_strand(io_)),
n(nstreams),
pol(pol_),
inbox(nstreams),
wait(nstreams),
outq(nstreams),
window(pol_.window_bytes)
{
fd = base_socket(sock).native_handle();
}
void check(std::size_t i) const
{
if (i >= n)
throw std::out_of_range("async_mux_stream_array index "
+ std::to_string(i));
}
void start()
{
auto self = this->shared_from_this();
asio::post(strand, [self] { self->read_hdr(); });
}
// --- reads ---------------------------------------------------------
void read(std::size_t i, void * dst, std::size_t nbytes, async_handler h)
{
check(i);
std::lock_guard<std::mutex> lock(mu);
if (fail)
{
detail::post_handler(io, std::move(h), fail);
return;
}
detail::stream_waiter & w = wait[i];
if (w.active)
throw std::logic_error(
"async_mux_stream_array: overlapping read on stream "
+ std::to_string(i));
const std::size_t k = inbox[i].take(dst, nbytes, pol.compact_bytes);
if (k == nbytes)
{
detail::post_handler(io, std::move(h), {});
return;
}
w.active = true;
w.reading = false;
w.dst = dst;
w.n = nbytes;
w.filled = k;
w.h = std::move(h);
w.abort.clear();
}
void satisfy_locked(std::size_t i)
{
detail::stream_waiter & w = wait[i];
if (!w.active || w.reading)
return;
w.filled += inbox[i].take(static_cast<std::uint8_t *>(w.dst) + w.filled,
w.n - w.filled, pol.compact_bytes);
if (w.filled == w.n)
{
w.active = false;
detail::post_handler(io, std::move(w.h), {});
}
}
void read_hdr()
{
auto self = this->shared_from_this();
asio::async_read(sock, asio::buffer(hdr_buf, hdr_wire),
asio::bind_executor(strand,
[self](const std::error_code & ec, std::size_t) {
self->on_hdr(ec);
}));
}
void on_hdr(const std::error_code & ec)
{
if (ec)
{
deliver_error(detail::end_of_stream(ec));
return;
}
std::uint32_t total = 0;
std::memcpy(&total, hdr_buf, 4);
stream_frame_hdr hdr{};
std::memcpy(&hdr, hdr_buf + 4, sizeof(hdr));
if (hdr.index >= n || total != sizeof(stream_frame_hdr) + hdr.length)
{
deliver_error(std::make_error_code(std::errc::protocol_error));
return;
}
if (hdr.length > pol.max_frame)
{
deliver_error(std::make_error_code(std::errc::message_size));
return;
}
cur_index = hdr.index;
cur_len = hdr.length;
if (cur_len == 0)
{
counters.read(hdr_wire, 0, 1);
read_hdr();
return;
}
void * dst = nullptr;
cur_direct = false;
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
detail::stream_waiter & w = wait[cur_index];
if (w.active && !w.reading && inbox[cur_index].avail() == 0
&& w.filled + cur_len <= w.n)
{
w.reading = true;
cur_direct = true;
dst = static_cast<std::uint8_t *>(w.dst) + w.filled;
}
}
if (!cur_direct)
{
rbuf.resize(cur_len);
dst = rbuf.data();
}
auto self = this->shared_from_this();
asio::async_read(sock, asio::buffer(dst, cur_len),
asio::bind_executor(strand,
[self](const std::error_code & xec, std::size_t) {
self->on_body(xec);
}));
}
void on_body(const std::error_code & ec)
{
if (cur_direct)
{
std::lock_guard<std::mutex> lock(mu);
detail::stream_waiter & w = wait[cur_index];
w.reading = false;
if (w.active && (ec || w.abort))
{
w.active = false;
detail::post_handler(io, std::move(w.h), ec ? ec : w.abort);
}
else if (w.active && !ec)
{
w.filled += cur_len;
if (w.filled == w.n)
{
w.active = false;
detail::post_handler(io, std::move(w.h), {});
}
}
}
if (ec)
{
deliver_error(detail::end_of_stream(ec));
return;
}
counters.read(hdr_wire + cur_len, cur_len, 1);
if (!cur_direct)
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
auto & box = inbox[cur_index].bytes;
box.insert(box.end(), rbuf.begin(), rbuf.end());
satisfy_locked(cur_index);
}
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
}
if (pol.socket.quickack)
rearm_quickack(base_socket(sock));
read_hdr();
}
void fail_waiters_locked(const std::error_code & ec)
{
for (auto & w : wait)
{
if (!w.active)
continue;
if (w.reading)
{
// Completes once the in-flight socket read ends: `dst`
// must stay owned by the handler until then.
w.abort = ec;
continue;
}
w.active = false;
detail::post_handler(io, std::move(w.h), ec);
}
}
void deliver_error(const std::error_code & ec)
{
{
std::lock_guard<std::mutex> lock(mu);
if (!fail)
fail = ec;
fail_waiters_locked(fail);
}
auto self = this->shared_from_this();
asio::post(strand, [self, ec] {
std::error_code e;
base_socket(self->sock).shutdown(asio::socket_base::shutdown_both, e);
self->fail_queued(ec);
});
}
void close(const std::error_code & ec)
{
{
std::lock_guard<std::mutex> lock(mu);
if (!fail)
fail = ec;
if (closed && aborted)
return;
closed = true;
aborted = true;
fail_waiters_locked(ec);
}
auto self = this->shared_from_this();
asio::post(strand, [self, ec] {
std::error_code e;
base_socket(self->sock).shutdown(asio::socket_base::shutdown_both, e);
base_socket(self->sock).close(e);
self->fail_queued(ec);
});
}
/// @brief Stop reads now; close the socket once queued writes drain.
void close_graceful()
{
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
closed = true;
fail_waiters_locked(asio::error::operation_aborted);
}
auto self = this->shared_from_this();
asio::post(strand, [self] {
self->draining = true;
if (!self->writing)
self->finish_close();
});
}
void finish_close()
{
std::error_code e;
base_socket(sock).shutdown(asio::socket_base::shutdown_both, e);
base_socket(sock).close(e);
}
stream_stats stats() const
{
stream_stats s;
counters.fill(s);
s.buffered = buffered.load(std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(mu);
for (const auto & b : inbox)
s.unread += b.avail();
s.error = fail;
s.closed = closed;
if (!closed)
detail::add_socket_bytes(fd, s);
return s;
}
// --- writes (strand) ----------------------------------------------
void write(std::size_t i, std::shared_ptr<std::vector<std::uint8_t>> payload,
async_handler h)
{
check(i);
const std::size_t len = payload ? payload->size() : 0;
{
std::lock_guard<std::mutex> lock(mu);
if (fail || closed)
{
detail::post_handler(io, std::move(h),
fail ? fail : asio::error::operation_aborted);
return;
}
}
if (len == 0)
{
detail::post_handler(io, std::move(h), {});
return;
}
if (pol.chunk_bytes == 0 && len > 0xffffffffu - sizeof(stream_frame_hdr))
throw std::invalid_argument("async_mux_stream_array frame too large");
const auto pieces = detail::split_chunks(len, pol.chunk_bytes);
auto op = std::make_shared<detail::write_op>();
op->h = std::move(h);
op->left = pieces.size();
std::vector<chunk> cs;
cs.reserve(pieces.size());
std::size_t wire = 0;
for (const auto & pc : pieces)
{
chunk c;
c.hdr.index = static_cast<std::uint16_t>(i);
c.hdr.length = static_cast<std::uint32_t>(pc.second);
c.total_le = static_cast<std::uint32_t>(
sizeof(stream_frame_hdr) + pc.second);
c.payload = payload;
c.off = pc.first;
c.len = pc.second;
c.op = op;
wire += hdr_wire + pc.second;
cs.push_back(std::move(c));
}
buffered.fetch_add(wire, std::memory_order_relaxed);
auto self = this->shared_from_this();
asio::post(strand, [self, i, cs = std::move(cs)]() mutable {
self->enqueue(i, std::move(cs));
});
}
void enqueue(std::size_t i, std::vector<chunk> cs)
{
std::error_code ec;
{
std::lock_guard<std::mutex> lock(mu);
ec = fail;
}
if (ec)
{
for (auto & c : cs)
finish_chunk(c, ec);
return;
}
for (auto & c : cs)
outq[i].push_back(std::move(c));
if (!writing)
start_write();
}
void finish_chunk(chunk & c, const std::error_code & ec)
{
buffered.fetch_sub(std::min(buffered.load(std::memory_order_relaxed),
hdr_wire + c.len),
std::memory_order_relaxed);
auto & op = *c.op;
if (ec && !op.ec)
op.ec = ec;
if (--op.left == 0)
detail::post_handler(io, std::move(op.h), op.ec);
}
void start_write()
{
inflight.clear();
std::size_t bytes = 0;
bool stop = false;
while (!stop && inflight.size() < pol.coalesce_frames)
{
bool found = false;
for (std::size_t k = 0; k < n; ++k)
{
const std::size_t lane = (rr + k) % n;
if (outq[lane].empty())
continue;
const std::size_t wire = hdr_wire + outq[lane].front().len;
if (!inflight.empty() && bytes + wire > pol.coalesce_bytes)
{
stop = true;
break;
}
inflight.push_back(std::move(outq[lane].front()));
outq[lane].pop_front();
bytes += wire;
rr = (lane + 1) % n;
found = true;
break;
}
if (!found)
break;
}
if (inflight.empty())
{
writing = false;
if (draining)
finish_close();
return;
}
writing = true;
inflight_wire = bytes;
std::vector<asio::const_buffer> bufs;
bufs.reserve(inflight.size() * 3);
for (auto & c : inflight)
{
bufs.push_back(asio::buffer(&c.total_le, 4));
bufs.push_back(asio::buffer(&c.hdr, sizeof(c.hdr)));
bufs.push_back(asio::buffer(c.payload->data() + c.off, c.len));
}
auto self = this->shared_from_this();
if constexpr (detail::is_layered_stream<Stream>::value)
{
// TLS seals each buffer as its own record; send the batch as one.
auto flat = detail::flatten(bufs, bytes);
asio::async_write(sock, asio::buffer(*flat),
asio::bind_executor(strand,
[self, flat](const std::error_code & ec, std::size_t) {
self->on_written(ec);
}));
return;
}
asio::async_write(sock, bufs,
asio::bind_executor(strand,
[self](const std::error_code & ec, std::size_t) {
self->on_written(ec);
}));
}
void on_written(const std::error_code & ec)
{
if (!ec)
{
std::size_t payload = 0;
for (const auto & c : inflight)
payload += c.len;
counters.wrote(inflight_wire, payload, inflight.size());
}
for (auto & c : inflight)
finish_chunk(c, ec);
inflight.clear();
if (ec)
{
writing = false;
deliver_error(detail::end_of_stream(ec));
if (draining)
finish_close();
return;
}
if (pol.socket.quickack)
rearm_quickack(base_socket(sock));
start_write();
}
void fail_queued(const std::error_code & ec)
{
for (auto & q : outq)
{
for (auto & c : q)
finish_chunk(c, ec);
q.clear();
}
}
asio::io_context & io;
Stream sock;
int fd = -1;
asio::strand<asio::io_context::executor_type> strand;
std::size_t n = 0;
wire_policy pol;
mutable std::mutex mu;
std::vector<detail::stream_inbox> inbox;
std::vector<detail::stream_waiter> wait;
std::error_code fail;
bool closed = false;
bool aborted = false;
// strand-only
std::vector<std::deque<chunk>> outq;
std::size_t rr = 0;
bool writing = false;
bool draining = false;
std::vector<chunk> inflight;
std::size_t inflight_wire = 0;
std::uint8_t hdr_buf[hdr_wire]{};
std::vector<std::uint8_t> rbuf;
std::size_t cur_index = 0;
std::size_t cur_len = 0;
bool cur_direct = false;
std::atomic<std::size_t> buffered{0};
std::atomic<std::size_t> window;
detail::io_counters counters;
};
std::shared_ptr<impl> impl_;
};
/// @brief Mux over a plain TCP socket.
using async_mux_stream_array = basic_async_mux_stream_array<asio::ip::tcp::socket>;
// ---------------------------------------------------------------------------
// Parallel backend (one socket PER lane)
// ---------------------------------------------------------------------------
/// @brief One dedicated socket per lane; each lane fully independent.
/// @details Wire frame per socket: `u32 length || payload`. Each socket has its
/// own strand, write queue, window, and read loop. Queued frames on a
/// socket are written in one gathered syscall.
template <typename Socket>
class basic_async_parallel_stream_array final : public async_stream_array
{
public:
basic_async_parallel_stream_array(asio::io_context & io,
std::vector<Socket> socks, const wire_policy & pol = {})
{
if (socks.empty())
throw std::invalid_argument("async_parallel_stream_array empty");
pol.validate();
impl_ = std::make_shared<impl>(io, std::move(socks), pol);
impl_->start();
}
basic_async_parallel_stream_array(const basic_async_parallel_stream_array &)
= delete;
basic_async_parallel_stream_array & operator=(
const basic_async_parallel_stream_array &) = delete;
~basic_async_parallel_stream_array() override
{
if (impl_)
impl_->close_graceful();
}
std::size_t size() const noexcept override { return impl_->conns.size(); }
asio::io_context & context() noexcept override { return impl_->io; }
void async_write(std::size_t i, const void * src, std::size_t n,
async_handler h) override
{
impl_->write(i, n == 0 ? nullptr : copy_buffer(src, n), std::move(h));
}
void async_write_owned(std::size_t i,
std::shared_ptr<std::vector<std::uint8_t>> buf, async_handler h) override
{
impl_->write(i, std::move(buf), std::move(h));
}
void async_read(std::size_t i, void * dst, std::size_t n,
async_handler h) override
{
impl_->read(i, dst, n, std::move(h));
}
std::size_t buffered_bytes() const noexcept override
{
std::size_t total = 0;
for (const auto & c : impl_->conns)
total += c->buffered.load(std::memory_order_relaxed);
return total;
}
/// @brief Sum of per-socket windows (matches `buffered_bytes`).
std::size_t window_bytes() const noexcept override
{
std::size_t total = 0;
for (const auto & c : impl_->conns)
{
const std::size_t w = c->window.load(std::memory_order_relaxed);
if (w == 0)
return 0;
total += w;
}
return total;
}
/// @brief Set every socket's window to `bytes`.
void set_window_bytes(std::size_t bytes) override
{
for (auto & c : impl_->conns)
c->window.store(bytes, std::memory_order_relaxed);
}
std::size_t lane_buffered_bytes(std::size_t i) const noexcept override
{
return i < impl_->conns.size()
? impl_->conns[i]->buffered.load(std::memory_order_relaxed)
: 0;
}
std::size_t lane_window_bytes(std::size_t i) const noexcept override
{
return i < impl_->conns.size()
? impl_->conns[i]->window.load(std::memory_order_relaxed)
: 0;
}
stream_stats stats() const override { return impl_->stats(); }
void close() noexcept override
{
impl_->close(asio::error::operation_aborted);
}
private:
struct chunk
{
std::uint32_t len_le = 0;
std::shared_ptr<std::vector<std::uint8_t>> payload;
std::size_t off = 0;
std::size_t len = 0;
std::shared_ptr<detail::write_op> op;
};
struct conn
{
conn(Socket s, asio::io_context & io, const wire_policy & pol)
: sock(std::move(s)),
strand(asio::make_strand(io)),
window(pol.window_bytes)
{
tune_stream(sock, pol.socket);
fd = base_socket(sock).native_handle();
}
Socket sock;
int fd = -1;
asio::strand<asio::io_context::executor_type> strand;
detail::stream_inbox inbox; // guarded by impl::mu
detail::stream_waiter wait; // guarded by impl::mu
std::deque<chunk> outq; // strand
std::vector<chunk> inflight; // strand
std::size_t inflight_wire = 0; // strand
bool writing = false; // strand
bool draining = false; // strand
std::uint32_t rlen_le = 0; // strand
std::vector<std::uint8_t> rbuf; // strand
std::size_t cur_len = 0; // strand
bool cur_direct = false; // strand
std::atomic<std::size_t> buffered{0};
std::atomic<std::size_t> window;
};
struct impl : std::enable_shared_from_this<impl>
{
impl(asio::io_context & io_, std::vector<Socket> socks,
const wire_policy & pol_)
: io(io_), pol(pol_)
{
conns.reserve(socks.size());
for (auto & s : socks)
conns.push_back(std::make_unique<conn>(std::move(s), io_, pol_));
}
void check(std::size_t i) const
{
if (i >= conns.size())
throw std::out_of_range("async_parallel_stream_array index "
+ std::to_string(i));
}
void start()
{
for (std::size_t i = 0; i < conns.size(); ++i)
{
auto self = this->shared_from_this();
asio::post(conns[i]->strand, [self, i] { self->read_len(i); });
}
}
// --- reads ---------------------------------------------------------
void read(std::size_t i, void * dst, std::size_t nbytes, async_handler h)
{
check(i);
conn & c = *conns[i];
std::lock_guard<std::mutex> lock(mu);
if (fail)
{
detail::post_handler(io, std::move(h), fail);
return;
}
if (c.wait.active)
throw std::logic_error(
"async_parallel_stream_array: overlapping read on stream "
+ std::to_string(i));
const std::size_t k = c.inbox.take(dst, nbytes, pol.compact_bytes);
if (k == nbytes)
{
detail::post_handler(io, std::move(h), {});
return;
}
c.wait.active = true;
c.wait.reading = false;
c.wait.dst = dst;
c.wait.n = nbytes;
c.wait.filled = k;
c.wait.h = std::move(h);
c.wait.abort.clear();
}
void satisfy_locked(conn & c)
{
auto & w = c.wait;
if (!w.active || w.reading)
return;
w.filled += c.inbox.take(static_cast<std::uint8_t *>(w.dst) + w.filled,
w.n - w.filled, pol.compact_bytes);
if (w.filled == w.n)
{
w.active = false;
detail::post_handler(io, std::move(w.h), {});
}
}
void read_len(std::size_t i)
{
auto self = this->shared_from_this();
conn & c = *conns[i];
asio::async_read(c.sock, asio::buffer(&c.rlen_le, 4),
asio::bind_executor(c.strand,
[self, i](const std::error_code & ec, std::size_t) {
self->on_len(i, ec);
}));
}
void on_len(std::size_t i, const std::error_code & ec)
{
conn & c = *conns[i];
if (ec)
{
deliver_error(detail::end_of_stream(ec));
return;
}
std::uint32_t len = 0;
std::memcpy(&len, &c.rlen_le, 4);
if (len > pol.max_frame)
{
deliver_error(std::make_error_code(std::errc::message_size));
return;
}
c.cur_len = len;
if (len == 0)
{
counters.read(4, 0, 1);
read_len(i);
return;
}
void * dst = nullptr;
c.cur_direct = false;
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
auto & w = c.wait;
if (w.active && !w.reading && c.inbox.avail() == 0
&& w.filled + len <= w.n)
{
w.reading = true;
c.cur_direct = true;
dst = static_cast<std::uint8_t *>(w.dst) + w.filled;
}
}
if (!c.cur_direct)
{
c.rbuf.resize(len);
dst = c.rbuf.data();
}
auto self = this->shared_from_this();
asio::async_read(c.sock, asio::buffer(dst, len),
asio::bind_executor(c.strand,
[self, i](const std::error_code & xec, std::size_t) {
self->on_body(i, xec);
}));
}
void on_body(std::size_t i, const std::error_code & ec)
{
conn & c = *conns[i];
if (c.cur_direct)
{
std::lock_guard<std::mutex> lock(mu);
auto & w = c.wait;
w.reading = false;
if (w.active && (ec || w.abort))
{
w.active = false;
detail::post_handler(io, std::move(w.h), ec ? ec : w.abort);
}
else if (w.active && !ec)
{
w.filled += c.cur_len;
if (w.filled == w.n)
{
w.active = false;
detail::post_handler(io, std::move(w.h), {});
}
}
}
if (ec)
{
deliver_error(detail::end_of_stream(ec));
return;
}
counters.read(4 + c.cur_len, c.cur_len, 1);
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
if (!c.cur_direct)
{
c.inbox.bytes.insert(c.inbox.bytes.end(), c.rbuf.begin(),
c.rbuf.end());
satisfy_locked(c);
}
}
rearm_stream(c.sock, pol.socket);
read_len(i);
}
void fail_waiters_locked(const std::error_code & ec)
{
for (auto & cp : conns)
{
auto & w = cp->wait;
if (!w.active)
continue;
if (w.reading)
{
w.abort = ec;
continue;
}
w.active = false;
detail::post_handler(io, std::move(w.h), ec);
}
}
void deliver_error(const std::error_code & ec)
{
{
std::lock_guard<std::mutex> lock(mu);
if (!fail)
fail = ec;
fail_waiters_locked(fail);
}
shutdown_all(ec, false);
}
void close(const std::error_code & ec)
{
{
std::lock_guard<std::mutex> lock(mu);
if (!fail)
fail = ec;
if (closed && aborted)
return;
closed = true;
aborted = true;
fail_waiters_locked(ec);
}
shutdown_all(ec, true);
}
void close_graceful()
{
{
std::lock_guard<std::mutex> lock(mu);
if (closed)
return;
closed = true;
fail_waiters_locked(asio::error::operation_aborted);
}
for (std::size_t i = 0; i < conns.size(); ++i)
{
auto self = this->shared_from_this();
asio::post(conns[i]->strand, [self, i] {
conn & c = *self->conns[i];
c.draining = true;
if (!c.writing)
finish_close(c);
});
}
}
static void finish_close(conn & c)
{
std::error_code e;
base_socket(c.sock).shutdown(asio::socket_base::shutdown_both, e);
base_socket(c.sock).close(e);
}
void shutdown_all(const std::error_code & ec, bool close_sockets)
{
for (std::size_t i = 0; i < conns.size(); ++i)
{
auto self = this->shared_from_this();
asio::post(conns[i]->strand, [self, i, ec, close_sockets] {
conn & c = *self->conns[i];
std::error_code e;
base_socket(c.sock).shutdown(asio::socket_base::shutdown_both, e);
if (close_sockets)
base_socket(c.sock).close(e);
for (auto & ch : c.outq)
self->finish_chunk(c, ch, ec);
c.outq.clear();
});
}
}
stream_stats stats() const
{
stream_stats s;
counters.fill(s);
for (const auto & c : conns)
s.buffered += c->buffered.load(std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(mu);
for (const auto & c : conns)
{
s.unread += c->inbox.avail();
if (!closed)
detail::add_socket_bytes(c->fd, s);
}
s.error = fail;
s.closed = closed;
return s;
}
// --- writes --------------------------------------------------------
void write(std::size_t i, std::shared_ptr<std::vector<std::uint8_t>> payload,
async_handler h)
{
check(i);
const std::size_t len = payload ? payload->size() : 0;
{
std::lock_guard<std::mutex> lock(mu);
if (fail || closed)
{
detail::post_handler(io, std::move(h),
fail ? fail : asio::error::operation_aborted);
return;
}
}
if (len == 0)
{
detail::post_handler(io, std::move(h), {});
return;
}
if (pol.chunk_bytes == 0 && len > 0xffffffffu)
throw std::invalid_argument(
"async_parallel_stream_array frame too large");
const auto pieces = detail::split_chunks(len, pol.chunk_bytes);
auto op = std::make_shared<detail::write_op>();
op->h = std::move(h);
op->left = pieces.size();
std::vector<chunk> cs;
cs.reserve(pieces.size());
std::size_t wire = 0;
for (const auto & pc : pieces)
{
chunk c;
c.len_le = static_cast<std::uint32_t>(pc.second);
c.payload = payload;
c.off = pc.first;
c.len = pc.second;
c.op = op;
wire += 4 + pc.second;
cs.push_back(std::move(c));
}
conn & cn = *conns[i];
cn.buffered.fetch_add(wire, std::memory_order_relaxed);
auto self = this->shared_from_this();
asio::post(cn.strand, [self, i, cs = std::move(cs)]() mutable {
self->enqueue(i, std::move(cs));
});
}
void enqueue(std::size_t i, std::vector<chunk> cs)
{
conn & c = *conns[i];
std::error_code ec;
{
std::lock_guard<std::mutex> lock(mu);
ec = fail;
}
if (ec)
{
for (auto & ch : cs)
finish_chunk(c, ch, ec);
return;
}
for (auto & ch : cs)
c.outq.push_back(std::move(ch));
if (!c.writing)
start_write(i);
}
void finish_chunk(conn & c, chunk & ch, const std::error_code & ec)
{
const std::size_t wire = 4 + ch.len;
c.buffered.fetch_sub(
std::min(c.buffered.load(std::memory_order_relaxed), wire),
std::memory_order_relaxed);
auto & op = *ch.op;
if (ec && !op.ec)
op.ec = ec;
if (--op.left == 0)
detail::post_handler(io, std::move(op.h), op.ec);
}
void start_write(std::size_t i)
{
conn & c = *conns[i];
c.inflight.clear();
std::size_t bytes = 0;
while (!c.outq.empty() && c.inflight.size() < pol.coalesce_frames)
{
const std::size_t wire = 4 + c.outq.front().len;
if (!c.inflight.empty() && bytes + wire > pol.coalesce_bytes)
break;
c.inflight.push_back(std::move(c.outq.front()));
c.outq.pop_front();
bytes += wire;
}
if (c.inflight.empty())
{
c.writing = false;
if (c.draining)
finish_close(c);
return;
}
c.writing = true;
c.inflight_wire = bytes;
std::vector<asio::const_buffer> bufs;
bufs.reserve(c.inflight.size() * 2);
for (auto & ch : c.inflight)
{
bufs.push_back(asio::buffer(&ch.len_le, 4));
bufs.push_back(asio::buffer(ch.payload->data() + ch.off, ch.len));
}
auto self = this->shared_from_this();
if constexpr (detail::is_layered_stream<Socket>::value)
{
auto flat = detail::flatten(bufs, bytes);
asio::async_write(c.sock, asio::buffer(*flat),
asio::bind_executor(c.strand,
[self, i, flat](const std::error_code & ec, std::size_t) {
self->on_written(i, ec);
}));
return;
}
asio::async_write(c.sock, bufs,
asio::bind_executor(c.strand,
[self, i](const std::error_code & ec, std::size_t) {
self->on_written(i, ec);
}));
}
void on_written(std::size_t i, const std::error_code & ec)
{
conn & c = *conns[i];
if (!ec)
{
std::size_t payload = 0;
for (const auto & ch : c.inflight)
payload += ch.len;
counters.wrote(c.inflight_wire, payload, c.inflight.size());
}
for (auto & ch : c.inflight)
finish_chunk(c, ch, ec);
c.inflight.clear();
if (ec)
{
c.writing = false;
for (auto & ch : c.outq)
finish_chunk(c, ch, ec);
c.outq.clear();
deliver_error(detail::end_of_stream(ec));
if (c.draining)
finish_close(c);
return;
}
rearm_stream(c.sock, pol.socket);
start_write(i);
}
asio::io_context & io;
wire_policy pol;
mutable std::mutex mu;
std::vector<std::unique_ptr<conn>> conns;
std::error_code fail;
bool closed = false;
bool aborted = false;
detail::io_counters counters;
};
std::shared_ptr<impl> impl_;
};
/// @brief Parallel array over TCP sockets (one connection per lane).
using async_parallel_stream_array =
basic_async_parallel_stream_array<asio::ip::tcp::socket>;
/// @brief Parallel array over unix-domain stream sockets.
using async_local_parallel_stream_array =
basic_async_parallel_stream_array<asio::local::stream_protocol::socket>;
#if DPF_HAS_OPENSSL
#include "dpf/net/tls.hpp"
/// @brief N lanes on one TLS connection.
using async_tls_mux_stream_array = basic_async_mux_stream_array<tls_stream>;
/// @brief One TLS connection per lane.
using async_tls_parallel_stream_array = basic_async_parallel_stream_array<tls_stream>;
#endif
// ---------------------------------------------------------------------------
// Connection helpers (setup only; protocol I/O stays async)
// ---------------------------------------------------------------------------
/// @brief Accept `nstreams` lane sockets on ONE port.
/// @details Binds `base_port` (0 = ephemeral), publishes the port, and accepts
/// one connection per lane. Each connection announces its lane index
/// as a `u32`, so accept order does not matter.
HEDLEY_WARN_UNUSED_RESULT
inline std::vector<asio::ip::tcp::socket> accept_parallel_tcp(
asio::io_context & io, std::atomic<unsigned short> & base_port,
std::size_t nstreams,
std::chrono::milliseconds budget = deadlines{}.accept)
{
if (nstreams == 0)
throw std::invalid_argument("accept_parallel_tcp empty");
asio::ip::tcp::acceptor acc(io);
open_listener(acc, base_port.load());
base_port.store(acc.local_endpoint().port());
std::vector<asio::ip::tcp::socket> socks;
std::vector<bool> have(nstreams, false);
for (std::size_t k = 0; k < nstreams; ++k)
socks.emplace_back(io);
for (std::size_t k = 0; k < nstreams; ++k)
{
asio::ip::tcp::socket s(io);
accept_until(acc, s, budget);
const std::uint32_t lane = exchange_u32(s.native_handle(),
static_cast<std::uint32_t>(nstreams), budget, "parallel lane handshake");
if (lane >= nstreams || have[lane])
throw std::runtime_error("accept_parallel_tcp: bad lane "
+ std::to_string(lane));
have[lane] = true;
socks[lane] = std::move(s);
}
return socks;
}
/// @brief Connect `nstreams` lane sockets to one port (lane index handshake).
HEDLEY_WARN_UNUSED_RESULT
inline std::vector<asio::ip::tcp::socket> connect_parallel_tcp(
asio::io_context & io, const std::string & host,
std::atomic<unsigned short> & base_port, std::size_t nstreams,
std::chrono::milliseconds budget = deadlines{}.connect)
{
if (nstreams == 0)
throw std::invalid_argument("connect_parallel_tcp empty");
std::vector<asio::ip::tcp::socket> socks;
socks.reserve(nstreams);
for (std::size_t k = 0; k < nstreams; ++k)
{
asio::ip::tcp::socket s(io);
connect_until(s, host, base_port.load(), budget);
const std::uint32_t n = exchange_u32(s.native_handle(),
static_cast<std::uint32_t>(k), budget, "parallel lane handshake");
if (n != nstreams)
throw std::runtime_error("connect_parallel_tcp: peer expects "
+ std::to_string(n) + " lanes, this side "
+ std::to_string(nstreams));
socks.push_back(std::move(s));
}
return socks;
}
/// @brief In-process unix socket pairs: lane `k` connects `ends.first[k]`
/// (on `io_a`) to `ends.second[k]` (on `io_b`).
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<std::vector<asio::local::stream_protocol::socket>,
std::vector<asio::local::stream_protocol::socket>>
make_local_socket_pairs(asio::io_context & io_a, asio::io_context & io_b,
std::size_t nstreams)
{
std::pair<std::vector<asio::local::stream_protocol::socket>,
std::vector<asio::local::stream_protocol::socket>>
out;
out.first.reserve(nstreams);
out.second.reserve(nstreams);
for (std::size_t k = 0; k < nstreams; ++k)
{
asio::local::stream_protocol::socket a(io_a);
asio::local::stream_protocol::socket b(io_b);
asio::local::connect_pair(a, b);
out.first.push_back(std::move(a));
out.second.push_back(std::move(b));
}
return out;
}
} // namespace net
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_NET_ASYNC_STREAM_ARRAY_HPP__