pub mod primitives;
pub use primitives::{TokioJoinSet, TokioMutex, TokioMutexGuard, TokioNotify, TokioSleep};
use std::cell::Cell;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::thread;
use std::time::Instant;
use proxima_core::ProximaError;
use crate::{BackgroundHandle, CoreId, Runtime, SpawnError, SpawnRequest};
thread_local! {
static CURRENT_CORE: Cell<Option<CoreId>> = const { Cell::new(None) };
}
struct CoreSlot {
spawn_tx: flume::Sender<SpawnRequest>,
handle: Option<thread::JoinHandle<()>>,
}
pub struct TokioPerCoreRuntime {
cores: Arc<Vec<CoreSlot>>,
background_pool: Option<Arc<dyn crate::BackgroundPool>>,
host: Option<tokio::runtime::Handle>,
}
impl TokioPerCoreRuntime {
pub fn new(num_cores: usize) -> Result<Self, ProximaError> {
let num_cores = num_cores.max(1);
let physical = core_affinity::get_core_ids().unwrap_or_default();
let (ready_tx, ready_rx) = flume::unbounded::<Result<(), String>>();
let mut cores: Vec<CoreSlot> = Vec::with_capacity(num_cores);
for index in 0..num_cores {
let core_id = CoreId(index);
let affinity = physical.get(index).copied();
let (spawn_tx, spawn_rx) = flume::unbounded();
let ready = ready_tx.clone();
let handle = thread::Builder::new()
.name(format!("proxima-core-{index}"))
.spawn(move || worker(core_id, affinity, spawn_rx, &ready))
.map_err(|err| {
ProximaError::Config(format!("spawn per-core worker thread: {err}"))
})?;
cores.push(CoreSlot {
spawn_tx,
handle: Some(handle),
});
}
drop(ready_tx);
for _ in 0..num_cores {
match ready_rx.recv() {
Ok(Ok(())) => {}
Ok(Err(reason)) => return Err(ProximaError::Config(reason)),
Err(_) => {
return Err(ProximaError::Config(
"per-core worker died before reporting readiness".into(),
));
}
}
}
Ok(Self {
cores: Arc::new(cores),
background_pool: None,
host: None,
})
}
#[must_use]
pub fn from_handle(handle: tokio::runtime::Handle) -> Self {
Self {
cores: Arc::new(Vec::new()),
background_pool: None,
host: Some(handle),
}
}
#[must_use]
pub fn with_background_pool(mut self, pool: Arc<dyn crate::BackgroundPool>) -> Self {
self.background_pool = Some(pool);
self
}
#[must_use = "block_on returns the future's output or the dispatch error"]
pub fn block_on<F>(&self, future: F) -> Result<F::Output, ProximaError>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
crate::block_on(self, future)
}
}
fn worker(
core_id: CoreId,
affinity: Option<core_affinity::CoreId>,
spawn_rx: flume::Receiver<SpawnRequest>,
ready: &flume::Sender<Result<(), String>>,
) {
if let Some(target) = affinity {
let _ = core_affinity::set_for_current(target);
}
CURRENT_CORE.with(|cell| cell.set(Some(core_id)));
run_event_loop(spawn_rx, ready);
}
async fn drain_loop(spawn_rx: flume::Receiver<SpawnRequest>) {
while let Ok(request) = spawn_rx.recv_async().await {
match request {
SpawnRequest::Send(future) => {
tokio::task::spawn_local(future);
}
SpawnRequest::Factory(factory) => {
let future = factory();
tokio::task::spawn_local(future);
}
SpawnRequest::SendInline(never) => match never {},
SpawnRequest::Shutdown => break,
}
}
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
fn run_event_loop(
spawn_rx: flume::Receiver<SpawnRequest>,
ready: &flume::Sender<Result<(), String>>,
) {
let _ = ready.send(Ok(()));
tokio_uring::start(drain_loop(spawn_rx));
}
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
fn run_event_loop(
spawn_rx: flume::Receiver<SpawnRequest>,
ready: &flume::Sender<Result<(), String>>,
) {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(err) => {
let _ = ready.send(Err(format!("build per-core tokio runtime: {err}")));
return;
}
};
let _ = ready.send(Ok(()));
let local = tokio::task::LocalSet::new();
runtime.block_on(local.run_until(drain_loop(spawn_rx)));
}
impl Runtime for TokioPerCoreRuntime {
fn spawn_on_current_core(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) {
if self.host.is_some() {
tokio::task::spawn_local(future);
return;
}
CURRENT_CORE.with(|cell| {
assert!(
cell.get().is_some(),
"spawn_on_current_core: not on a TokioPerCoreRuntime worker thread — \
use spawn_on_core(N, ...) for cross-core dispatch"
);
});
tokio::task::spawn_local(future);
}
fn spawn_on_core(
&self,
core_id: CoreId,
future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
) -> Result<(), SpawnError> {
if let Some(host) = &self.host {
host.spawn(future);
return Ok(());
}
let Some(slot) = self.cores.get(core_id.0) else {
return Err(SpawnError::Disconnected);
};
let request = SpawnRequest::Send(future);
slot.spawn_tx
.send(request)
.map_err(|_| SpawnError::Disconnected)
}
fn spawn_factory_on_core(
&self,
core_id: CoreId,
factory: Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + 'static>> + Send + 'static>,
) -> Result<(), SpawnError> {
if self.host.is_some() {
tokio::task::spawn_local(factory());
return Ok(());
}
let Some(slot) = self.cores.get(core_id.0) else {
return Err(SpawnError::Disconnected);
};
let request = SpawnRequest::Factory(factory);
slot.spawn_tx
.send(request)
.map_err(|_| SpawnError::Disconnected)
}
fn spawn_background_blocking(
&self,
work: Box<dyn FnOnce() -> Result<Box<dyn std::any::Any + Send>, ProximaError> + Send>,
) -> BackgroundHandle<Box<dyn std::any::Any + Send>> {
if let Some(pool) = &self.background_pool {
return pool.spawn(work);
}
let join = match &self.host {
Some(host) => host.spawn_blocking(work),
None => tokio::task::spawn_blocking(work),
};
Box::pin(async move {
match join.await {
Ok(Ok(value)) => Ok(value),
Ok(Err(err)) => Err(err),
Err(join_err) => Err(ProximaError::Body(format!(
"background task aborted: {join_err}"
))),
}
})
}
fn timer_at(&self, deadline: Instant) -> Pin<Box<dyn Future<Output = ()> + 'static>> {
let tokio_deadline = tokio::time::Instant::from_std(deadline);
Box::pin(tokio::time::sleep_until(tokio_deadline))
}
fn num_cores(&self) -> usize {
self.cores.len()
}
fn current_core(&self) -> CoreId {
CURRENT_CORE.with(|cell| match cell.get() {
Some(id) => id,
None => panic!("current_core: called from outside a TokioPerCoreRuntime worker thread"),
})
}
}
impl Drop for TokioPerCoreRuntime {
fn drop(&mut self) {
let current_core = CURRENT_CORE.with(|cell| cell.get());
if let Some(cores) = Arc::get_mut(&mut self.cores) {
for slot in cores.iter() {
let _ = slot.spawn_tx.send(SpawnRequest::Shutdown);
}
for (index, slot) in cores.iter_mut().enumerate() {
if let Some(handle) = slot.handle.take() {
if Some(CoreId(index)) == current_core {
std::mem::forget(handle);
} else {
let _ = handle.join();
}
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::sync::mpsc::{Receiver, channel};
use std::time::Duration;
use super::*;
fn awaited<T>(receiver: &Receiver<T>, what: &str) -> T {
receiver
.recv_timeout(Duration::from_secs(10))
.unwrap_or_else(|err| panic!("{what}: {err}"))
}
#[test]
fn new_runtime_spawns_requested_number_of_workers() {
let runtime = TokioPerCoreRuntime::new(2).expect("build runtime");
assert_eq!(runtime.num_cores(), 2);
}
#[test]
fn new_returns_only_once_every_worker_is_live() {
let runtime = TokioPerCoreRuntime::new(4).expect("build runtime");
let (sender, receiver) = channel();
for index in 0..runtime.num_cores() {
let sender = sender.clone();
runtime
.spawn_on_core(
CoreId(index),
Box::pin(async move {
let _ = sender.send(CURRENT_CORE.with(|cell| cell.get()));
}),
)
.expect("spawn on a live core");
}
let mut reported: Vec<CoreId> = (0..4)
.map(|_| awaited(&receiver, "worker report").expect("worker knows its core"))
.collect();
reported.sort_by_key(CoreId::as_usize);
assert_eq!(reported, vec![CoreId(0), CoreId(1), CoreId(2), CoreId(3)]);
}
#[test]
fn from_handle_dispatches_send_work_onto_the_host_with_no_new_threads() {
let host = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_name("host-tokio-worker")
.enable_all()
.build()
.expect("host runtime");
let runtime = TokioPerCoreRuntime::from_handle(host.handle().clone());
let (sender, receiver) = channel();
runtime
.spawn_on_core(
CoreId(0),
Box::pin(async move {
let name = std::thread::current()
.name()
.unwrap_or_default()
.to_string();
let _ = sender.send(name);
}),
)
.expect("spawn onto host runtime");
let thread_name = awaited(&receiver, "host runtime ran the future");
assert!(
thread_name.starts_with("host-tokio-worker"),
"future ran on a host-tokio worker, not a new proxima thread; got {thread_name}"
);
assert!(
runtime.cores.is_empty(),
"from_handle owns no worker threads"
);
}
#[test]
fn spawn_on_core_dispatches_to_target_worker() {
let runtime = TokioPerCoreRuntime::new(2).expect("build runtime");
let (sender, receiver) = channel();
for index in 0..2 {
let sender = sender.clone();
runtime
.spawn_on_core(
CoreId(index),
Box::pin(async move {
let _ = sender.send(index);
}),
)
.expect("spawn on fresh runtime");
}
let mut dispatched = vec![
awaited(&receiver, "first worker"),
awaited(&receiver, "second worker"),
];
dispatched.sort_unstable();
assert_eq!(dispatched, vec![0, 1]);
}
#[test]
fn current_core_inside_worker_returns_dispatched_id() {
let runtime = TokioPerCoreRuntime::new(2).expect("build runtime");
let (sender, receiver) = channel();
runtime
.spawn_on_core(
CoreId(1),
Box::pin(async move {
let _ = sender.send(CURRENT_CORE.with(|cell| cell.get()));
}),
)
.expect("spawn on fresh runtime");
assert_eq!(awaited(&receiver, "core 1 report"), Some(CoreId(1)));
}
}