use core::future::Future;
use core::pin::Pin;
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use core::task::{Context, Poll};
use proxima_core::markers::DropSafe;
use crate::pipe::fan_in_sized::FAN_IN_SOURCE_CAP;
use crate::pipe::primitives::{Pipe, UnpinPipe, UnpinSendPipe};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("source exhausted: will never produce again")]
pub struct Exhausted;
pub trait FanInStrategy {
fn index(&self, step: usize, start: usize, n: usize) -> usize;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Select {
RoundRobin,
Fifo,
Lifo,
}
impl FanInStrategy for Select {
fn index(&self, step: usize, start: usize, n: usize) -> usize {
match self {
Select::RoundRobin => (start + step) % n,
Select::Fifo => step,
Select::Lifo => n - 1 - step,
}
}
}
pub trait FanInCompletion {
fn satisfied(&self, retired: usize, n: usize) -> bool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quorum(pub usize);
impl FanInCompletion for Quorum {
fn satisfied(&self, retired: usize, _n: usize) -> bool {
retired >= self.0
}
}
pub struct FanIn<S, Strategy, const N: usize, Completion = Quorum> {
sources: [S; N],
live: [AtomicBool; N],
remaining: AtomicUsize,
cursor: AtomicUsize,
strategy: Strategy,
completion: Completion,
}
impl<S, Strategy, const N: usize> FanIn<S, Strategy, N, Quorum> {
#[must_use]
pub fn new(sources: [S; N], strategy: Strategy) -> Self {
Self::with_completion(sources, strategy, Quorum(N))
}
}
impl<S, Strategy, const N: usize, Completion> FanIn<S, Strategy, N, Completion> {
#[must_use]
pub fn with_completion(sources: [S; N], strategy: Strategy, completion: Completion) -> Self {
Self {
sources,
live: core::array::from_fn(|_| AtomicBool::new(true)),
remaining: AtomicUsize::new(N),
cursor: AtomicUsize::new(0),
strategy,
completion,
}
}
#[must_use]
pub fn strategy(&self) -> &Strategy {
&self.strategy
}
#[must_use]
pub fn completion(&self) -> &Completion {
&self.completion
}
#[must_use]
pub fn live_count(&self) -> usize {
self.remaining.load(Ordering::Relaxed)
}
}
struct FanInCall<'fan, S, Strategy, const N: usize, Completion = Quorum> {
fan: &'fan FanIn<S, Strategy, N, Completion>,
}
impl<S, Strategy, const N: usize, Completion> Future for FanInCall<'_, S, Strategy, N, Completion>
where
S: UnpinPipe<In = (), Err = Exhausted>,
Strategy: FanInStrategy,
Completion: FanInCompletion,
{
type Output = Result<S::Out, Exhausted>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let fan = self.fan;
let retired = N - fan.remaining.load(Ordering::Relaxed);
if fan.completion.satisfied(retired, N) {
return Poll::Ready(Err(Exhausted));
}
let cursor = fan.cursor.load(Ordering::Relaxed);
for step in 0..N {
let index = fan.strategy.index(step, cursor, N);
if !fan.live[index].load(Ordering::Relaxed) {
continue;
}
let mut call = fan.sources[index].call(());
match Pin::new(&mut call).poll(cx) {
Poll::Ready(Ok(item)) => {
fan.cursor.store((index + 1) % N, Ordering::Relaxed);
return Poll::Ready(Ok(item));
}
Poll::Ready(Err(Exhausted)) => {
fan.live[index].store(false, Ordering::Relaxed);
let remaining = fan.remaining.fetch_sub(1, Ordering::Relaxed) - 1;
let retired = N - remaining;
if fan.completion.satisfied(retired, N) {
return Poll::Ready(Err(Exhausted));
}
}
Poll::Pending => {}
}
}
Poll::Pending
}
}
struct FanInSendCall<'fan, S, Strategy, const N: usize, Completion = Quorum> {
fan: &'fan FanIn<S, Strategy, N, Completion>,
}
impl<S, Strategy, const N: usize, Completion> Future
for FanInSendCall<'_, S, Strategy, N, Completion>
where
S: UnpinSendPipe<In = (), Err = Exhausted>,
Strategy: FanInStrategy,
Completion: FanInCompletion,
{
type Output = Result<S::Out, Exhausted>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let fan = self.fan;
let retired = N - fan.remaining.load(Ordering::Relaxed);
if fan.completion.satisfied(retired, N) {
return Poll::Ready(Err(Exhausted));
}
let cursor = fan.cursor.load(Ordering::Relaxed);
for step in 0..N {
let index = fan.strategy.index(step, cursor, N);
if !fan.live[index].load(Ordering::Relaxed) {
continue;
}
let mut call = UnpinSendPipe::call(&fan.sources[index], ());
match Pin::new(&mut call).poll(cx) {
Poll::Ready(Ok(item)) => {
fan.cursor.store((index + 1) % N, Ordering::Relaxed);
return Poll::Ready(Ok(item));
}
Poll::Ready(Err(Exhausted)) => {
fan.live[index].store(false, Ordering::Relaxed);
let remaining = fan.remaining.fetch_sub(1, Ordering::Relaxed) - 1;
let retired = N - remaining;
if fan.completion.satisfied(retired, N) {
return Poll::Ready(Err(Exhausted));
}
}
Poll::Pending => {}
}
}
Poll::Pending
}
}
impl<S, Strategy, const N: usize, Completion> Pipe for FanIn<S, Strategy, N, Completion>
where
S: UnpinPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
Completion: FanInCompletion,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> {
FanInCall { fan: self }
}
}
impl<S, Strategy, const N: usize, Completion> UnpinPipe for FanIn<S, Strategy, N, Completion>
where
S: UnpinPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
Completion: FanInCompletion,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Unpin {
FanInCall { fan: self }
}
}
impl<S, Strategy, const N: usize, Completion> UnpinSendPipe for FanIn<S, Strategy, N, Completion>
where
S: UnpinSendPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy + Send + Sync + 'static,
Completion: FanInCompletion + Send + Sync + 'static,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Send + Unpin {
FanInSendCall { fan: self }
}
}
impl<S: DropSafe, Strategy, const N: usize, Completion> DropSafe
for FanIn<S, Strategy, N, Completion>
{
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("source count {attempted} exceeds fan-in capacity {capacity}")]
pub struct CapacityExceeded {
pub attempted: usize,
pub capacity: usize,
}
pub struct FanInVec<S, Strategy> {
sources: heapless::Vec<S, FAN_IN_SOURCE_CAP>,
live: heapless::Vec<AtomicBool, FAN_IN_SOURCE_CAP>,
remaining: AtomicUsize,
cursor: AtomicUsize,
strategy: Strategy,
}
impl<S, Strategy> FanInVec<S, Strategy> {
pub fn new<I>(sources: I, strategy: Strategy) -> Result<Self, CapacityExceeded>
where
I: IntoIterator<Item = S>,
I::IntoIter: ExactSizeIterator,
{
let iter = sources.into_iter();
let attempted = iter.len();
if attempted > FAN_IN_SOURCE_CAP {
return Err(CapacityExceeded {
attempted,
capacity: FAN_IN_SOURCE_CAP,
});
}
let mut backing: heapless::Vec<S, FAN_IN_SOURCE_CAP> = heapless::Vec::new();
let mut live: heapless::Vec<AtomicBool, FAN_IN_SOURCE_CAP> = heapless::Vec::new();
for source in iter {
backing.push(source).map_err(|_| CapacityExceeded {
attempted,
capacity: FAN_IN_SOURCE_CAP,
})?;
live.push(AtomicBool::new(true))
.map_err(|_| CapacityExceeded {
attempted,
capacity: FAN_IN_SOURCE_CAP,
})?;
}
let count = backing.len();
Ok(Self {
sources: backing,
live,
remaining: AtomicUsize::new(count),
cursor: AtomicUsize::new(0),
strategy,
})
}
#[must_use]
pub fn strategy(&self) -> &Strategy {
&self.strategy
}
#[must_use]
pub fn live_count(&self) -> usize {
self.remaining.load(Ordering::Relaxed)
}
}
struct FanInVecCall<'fan, S, Strategy> {
fan: &'fan FanInVec<S, Strategy>,
}
impl<S, Strategy> Future for FanInVecCall<'_, S, Strategy>
where
S: UnpinPipe<In = (), Err = Exhausted>,
Strategy: FanInStrategy,
{
type Output = Result<S::Out, Exhausted>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let fan = self.fan;
if fan.remaining.load(Ordering::Relaxed) == 0 {
return Poll::Ready(Err(Exhausted));
}
let n = fan.sources.len();
let cursor = fan.cursor.load(Ordering::Relaxed);
for step in 0..n {
let index = fan.strategy.index(step, cursor, n);
if !fan.live[index].load(Ordering::Relaxed) {
continue;
}
let mut call = fan.sources[index].call(());
match Pin::new(&mut call).poll(cx) {
Poll::Ready(Ok(item)) => {
fan.cursor.store((index + 1) % n, Ordering::Relaxed);
return Poll::Ready(Ok(item));
}
Poll::Ready(Err(Exhausted)) => {
fan.live[index].store(false, Ordering::Relaxed);
let remaining = fan.remaining.fetch_sub(1, Ordering::Relaxed) - 1;
if remaining == 0 {
return Poll::Ready(Err(Exhausted));
}
}
Poll::Pending => {}
}
}
Poll::Pending
}
}
struct FanInVecSendCall<'fan, S, Strategy> {
fan: &'fan FanInVec<S, Strategy>,
}
impl<S, Strategy> Future for FanInVecSendCall<'_, S, Strategy>
where
S: UnpinSendPipe<In = (), Err = Exhausted>,
Strategy: FanInStrategy,
{
type Output = Result<S::Out, Exhausted>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let fan = self.fan;
if fan.remaining.load(Ordering::Relaxed) == 0 {
return Poll::Ready(Err(Exhausted));
}
let n = fan.sources.len();
let cursor = fan.cursor.load(Ordering::Relaxed);
for step in 0..n {
let index = fan.strategy.index(step, cursor, n);
if !fan.live[index].load(Ordering::Relaxed) {
continue;
}
let mut call = UnpinSendPipe::call(&fan.sources[index], ());
match Pin::new(&mut call).poll(cx) {
Poll::Ready(Ok(item)) => {
fan.cursor.store((index + 1) % n, Ordering::Relaxed);
return Poll::Ready(Ok(item));
}
Poll::Ready(Err(Exhausted)) => {
fan.live[index].store(false, Ordering::Relaxed);
let remaining = fan.remaining.fetch_sub(1, Ordering::Relaxed) - 1;
if remaining == 0 {
return Poll::Ready(Err(Exhausted));
}
}
Poll::Pending => {}
}
}
Poll::Pending
}
}
impl<S, Strategy> Pipe for FanInVec<S, Strategy>
where
S: UnpinPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> {
FanInVecCall { fan: self }
}
}
impl<S, Strategy> UnpinPipe for FanInVec<S, Strategy>
where
S: UnpinPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Unpin {
FanInVecCall { fan: self }
}
}
impl<S, Strategy> UnpinSendPipe for FanInVec<S, Strategy>
where
S: UnpinSendPipe<In = (), Err = Exhausted> + DropSafe,
Strategy: FanInStrategy + Send + Sync + 'static,
{
type In = ();
type Out = S::Out;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Send + Unpin {
FanInVecSendCall { fan: self }
}
}
impl<S: DropSafe, Strategy> DropSafe for FanInVec<S, Strategy> {}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use core::task::Waker;
#[derive(Clone, Copy)]
enum Step {
Yield(u32),
Pend,
Done,
}
struct Script<const M: usize> {
steps: [Step; M],
pos: AtomicUsize,
}
impl<const M: usize> Script<M> {
fn new(steps: [Step; M]) -> Self {
Self {
steps,
pos: AtomicUsize::new(0),
}
}
}
impl<const M: usize> DropSafe for Script<M> {}
struct ScriptCall(Poll<Result<u32, Exhausted>>);
impl Future for ScriptCall {
type Output = Result<u32, Exhausted>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
self.0
}
}
impl<const M: usize> UnpinPipe for Script<M> {
type In = ();
type Out = u32;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<u32, Exhausted>> + Unpin {
let pos = self.pos.load(Ordering::Relaxed);
if pos >= M {
return ScriptCall(Poll::Ready(Err(Exhausted)));
}
let step = self.steps[pos];
self.pos.store(pos + 1, Ordering::Relaxed);
match step {
Step::Yield(value) => ScriptCall(Poll::Ready(Ok(value))),
Step::Pend => ScriptCall(Poll::Pending),
Step::Done => ScriptCall(Poll::Ready(Err(Exhausted))),
}
}
}
impl<const M: usize> UnpinSendPipe for Script<M> {
type In = ();
type Out = u32;
type Err = Exhausted;
fn call(&self, (): ()) -> impl Future<Output = Result<u32, Exhausted>> + Send + Unpin {
let pos = self.pos.load(Ordering::Relaxed);
if pos >= M {
return ScriptCall(Poll::Ready(Err(Exhausted)));
}
let step = self.steps[pos];
self.pos.store(pos + 1, Ordering::Relaxed);
match step {
Step::Yield(value) => ScriptCall(Poll::Ready(Ok(value))),
Step::Pend => ScriptCall(Poll::Pending),
Step::Done => ScriptCall(Poll::Ready(Err(Exhausted))),
}
}
}
fn drain<S, Strategy, const N: usize, Completion>(
fan: FanIn<S, Strategy, N, Completion>,
out: &mut [u32],
) -> usize
where
S: UnpinPipe<In = (), Out = u32, Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
Completion: FanInCompletion,
{
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut count = 0;
for _ in 0..10_000 {
let mut call = Pipe::call(&fan, ());
match Pin::new(&mut call).poll(&mut cx) {
Poll::Ready(Ok(value)) => {
out[count] = value;
count += 1;
}
Poll::Ready(Err(Exhausted)) => break,
Poll::Pending => {}
}
}
count
}
#[test]
fn merges_all_sources_in_round_robin_order() {
let fan = FanIn::new(
[
Script::new([Step::Yield(0), Step::Yield(1), Step::Done]),
Script::new([Step::Yield(10), Step::Yield(11), Step::Done]),
Script::new([Step::Yield(20), Step::Yield(21), Step::Done]),
],
Select::RoundRobin,
);
let mut buf = [0u32; 16];
let count = drain(fan, &mut buf);
assert_eq!(
&buf[..count],
&[0, 10, 20, 1, 11, 21],
"round-robin fairness"
);
}
#[test]
fn drained_source_is_skipped() {
let fan = FanIn::new(
[
Script::new([Step::Done, Step::Done, Step::Done]),
Script::new([Step::Yield(1), Step::Yield(2), Step::Done]),
Script::new([Step::Yield(3), Step::Done, Step::Done]),
],
Select::RoundRobin,
);
let mut buf = [0u32; 16];
let count = drain(fan, &mut buf);
let got = &mut buf[..count];
got.sort_unstable();
assert_eq!(
got,
&[1, 2, 3],
"items from live sources, drained one skipped"
);
}
struct StickyThen(usize);
impl FanInStrategy for StickyThen {
fn index(&self, step: usize, start: usize, n: usize) -> usize {
if step == 0 {
self.0 % n
} else {
(start + step) % n
}
}
}
#[test]
fn a_caller_defined_strategy_drives_the_merge() {
let fan = FanIn::new(
[
Script::new([Step::Yield(0), Step::Done]),
Script::new([Step::Yield(10), Step::Done]),
Script::new([Step::Yield(20), Step::Done]),
],
StickyThen(2),
);
let mut buf = [0u32; 8];
let count = drain(fan, &mut buf);
assert_eq!(count, 3, "every source still drains");
assert_eq!(
buf[0], 20,
"the caller's own strategy picked source #2 first"
);
}
#[test]
fn select_decides_which_ready_source_wins() {
fn drain_with(select: Select) -> [u32; 3] {
let fan = FanIn::new(
[
Script::new([Step::Yield(0), Step::Done]),
Script::new([Step::Yield(10), Step::Done]),
Script::new([Step::Yield(20), Step::Done]),
],
select,
);
let mut buf = [0u32; 8];
let count = drain(fan, &mut buf);
assert_eq!(count, 3, "every source yields exactly one item");
[buf[0], buf[1], buf[2]]
}
assert_eq!(
drain_with(Select::Fifo),
[0, 10, 20],
"earliest source first"
);
assert_eq!(drain_with(Select::Lifo), [20, 10, 0], "latest source first");
assert_eq!(
drain_with(Select::RoundRobin),
[0, 10, 20],
"fair: the cursor steps past whoever just emitted"
);
}
#[test]
fn all_done_terminates_immediately() {
let fan = FanIn::new(
[Script::new([Step::Done]), Script::new([Step::Done])],
Select::RoundRobin,
);
let mut buf = [0u32; 4];
assert_eq!(drain(fan, &mut buf), 0);
}
#[test]
fn pending_source_is_not_drained() {
let fan = FanIn::new(
[Script::new([Step::Pend, Step::Yield(7), Step::Done])],
Select::RoundRobin,
);
let mut buf = [0u32; 4];
let count = drain(fan, &mut buf);
assert_eq!(
&buf[..count],
&[7],
"Pending re-polled, not treated as drained"
);
}
#[test]
fn live_count_tracks_draining() {
let fan = FanIn::new(
[Script::new([Step::Yield(1)]), Script::new([Step::Yield(2)])],
Select::RoundRobin,
);
assert_eq!(fan.live_count(), 2);
}
#[test]
fn fan_in_nests_inside_a_bigger_fan_in() {
let inner_a = FanIn::new(
[Script::new([Step::Yield(1), Step::Done])],
Select::RoundRobin,
);
let inner_b = FanIn::new(
[Script::new([Step::Yield(2), Step::Done])],
Select::RoundRobin,
);
let outer = FanIn::new([inner_a, inner_b], Select::RoundRobin);
let mut buf = [0u32; 4];
let count = drain(outer, &mut buf);
let got = &mut buf[..count];
got.sort_unstable();
assert_eq!(
got,
&[1, 2],
"both nested fan-ins drain through the outer merge"
);
}
#[test]
fn quorum_completion_ends_the_merge_before_every_source_retires() {
let fan = FanIn::with_completion(
[
Script::new([Step::Yield(1), Step::Done]),
Script::new([Step::Yield(2), Step::Done]),
Script::new([Step::Pend, Step::Pend]),
],
Select::RoundRobin,
Quorum(2),
);
let mut buf = [0u32; 8];
let count = drain(fan, &mut buf);
assert_eq!(
count, 2,
"both live sources yield exactly once before quorum ends the merge"
);
}
#[test]
fn explicit_quorum_at_arity_matches_the_default_for_every_select_order() {
fn drain_default(select: Select) -> [u32; 3] {
let fan = FanIn::new(
[
Script::new([Step::Yield(0), Step::Done]),
Script::new([Step::Yield(10), Step::Done]),
Script::new([Step::Yield(20), Step::Done]),
],
select,
);
let mut buf = [0u32; 8];
let count = drain(fan, &mut buf);
assert_eq!(count, 3, "every source yields exactly one item");
[buf[0], buf[1], buf[2]]
}
fn drain_explicit_quorum_at_arity(select: Select) -> [u32; 3] {
let fan = FanIn::with_completion(
[
Script::new([Step::Yield(0), Step::Done]),
Script::new([Step::Yield(10), Step::Done]),
Script::new([Step::Yield(20), Step::Done]),
],
select,
Quorum(3),
);
let mut buf = [0u32; 8];
let count = drain(fan, &mut buf);
assert_eq!(count, 3, "every source yields exactly one item");
[buf[0], buf[1], buf[2]]
}
for select in [Select::Fifo, Select::Lifo, Select::RoundRobin] {
assert_eq!(
drain_default(select),
drain_explicit_quorum_at_arity(select),
"FanIn::new's default completion is Quorum bound to the arity"
);
}
}
fn drain_send<S, Strategy, const N: usize, Completion>(
fan: FanIn<S, Strategy, N, Completion>,
out: &mut [u32],
) -> usize
where
S: UnpinSendPipe<In = (), Out = u32, Err = Exhausted> + DropSafe,
Strategy: FanInStrategy + Send + Sync + 'static,
Completion: FanInCompletion + Send + Sync + 'static,
{
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut count = 0;
for _ in 0..10_000 {
let mut call = UnpinSendPipe::call(&fan, ());
match Pin::new(&mut call).poll(&mut cx) {
Poll::Ready(Ok(value)) => {
out[count] = value;
count += 1;
}
Poll::Ready(Err(Exhausted)) => break,
Poll::Pending => {}
}
}
count
}
#[test]
fn unpin_send_pipe_merges_all_sources_in_round_robin_order() {
let fan = FanIn::new(
[
Script::new([Step::Yield(0), Step::Yield(1), Step::Done]),
Script::new([Step::Yield(10), Step::Yield(11), Step::Done]),
Script::new([Step::Yield(20), Step::Yield(21), Step::Done]),
],
Select::RoundRobin,
);
let mut buf = [0u32; 16];
let count = drain_send(fan, &mut buf);
assert_eq!(
&buf[..count],
&[0, 10, 20, 1, 11, 21],
"same round-robin fairness as the UnpinPipe tier"
);
}
#[test]
fn unpin_send_pipe_future_is_send_and_unpin() {
fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
let fan = FanIn::new([Script::new([Step::Yield(1), Step::Done])], Select::Fifo);
let call = UnpinSendPipe::call(&fan, ());
needs_send_unpin(&call);
}
fn drain_vec<S, Strategy>(fan: &FanInVec<S, Strategy>, out: &mut [u32]) -> usize
where
S: UnpinPipe<In = (), Out = u32, Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
{
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut count = 0;
for _ in 0..10_000 {
let mut call = Pipe::call(fan, ());
match Pin::new(&mut call).poll(&mut cx) {
Poll::Ready(Ok(value)) => {
out[count] = value;
count += 1;
}
Poll::Ready(Err(Exhausted)) => break,
Poll::Pending => {}
}
}
count
}
fn take_vec<S, Strategy>(fan: &FanInVec<S, Strategy>, out: &mut [u32], target: usize) -> usize
where
S: UnpinPipe<In = (), Out = u32, Err = Exhausted> + DropSafe,
Strategy: FanInStrategy,
{
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut count = 0;
while count < target {
let mut call = Pipe::call(fan, ());
match Pin::new(&mut call).poll(&mut cx) {
Poll::Ready(Ok(value)) => {
out[count] = value;
count += 1;
}
Poll::Ready(Err(Exhausted)) => break,
Poll::Pending => {}
}
}
count
}
#[test]
fn vec_merges_all_sources_in_round_robin_order() {
let fan = FanInVec::new(
[
Script::new([Step::Yield(0), Step::Yield(1), Step::Done]),
Script::new([Step::Yield(10), Step::Yield(11), Step::Done]),
Script::new([Step::Yield(20), Step::Yield(21), Step::Done]),
],
Select::RoundRobin,
)
.expect("3 sources fit the default cap");
let mut buf = [0u32; 16];
let count = drain_vec(&fan, &mut buf);
assert_eq!(
&buf[..count],
&[0, 10, 20, 1, 11, 21],
"round-robin fairness, same as the fixed-arity form"
);
}
#[test]
fn vec_a_hot_source_does_not_starve_the_others() {
let fan = FanInVec::new(
[
Script::new([
Step::Yield(0),
Step::Yield(1),
Step::Yield(2),
Step::Yield(3),
]),
Script::new([Step::Yield(100), Step::Done, Step::Done, Step::Done]),
Script::new([Step::Yield(200), Step::Done, Step::Done, Step::Done]),
],
Select::RoundRobin,
)
.expect("3 sources fit the default cap");
let mut buf = [0u32; 8];
let count = take_vec(&fan, &mut buf, 3);
assert_eq!(
&buf[..count],
&[0, 100, 200],
"every source is visited once before the hot source's second item"
);
}
#[test]
fn vec_drained_source_is_dropped_and_merge_continues() {
let fan = FanInVec::new(
[
Script::new([Step::Done, Step::Done, Step::Done]),
Script::new([Step::Yield(1), Step::Yield(2), Step::Done]),
Script::new([Step::Yield(3), Step::Done, Step::Done]),
],
Select::RoundRobin,
)
.expect("3 sources fit the default cap");
let mut buf = [0u32; 16];
let count = drain_vec(&fan, &mut buf);
let got = &mut buf[..count];
got.sort_unstable();
assert_eq!(
got,
&[1, 2, 3],
"items from live sources, drained one skipped, merge keeps going"
);
}
#[test]
fn vec_all_sources_exhausted_resolves_exhausted() {
let fan = FanInVec::new(
[Script::new([Step::Done]), Script::new([Step::Done])],
Select::RoundRobin,
)
.expect("2 sources fit the default cap");
let mut buf = [0u32; 4];
assert_eq!(
drain_vec(&fan, &mut buf),
0,
"every source starts exhausted, so the merge resolves Exhausted immediately"
);
assert_eq!(fan.live_count(), 0);
}
#[test]
fn vec_exceeding_cap_is_reported_not_truncated() {
let too_many = (0..(FAN_IN_SOURCE_CAP + 1))
.map(|index| Script::new([Step::Yield(index as u32), Step::Done]));
let result = FanInVec::new(too_many, Select::RoundRobin);
let err = match result {
Ok(_) => panic!("CAP + 1 sources must be rejected, not truncated"),
Err(err) => err,
};
assert_eq!(err.attempted, FAN_IN_SOURCE_CAP + 1);
assert_eq!(err.capacity, FAN_IN_SOURCE_CAP);
}
#[test]
fn vec_arity_of_zero_resolves_exhausted_immediately() {
let fan = FanInVec::<Script<1>, Select>::new(core::iter::empty(), Select::RoundRobin)
.expect("zero sources is within capacity");
assert_eq!(fan.live_count(), 0);
let mut buf = [0u32; 1];
assert_eq!(
drain_vec(&fan, &mut buf),
0,
"0 sources => immediately Exhausted"
);
}
#[test]
fn vec_arity_of_one_drains_correctly() {
let fan = FanInVec::new(
[Script::new([Step::Yield(42), Step::Done])],
Select::RoundRobin,
)
.expect("1 source fits the default cap");
let mut buf = [0u32; 4];
let count = drain_vec(&fan, &mut buf);
assert_eq!(&buf[..count], &[42]);
}
#[test]
fn vec_live_count_tracks_draining() {
let fan = FanInVec::new(
[Script::new([Step::Yield(1)]), Script::new([Step::Yield(2)])],
Select::RoundRobin,
)
.expect("2 sources fit the default cap");
assert_eq!(fan.live_count(), 2);
}
fn drain_vec_send<S, Strategy>(fan: &FanInVec<S, Strategy>, out: &mut [u32]) -> usize
where
S: UnpinSendPipe<In = (), Out = u32, Err = Exhausted> + DropSafe,
Strategy: FanInStrategy + Send + Sync + 'static,
{
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut count = 0;
for _ in 0..10_000 {
let mut call = UnpinSendPipe::call(fan, ());
match Pin::new(&mut call).poll(&mut cx) {
Poll::Ready(Ok(value)) => {
out[count] = value;
count += 1;
}
Poll::Ready(Err(Exhausted)) => break,
Poll::Pending => {}
}
}
count
}
#[test]
fn vec_unpin_send_pipe_merges_all_sources_in_round_robin_order() {
let fan = FanInVec::new(
[
Script::new([Step::Yield(0), Step::Yield(1), Step::Done]),
Script::new([Step::Yield(10), Step::Yield(11), Step::Done]),
Script::new([Step::Yield(20), Step::Yield(21), Step::Done]),
],
Select::RoundRobin,
)
.expect("3 sources fit the default cap");
let mut buf = [0u32; 16];
let count = drain_vec_send(&fan, &mut buf);
assert_eq!(
&buf[..count],
&[0, 10, 20, 1, 11, 21],
"same round-robin fairness as the UnpinPipe tier"
);
}
#[test]
fn vec_unpin_send_pipe_future_is_send_and_unpin() {
fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
let fan = FanInVec::new([Script::new([Step::Yield(1), Step::Done])], Select::Fifo)
.expect("1 source fits the default cap");
let call = UnpinSendPipe::call(&fan, ());
needs_send_unpin(&call);
}
}