use std::fmt::Display;
use std::future::{Future, Pending, poll_fn};
use std::hint::spin_loop;
use std::io;
use std::ops::Deref;
use std::pin::{Pin, pin};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering, fence};
use std::task::{Context as TaskContext, Poll};
use std::thread;
use std::time::{Duration, Instant};
use futures::Stream;
use futures::task::AtomicWaker;
use rtrb::{Consumer, Producer, RingBuffer};
use thiserror::Error;
use tokio::runtime::{Builder, Runtime};
use tokio::sync::oneshot;
use tokio::task::coop::consume_budget;
use tokio_util::task::TaskTracker;
use tracing::{debug, error};
use super::{Shutdown, Turn, lane_of};
pub(super) const RING: usize = 8;
const SPIN: Duration = Duration::from_micros(10);
const LONGEST_GAP: u32 = 64;
#[derive(Debug, Error)]
#[error("subscription `{subscription}` could not start its dedicated thread {index}")]
pub(crate) struct StartThreadError {
subscription: String,
index: usize,
#[source]
source: io::Error,
}
#[repr(align(128))]
struct Padded<T>(T);
impl<T> Deref for Padded<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
const ANY: usize = usize::MAX;
struct Signals {
parked: Box<[Padded<AtomicBool>]>,
wakers: Box<[Padded<AtomicWaker>]>,
loop_waiting: Padded<AtomicUsize>,
loop_waker: AtomicWaker,
}
#[derive(Clone, Copy)]
enum Which {
One(usize),
Next(usize),
}
enum Stuck {
Gone(usize),
Shutdown,
}
impl Signals {
fn new(count: usize) -> Self {
Self {
parked: (0..count).map(|_| Padded(AtomicBool::new(false))).collect(),
wakers: (0..count).map(|_| Padded(AtomicWaker::new())).collect(),
loop_waiting: Padded(AtomicUsize::new(0)),
loop_waker: AtomicWaker::new(),
}
}
fn freed(&self, index: usize, held: usize) {
fence(Ordering::SeqCst);
let waiting = self.loop_waiting.load(Ordering::Relaxed);
let wakes = match waiting {
0 => false,
ANY => held <= RING / 2,
one => one == index + 1,
};
if wakes
&& self
.loop_waiting
.compare_exchange(waiting, 0, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
self.loop_waker.wake();
}
}
fn pushed(&self, index: usize) {
fence(Ordering::SeqCst);
if self.parked[index].load(Ordering::Relaxed)
&& self.parked[index].swap(false, Ordering::SeqCst)
{
self.wakers[index].wake();
}
}
fn leaving(&self) {
self.loop_waiting.store(0, Ordering::SeqCst);
self.loop_waker.wake();
}
fn poll_room<Item>(
&self,
rings: &[Producer<Item>],
which: Which,
cx: &TaskContext<'_>,
) -> Poll<Result<usize, usize>> {
let usable = |index: usize| {
let ring: &Producer<Item> = &rings[index];
if ring.is_abandoned() {
Some(Err(index))
} else {
(!ring.is_full()).then_some(Ok(index))
}
};
let pick = || match which {
Which::One(index) => usable(index),
Which::Next(start) => {
let count = rings.len();
(0..count).find_map(|offset| usable((start + offset) % count))
}
};
if let Some(found) = pick() {
return Poll::Ready(found);
}
self.loop_waker.register(cx.waker());
let waiting = match which {
Which::One(index) => index + 1,
Which::Next(_) => ANY,
};
self.loop_waiting.store(waiting, Ordering::SeqCst);
fence(Ordering::SeqCst);
if let Some(found) = pick() {
self.loop_waiting.store(0, Ordering::SeqCst);
return Poll::Ready(found);
}
Poll::Pending
}
fn close(&self) {
for waker in &*self.wakers {
waker.wake();
}
}
}
struct Leaving<'a>(&'a Signals);
impl Drop for Leaving<'_> {
fn drop(&mut self) {
self.0.leaving();
}
}
#[allow(clippy::future_not_send)]
async fn work<Item, Handle>(
signals: Arc<Signals>,
index: usize,
ring: Consumer<Item>,
mut handle: Handle,
) where
Handle: AsyncFnMut(Item),
{
let signals = &*signals;
let _leaving = Leaving(signals);
let mut ring = ring;
loop {
if let Ok(item) = ring.pop() {
signals.freed(index, ring.slots());
handle(item).await;
consume_budget().await;
continue;
}
if ring.is_abandoned() {
fence(Ordering::Acquire);
if ring.is_empty() {
break;
}
continue;
}
if arrives(&ring) {
continue;
}
let waiting = ˚
poll_fn(|cx| {
signals.wakers[index].register(cx.waker());
signals.parked[index].store(true, Ordering::SeqCst);
fence(Ordering::SeqCst);
if !waiting.is_empty() || waiting.is_abandoned() {
signals.parked[index].store(false, Ordering::SeqCst);
return Poll::Ready(());
}
Poll::Pending
})
.await;
}
}
fn arrives<Item>(ring: &Consumer<Item>) -> bool {
let start = Instant::now();
let mut spins = 1;
loop {
for _ in 0..spins {
spin_loop();
}
if !ring.is_empty() || ring.is_abandoned() {
return true;
}
if spins < LONGEST_GAP {
spins *= 2;
} else if start.elapsed() >= SPIN {
return false;
}
}
}
struct Member {
done: oneshot::Receiver<()>,
abort: Option<oneshot::Sender<()>>,
}
impl Member {
async fn join(&mut self) {
if (&mut self.done).await.is_err() {
error!(target: "ruststream::dispatch", "a dedicated thread ended abnormally");
}
self.abort = None;
}
}
struct Crew(Vec<Member>);
impl Crew {
async fn join(&mut self) {
for member in &mut self.0 {
member.join().await;
}
}
}
fn start_thread<Work, Fut>(
subscription: &str,
index: usize,
runtime: Runtime,
work: Work,
) -> Result<Member, StartThreadError>
where
Work: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = ()> + 'static,
{
let (done_side, done) = oneshot::channel();
let (abort, mut aborted) = oneshot::channel::<()>();
let name = format!("{}/{index}", subscription.replace('\0', ""));
thread::Builder::new()
.name(name)
.spawn(move || {
runtime.block_on(async move {
let work = work();
tokio::select! {
biased;
_ = &mut aborted => {}
() = work => {}
}
});
drop(runtime);
let _ = done_side.send(());
})
.map_err(|source| StartThreadError {
subscription: subscription.to_owned(),
index,
source,
})?;
Ok(Member {
done,
abort: Some(abort),
})
}
pub(super) struct Threads<Item> {
rings: Vec<Producer<Item>>,
signals: Arc<Signals>,
crew: Crew,
keyed: bool,
}
impl<Item: Send + 'static> Threads<Item> {
pub(super) fn start<Each, Handle>(
subscription: &str,
count: usize,
keyed: bool,
mut each: Each,
) -> Result<Self, StartThreadError>
where
Each: FnMut(TaskTracker) -> Handle,
Handle: AsyncFnMut(Item) + Send + 'static,
{
let signals = Arc::new(Signals::new(count));
let mut rings = Vec::with_capacity(count);
let mut crew = Crew(Vec::with_capacity(count));
for index in 0..count {
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.map_err(|source| StartThreadError {
subscription: subscription.to_owned(),
index,
source,
})?;
let (producer, consumer) = RingBuffer::new(RING);
let signals = Arc::clone(&signals);
let own = TaskTracker::new();
let handle = each(own.clone());
crew.0.push(start_thread(
subscription,
index,
runtime,
move || async move {
work(signals, index, consumer, handle).await;
own.close();
own.wait().await;
},
)?);
rings.push(producer);
}
Ok(Self {
rings,
signals,
crew,
keyed,
})
}
pub(super) async fn feed<Items, Failure>(
mut self,
stream: Items,
name: &str,
shutdown: &Shutdown,
key: fn(&Item) -> Option<&[u8]>,
) where
Items: Stream<Item = Result<Item, Failure>>,
Failure: Display,
{
self.read(stream, name, shutdown, key).await;
self.let_go();
self.crew.join().await;
}
fn let_go(&mut self) {
self.rings.clear();
self.signals.close();
}
async fn read<Items, Failure>(
&mut self,
stream: Items,
name: &str,
shutdown: &Shutdown,
key: fn(&Item) -> Option<&[u8]>,
) where
Items: Stream<Item = Result<Item, Failure>>,
Failure: Display,
{
let count = self.rings.len();
let mut stream = pin!(stream);
let mut cancelled = pin!(shutdown.cancelled());
let mut turn = 0usize;
loop {
if shutdown.is_cancelled() {
break;
}
let picked = if self.keyed {
None
} else {
let room = self.room(Which::Next(turn), Some(cancelled.as_mut())).await;
match room {
Ok(index) => Some(index),
Err(stuck) => {
report(name, &stuck);
break;
}
}
};
let pulled = poll_fn(|cx| match stream.as_mut().poll_next(cx) {
Poll::Ready(Some(item)) => Poll::Ready(Turn::Delivery(item)),
Poll::Ready(None) => Poll::Ready(Turn::Ended),
Poll::Pending => cancelled.as_mut().poll(cx).map(|()| Turn::Shutdown),
})
.await;
match pulled {
Turn::Delivery(Ok(item)) => {
let keyed = if picked.is_some() { None } else { key(&item) };
let index =
picked.unwrap_or_else(|| keyed.map_or(turn, |key| lane_of(key, count)));
if keyed.is_none() {
turn = (index + 1) % count;
}
if picked.is_none() {
if let Err(stuck) = self
.room(Which::One(index), None::<Pin<&mut Pending<()>>>)
.await
{
report(name, &stuck);
break;
}
}
if self.rings[index].push(item).is_err() {
unreachable!("the loop pushes only after it saw room");
}
self.signals.pushed(index);
}
Turn::Delivery(Err(err)) => {
error!(
target: "ruststream::dispatch",
subscription = %name,
error = %err,
"subscriber stream error",
);
}
Turn::Ended => {
debug!(
target: "ruststream::dispatch",
subscription = %name,
"subscriber stream ended",
);
break;
}
Turn::Shutdown => break,
}
}
}
async fn room<Cancelled>(
&mut self,
which: Which,
mut cancelled: Option<Pin<&mut Cancelled>>,
) -> Result<usize, Stuck>
where
Cancelled: Future<Output = ()>,
{
let (rings, signals) = (&mut self.rings, &*self.signals);
poll_fn(move |cx| match signals.poll_room(rings, which, cx) {
Poll::Ready(Ok(index)) => Poll::Ready(Ok(index)),
Poll::Ready(Err(index)) => Poll::Ready(Err(Stuck::Gone(index))),
Poll::Pending => cancelled.as_mut().map_or(Poll::Pending, |cancelled| {
cancelled.as_mut().poll(cx).map(|()| Err(Stuck::Shutdown))
}),
})
.await
}
}
fn report(name: &str, stuck: &Stuck) {
if let Stuck::Gone(thread) = stuck {
error!(
target: "ruststream::dispatch",
subscription = %name,
thread,
"dedicated thread terminated; stopping dispatch",
);
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use std::sync::atomic::{AtomicU32, AtomicUsize};
use futures::StreamExt;
use futures::stream;
use tokio::sync::{Notify, Semaphore};
use super::*;
const READ_AHEAD: usize = 2 * (1 + RING);
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_read_ahead_is_bounded_and_drained_on_shutdown() {
let gate = Arc::new(Semaphore::new(0));
let handled = Arc::new(AtomicUsize::new(0));
let pulled = Arc::new(AtomicUsize::new(0));
let full = Arc::new(Notify::new());
let threads = Threads::start("bounded", 2, false, |_own| {
let (gate, handled) = (Arc::clone(&gate), Arc::clone(&handled));
async move |_item: u32| {
gate.acquire().await.expect("the gate stays open").forget();
handled.fetch_add(1, Ordering::SeqCst);
}
})
.expect("the threads start");
let items = {
let (pulled, full) = (Arc::clone(&pulled), Arc::clone(&full));
stream::iter(0..100u32).map(move |item| {
if pulled.fetch_add(1, Ordering::SeqCst) + 1 == 2 * RING {
full.notify_one();
}
Ok::<_, Infallible>(item)
})
};
let shutdown = Shutdown::new();
let stopping = shutdown.clone();
let reading = tokio::spawn(async move {
let mut threads = threads;
threads.read(items, "bounded", &stopping, |_| None).await;
threads
});
full.notified().await;
shutdown.cancel();
let mut threads = reading.await.expect("the loop stops reading");
threads.let_go();
gate.add_permits(100);
threads.crew.join().await;
let pulled = pulled.load(Ordering::SeqCst);
assert!(pulled <= READ_AHEAD, "read {pulled} ahead of held handlers");
assert_eq!(
handled.load(Ordering::SeqCst),
pulled,
"a delivery read was not handled"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_busy_thread_lets_its_own_tasks_run() {
const ITEMS: u32 = 1000;
let ran = Arc::new(AtomicBool::new(false));
let first_seen = Arc::new(AtomicU32::new(u32::MAX));
let threads = Threads::start("busy", 1, false, |_own| {
let (ran, first_seen) = (Arc::clone(&ran), Arc::clone(&first_seen));
async move |item: u32| {
if item == 0 {
let ran = Arc::clone(&ran);
tokio::spawn(async move { ran.store(true, Ordering::SeqCst) });
} else if ran.load(Ordering::SeqCst) {
let _ = first_seen.compare_exchange(
u32::MAX,
item,
Ordering::SeqCst,
Ordering::SeqCst,
);
}
let until = Instant::now() + Duration::from_micros(50);
while Instant::now() < until {
spin_loop();
}
}
})
.expect("the threads start");
let items = stream::iter(0..ITEMS).map(Ok::<_, Infallible>);
threads
.feed(items, "busy", &Shutdown::new(), |_| None)
.await;
let seen = first_seen.load(Ordering::SeqCst);
assert!(
seen < ITEMS / 2,
"the spawned task ran only at delivery {seen} of {ITEMS}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_thread_that_dies_stops_the_loop() {
for keyed in [false, true] {
let threads = Threads::start("dying", 1, keyed, |_own| {
async move |item: u32| {
assert!(item > 0, "the first delivery takes the thread down");
}
})
.expect("the threads start");
let items = stream::iter(0..1000u32).map(Ok::<_, Infallible>);
let shutdown = Shutdown::new();
let fed = tokio::time::timeout(
Duration::from_secs(10),
threads.feed(items, "dying", &shutdown, |_| Some(b"key".as_slice())),
)
.await;
assert!(
fed.is_ok(),
"the loop waited on a dead thread (keyed: {keyed})"
);
}
}
}