use std::io;
use crate::feed::MAX_CONCURRENT_SUBSCRIPTIONS;
pub const FEED_EXECUTOR_HEADROOM: usize = 16;
pub const FEED_EXECUTOR_THREADS: usize = MAX_CONCURRENT_SUBSCRIPTIONS + FEED_EXECUTOR_HEADROOM;
const _: () = {
assert!(
FEED_EXECUTOR_HEADROOM > 0,
"zero headroom leaves no thread for a fresh dispatch to run on while a \
finishing pump's thread has not yet returned to the pool"
);
assert!(
FEED_EXECUTOR_THREADS > MAX_CONCURRENT_SUBSCRIPTIONS,
"the executor must exceed the subscription budget, or the last admitted \
subscription has no thread to pump on"
);
};
#[derive(Debug)]
pub struct FeedExecutor {
runtime: Option<tokio::runtime::Runtime>,
}
impl FeedExecutor {
pub fn start_production() -> io::Result<Self> {
Self::start(FEED_EXECUTOR_THREADS)
}
pub fn start(max_blocking: usize) -> io::Result<Self> {
tokio::runtime::Builder::new_current_thread()
.max_blocking_threads(max_blocking)
.thread_name("polychrome-feed-pump")
.enable_all()
.build()
.map(|runtime| Self {
runtime: Some(runtime),
})
}
pub fn spawn_pump(&self, job: impl FnOnce() + Send + 'static) {
self.runtime
.as_ref()
.expect("spawn_pump never runs after drop")
.handle()
.spawn_blocking(job);
}
}
impl Drop for FeedExecutor {
fn drop(&mut self) {
if let Some(runtime) = self.runtime.take() {
runtime.shutdown_background();
}
}
}
#[cfg(test)]
mod tests {
use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use super::FeedExecutor;
#[tokio::test]
async fn dropping_the_executor_inside_another_runtimes_async_task_does_not_panic() {
let executor = FeedExecutor::start(2).expect("start");
drop(executor);
}
#[test]
fn spawned_work_runs_without_a_caller_runtime() {
let executor = FeedExecutor::start(4).expect("start");
let ran = Arc::new(AtomicUsize::new(0));
let done = Arc::clone(&ran);
executor.spawn_pump(move || {
done.fetch_add(1, Ordering::SeqCst);
});
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while ran.load(Ordering::SeqCst) == 0 {
assert!(
std::time::Instant::now() < deadline,
"spawned job never ran"
);
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn many_spawned_jobs_run_concurrently_up_to_the_pool_size() {
let executor = FeedExecutor::start(8).expect("start");
let barrier = Arc::new(std::sync::Barrier::new(8));
let mut handles = Vec::new();
for _ in 0..8 {
let (tx, rx) = std::sync::mpsc::channel();
handles.push(rx);
let barrier = Arc::clone(&barrier);
executor.spawn_pump(move || {
barrier.wait();
let _ = tx.send(());
});
}
let deadline = std::time::Instant::now() + Duration::from_secs(5);
for rx in handles {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
rx.recv_timeout(remaining)
.expect("every job reaches the barrier and completes");
}
}
}