use core::ops::ControlFlow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrainState {
More,
Drained,
}
pub trait DrainSource {
type Item: ?Sized;
fn drain_ready(
&mut self,
visitor: &mut dyn FnMut(&Self::Item) -> ControlFlow<()>,
) -> DrainState;
}
pub struct DrainFanIn<S, const N: usize> {
sources: [S; N],
live: [bool; N],
remaining: usize,
cursor: usize,
}
impl<S, const N: usize> DrainFanIn<S, N> {
#[must_use]
pub fn new(sources: [S; N]) -> Self {
Self {
sources,
live: [true; N],
remaining: N,
cursor: 0,
}
}
#[must_use]
pub fn live_count(&self) -> usize {
self.remaining
}
pub fn drain_each<F>(&mut self, mut visitor: F) -> DrainState
where
S: DrainSource,
F: FnMut(&S::Item) -> ControlFlow<()>,
{
if self.remaining == 0 {
return DrainState::Drained;
}
let start = self.cursor;
let mut any_more = false;
for step in 0..N {
let index = (start + step) % N;
if !self.live[index] {
continue;
}
let mut broke = false;
let state = self.sources[index].drain_ready(&mut |item| {
let flow = visitor(item);
if flow.is_break() {
broke = true;
}
flow
});
match state {
DrainState::Drained => {
self.live[index] = false;
self.remaining -= 1;
if self.remaining == 0 {
return DrainState::Drained;
}
}
DrainState::More => any_more = true,
}
if broke {
self.cursor = index; return DrainState::More;
}
}
self.cursor = (start + 1) % N; if any_more {
DrainState::More
} else {
DrainState::Drained
}
}
}
impl<S: DrainSource, const N: usize> DrainSource for DrainFanIn<S, N> {
type Item = S::Item;
fn drain_ready(
&mut self,
visitor: &mut dyn FnMut(&Self::Item) -> ControlFlow<()>,
) -> DrainState {
self.drain_each(|item| visitor(item))
}
}
pub struct RingSource<const SLOTS: usize, const SLOT: usize> {
arena: [[u8; SLOT]; SLOTS],
lengths: [usize; SLOTS],
read: usize,
write: usize,
count: usize,
closed: bool,
#[cfg(feature = "io-async")]
frame_offset: usize,
}
impl<const SLOTS: usize, const SLOT: usize> Default for RingSource<SLOTS, SLOT> {
fn default() -> Self {
Self::new()
}
}
impl<const SLOTS: usize, const SLOT: usize> RingSource<SLOTS, SLOT> {
#[must_use]
pub const fn new() -> Self {
Self {
arena: [[0u8; SLOT]; SLOTS],
lengths: [0usize; SLOTS],
read: 0,
write: 0,
count: 0,
closed: false,
#[cfg(feature = "io-async")]
frame_offset: 0,
}
}
pub fn push(&mut self, frame: &[u8]) -> bool {
if self.count == SLOTS || frame.len() > SLOT {
return false;
}
let slot = self.write % SLOTS;
self.arena[slot][..frame.len()].copy_from_slice(frame);
self.lengths[slot] = frame.len();
self.write = self.write.wrapping_add(1);
self.count += 1;
true
}
pub fn close(&mut self) {
self.closed = true;
}
}
impl<const SLOTS: usize, const SLOT: usize> DrainSource for RingSource<SLOTS, SLOT> {
type Item = [u8];
fn drain_ready(&mut self, visitor: &mut dyn FnMut(&[u8]) -> ControlFlow<()>) -> DrainState {
while self.count > 0 {
let slot = self.read % SLOTS;
let view: &[u8] = &self.arena[slot][..self.lengths[slot]];
self.read = self.read.wrapping_add(1);
self.count -= 1;
if visitor(view).is_break() {
return DrainState::More;
}
}
if self.closed {
DrainState::Drained
} else {
DrainState::More
}
}
}
#[cfg(feature = "io-async")]
impl<const SLOTS: usize, const SLOT: usize> proxima_core::io::AsyncRead
for RingSource<SLOTS, SLOT>
{
type Error = core::convert::Infallible;
fn poll_read(
self: core::pin::Pin<&mut Self>,
_cx: &mut core::task::Context<'_>,
buf: &mut [u8],
) -> core::task::Poll<Result<usize, Self::Error>> {
let this = self.get_mut();
if this.count == 0 {
return if this.closed {
core::task::Poll::Ready(Ok(0))
} else {
core::task::Poll::Pending
};
}
let slot = this.read % SLOTS;
let frame = &this.arena[slot][this.frame_offset..this.lengths[slot]];
let take = frame.len().min(buf.len());
buf[..take].copy_from_slice(&frame[..take]);
this.frame_offset += take;
if this.frame_offset == this.lengths[slot] {
this.frame_offset = 0;
this.read = this.read.wrapping_add(1);
this.count -= 1;
}
core::task::Poll::Ready(Ok(take))
}
}
#[cfg(all(feature = "io-async", feature = "std"))]
impl<const SLOTS: usize, const SLOT: usize> futures::io::AsyncRead for RingSource<SLOTS, SLOT> {
fn poll_read(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
buf: &mut [u8],
) -> core::task::Poll<std::io::Result<usize>> {
match <Self as proxima_core::io::AsyncRead>::poll_read(self, cx, buf) {
core::task::Poll::Ready(Ok(count)) => core::task::Poll::Ready(Ok(count)),
core::task::Poll::Ready(Err(never)) => match never {},
core::task::Poll::Pending => core::task::Poll::Pending,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn ring<const SLOTS: usize, const SLOT: usize>(frames: &[&[u8]]) -> RingSource<SLOTS, SLOT> {
let mut source = RingSource::new();
for frame in frames {
assert!(source.push(frame), "test ring overflow");
}
source.close();
source
}
#[test]
fn merges_two_rings_round_robin_zero_copy() {
let core0 = ring::<4, 8>(&[&[0x0a, 0x01, 0x41], &[0x0a, 0x01, 0x42]]);
let core1 = ring::<4, 8>(&[&[0x0a, 0x01, 0x43]]);
let mut fan = DrainFanIn::new([core0, core1]);
let mut got = [0u8; 8];
let mut count = 0;
let state = fan.drain_each(|frame: &[u8]| {
got[count] = frame[2];
count += 1;
ControlFlow::Continue(())
});
assert_eq!(state, DrainState::Drained);
assert_eq!(&got[..count], b"ABC");
}
#[test]
fn visitor_break_is_backpressure_not_drain() {
let core0 = ring::<4, 8>(&[&[1], &[2]]);
let core1 = ring::<4, 8>(&[&[3]]);
let mut fan = DrainFanIn::new([core0, core1]);
let mut seen = 0;
let state = fan.drain_each(|_frame: &[u8]| {
seen += 1;
ControlFlow::Break(())
});
assert_eq!(state, DrainState::More, "break = source not exhausted");
assert_eq!(seen, 1, "stopped after the first item");
assert_eq!(fan.live_count(), 2, "both sources still live");
}
#[test]
fn open_empty_ring_is_more_closed_empty_is_drained() {
let mut open: RingSource<2, 4> = RingSource::new();
assert_eq!(
open.drain_ready(&mut |_| ControlFlow::Continue(())),
DrainState::More,
"open empty ring may get more"
);
open.close();
assert_eq!(
open.drain_ready(&mut |_| ControlFlow::Continue(())),
DrainState::Drained,
"closed empty ring is drained"
);
}
#[test]
fn nested_merge_composes_without_new_types() {
let level_a = DrainFanIn::new([ring::<2, 4>(&[&[1]]), ring::<2, 4>(&[&[2]])]);
let level_b = DrainFanIn::new([ring::<2, 4>(&[&[3]]), ring::<2, 4>(&[&[4]])]);
let mut root = DrainFanIn::new([level_a, level_b]);
let mut count = 0;
root.drain_each(|_| {
count += 1;
ControlFlow::Continue(())
});
assert_eq!(count, 4, "all four leaf frames reach the root visitor");
}
#[cfg(feature = "io-async")]
#[test]
fn ring_source_async_read_reassembles_a_captured_http_request() {
use core::pin::Pin;
use core::task::{Context, Poll, Waker};
use proxima_core::io::AsyncRead as _;
const REQUEST_LINE: &[u8] = b"GET /orders?id=42 HTTP/1.1\r\n";
const HOST_HEADER: &[u8] = b"Host: api.example.internal\r\n";
let mut source: RingSource<4, 64> = RingSource::new();
assert!(source.push(REQUEST_LINE));
assert!(source.push(HOST_HEADER));
source.close();
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut collected = [0u8; 128];
let mut total = 0;
loop {
let mut chunk = [0u8; 5];
match Pin::new(&mut source).poll_read(&mut cx, &mut chunk) {
Poll::Ready(Ok(0)) => break,
Poll::Ready(Ok(count)) => {
collected[total..total + count].copy_from_slice(&chunk[..count]);
total += count;
}
Poll::Ready(Err(never)) => match never {},
Poll::Pending => panic!("closed ring with ready frames must not report Pending"),
}
}
let mut expected = [0u8; 128];
let mut expected_len = 0;
expected[..REQUEST_LINE.len()].copy_from_slice(REQUEST_LINE);
expected_len += REQUEST_LINE.len();
expected[expected_len..expected_len + HOST_HEADER.len()].copy_from_slice(HOST_HEADER);
expected_len += HOST_HEADER.len();
assert_eq!(&collected[..total], &expected[..expected_len]);
}
}