Rename reactor -> driver, prep for lib/reactivity

This commit is contained in:
2026-03-19 19:47:06 -04:00
parent 3fd8209420
commit 7b3c2fcbef
13 changed files with 490 additions and 62 deletions

View File

@@ -1,3 +1,5 @@
//! Public runtime driver primitives.
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::BTreeMap;
@@ -51,23 +53,29 @@ impl NotifierInner {
}
#[derive(Clone)]
/// Cross-thread notifier for a runtime thread's driver.
pub struct ThreadNotifier {
inner: Arc<NotifierInner>,
}
impl ThreadNotifier {
/// Sends a wake notification to the target runtime thread.
pub fn notify(&self) -> io::Result<()> {
self.inner.notify()
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
/// Readiness information returned by [`Driver::poll`].
pub struct ReadyEvents {
/// One or more timer expirations are pending.
pub timer: bool,
/// One or more cross-thread wake notifications are pending.
pub wake: bool,
}
pub struct Reactor {
/// Low-level Linux runtime driver backed by `io_uring`.
pub struct Driver {
ring: IoUring,
notifier: Arc<NotifierInner>,
next_token: Cell<u64>,
@@ -77,11 +85,16 @@ pub struct Reactor {
completions: RefCell<BTreeMap<u64, CompletionHandler>>,
}
pub fn create() -> io::Result<(Reactor, ThreadNotifier)> {
create_reactor()
/// Creates a new driver and its paired [`ThreadNotifier`].
pub fn create() -> io::Result<(Driver, ThreadNotifier)> {
create_driver()
}
pub fn create_reactor() -> io::Result<(Reactor, ThreadNotifier)> {
/// Creates a new driver and its paired [`ThreadNotifier`].
///
/// This is identical to [`create`] and exists as a more explicit name for callers that want to
/// emphasize driver construction.
pub fn create_driver() -> io::Result<(Driver, ThreadNotifier)> {
let ring = IoUring::new(64)?;
let notifier = Arc::new(NotifierInner {
ring_fd: ring.ring_fd(),
@@ -89,7 +102,7 @@ pub fn create_reactor() -> io::Result<(Reactor, ThreadNotifier)> {
});
Ok((
Reactor {
Driver {
ring,
notifier: Arc::clone(&notifier),
next_token: Cell::new(1),
@@ -102,7 +115,7 @@ pub fn create_reactor() -> io::Result<(Reactor, ThreadNotifier)> {
))
}
impl Reactor {
impl Driver {
pub(crate) fn bind_current_thread(&self) {
self.ring.bind_current_thread();
}
@@ -111,6 +124,7 @@ impl Reactor {
self.ring.unbind_current_thread();
}
/// Polls the driver without blocking.
pub fn poll(&self) -> io::Result<Option<ReadyEvents>> {
let mut ready = ReadyEvents::default();
let saw_any = self
@@ -119,10 +133,14 @@ impl Reactor {
if saw_any { Ok(Some(ready)) } else { Ok(None) }
}
/// Blocks until at least one completion is available.
pub fn wait(&self) -> io::Result<()> {
self.ring.wait_for_cqe()
}
/// Updates the currently armed timer deadline.
///
/// Passing `None` removes any active timer.
pub fn rearm_timer(&self, deadline: Option<Duration>) -> io::Result<()> {
match (self.active_timer_token.get(), deadline) {
(Some(active), Some(deadline)) => {
@@ -171,6 +189,7 @@ impl Reactor {
})
}
/// Drains the accumulated wake notification count.
pub fn drain_wake(&self) -> io::Result<u64> {
let wakes = self.pending_wakes.replace(0);
if wakes == 0 {
@@ -183,6 +202,7 @@ impl Reactor {
}
}
/// Drains the accumulated timer-expiration count.
pub fn drain_timer(&self) -> io::Result<u64> {
let timers = self.pending_timers.replace(0);
if timers == 0 {
@@ -234,12 +254,13 @@ impl Reactor {
}
}
impl Drop for Reactor {
impl Drop for Driver {
fn drop(&mut self) {
self.notifier.closed.store(true, Ordering::Release);
}
}
/// Returns the current monotonic time used by the runtime timer system.
pub fn monotonic_now() -> io::Result<Duration> {
let mut now = std::mem::MaybeUninit::<libc::timespec>::uninit();
let result = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()) };
@@ -268,16 +289,16 @@ fn decode_token_kind(token: u64) -> Option<CompletionKind> {
#[cfg(test)]
mod tests {
use super::{create_reactor, monotonic_now};
use super::{create_driver, monotonic_now};
use std::thread;
use std::time::Duration;
#[test]
fn notifier_wakes_target_ring() {
let (sender, _) = create_reactor().expect("sender reactor should initialize");
let (sender, _) = create_driver().expect("sender driver should initialize");
sender.bind_current_thread();
let (target, notifier) = create_reactor().expect("target reactor should initialize");
let (target, notifier) = create_driver().expect("target driver should initialize");
notifier.notify().expect("notify should succeed");
let ready = loop {
@@ -295,7 +316,7 @@ mod tests {
#[test]
fn notifier_wakes_target_ring_from_plain_thread() {
let (target, notifier) = create_reactor().expect("target reactor should initialize");
let (target, notifier) = create_driver().expect("target driver should initialize");
thread::spawn(move || {
notifier.notify().expect("notify should succeed");
@@ -317,14 +338,14 @@ mod tests {
#[test]
fn timeout_reports_deadlines() {
let (reactor, _notifier) = create_reactor().expect("reactor should initialize");
let (driver, _notifier) = create_driver().expect("driver should initialize");
let deadline = monotonic_now().expect("clock should work") + Duration::from_millis(20);
reactor
driver
.rearm_timer(Some(deadline))
.expect("timer should arm");
let ready = loop {
if let Some(ready) = reactor.poll().expect("poll should succeed") {
if let Some(ready) = driver.poll().expect("poll should succeed") {
break ready;
}
thread::sleep(Duration::from_millis(5));
@@ -332,9 +353,6 @@ mod tests {
assert!(ready.timer);
assert!(!ready.wake);
assert_eq!(
reactor.drain_timer().expect("timer drain should succeed"),
1
);
assert_eq!(driver.drain_timer().expect("timer drain should succeed"), 1);
}
}

View File

@@ -1,4 +1,4 @@
pub mod driver;
pub mod mesh_alloc;
pub mod reactor;
pub mod runtime;
pub(crate) mod uring;

View File

@@ -1,3 +1,5 @@
//! Public runtime loop and worker-thread primitives.
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, VecDeque};
use std::future::Future;
@@ -9,7 +11,7 @@ use std::sync::{Arc, Mutex, MutexGuard};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::Duration;
use super::reactor::{Reactor, ThreadNotifier, create as create_reactor, monotonic_now};
use super::driver::{Driver, ThreadNotifier, create_driver, monotonic_now};
type LocalTask = Box<dyn FnOnce() + 'static>;
type SendTask = Box<dyn FnOnce() + Send + 'static>;
@@ -19,16 +21,19 @@ type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
static mut CURRENT_THREAD: *mut ThreadState = ptr::null_mut();
#[derive(Clone)]
/// Handle for queueing work onto a specific runtime thread.
pub struct ThreadHandle {
shared: Arc<ThreadShared>,
}
/// Handle for a worker runtime thread spawned with [`spawn_worker`].
pub struct WorkerHandle {
thread: ThreadHandle,
completion: Arc<WorkerCompletion>,
}
#[derive(Clone)]
/// Handle returned by [`set_timeout`].
pub struct TimeoutHandle {
id: usize,
owner: *const ThreadState,
@@ -36,20 +41,37 @@ pub struct TimeoutHandle {
}
#[derive(Clone)]
/// Handle returned by [`set_interval`].
pub struct IntervalHandle {
id: usize,
owner: *const ThreadState,
_local: Rc<()>,
}
/// Handle returned by [`queue_future`].
///
/// Awaiting a join handle yields the output of the queued future.
pub struct JoinHandle<T> {
state: Rc<JoinState<T>>,
}
/// Future returned by [`yield_now`].
///
/// Awaiting this future will immediately yield control back to the runtime scheduler, allowing other queued microtasks
/// to run before the current task continues executing. Note that continuation of futures runs as a microtask, so this
/// can only yield to other microtasks and not to macrotasks (driver events such as file or network I/O, timers, or
/// channel messages).
pub struct YieldNow {
yielded: bool,
}
/// Returns a handle for the current runtime thread.
///
/// If the current thread has not yet entered the runtime, the runtime state is initialized lazily.
///
/// # Panics
///
/// Panics if the runtime cannot initialize its driver for the current thread.
pub fn current_thread_handle() -> ThreadHandle {
current_thread().handle()
}
@@ -58,10 +80,17 @@ pub(crate) fn try_current_thread_handle() -> Option<ThreadHandle> {
unsafe { (!CURRENT_THREAD.is_null()).then(|| (*CURRENT_THREAD).handle()) }
}
pub(crate) fn with_current_reactor<T>(f: impl FnOnce(&Reactor) -> T) -> T {
f(&current_thread().reactor)
pub(crate) fn with_current_driver<T>(f: impl FnOnce(&Driver) -> T) -> T {
f(&current_thread().driver)
}
/// Queues a macrotask on the current runtime thread.
///
/// The task runs after all currently-queued macrotasks, and after all microtasks.
///
/// # Panics
///
/// Panics if the runtime cannot initialize its state for the current thread.
pub fn queue_task<F>(task: F)
where
F: FnOnce() + 'static,
@@ -69,6 +98,14 @@ where
push_local_macrotask(Box::new(task));
}
/// Queues a microtask on the current runtime thread.
///
/// Microtasks run before the next macrotask turn, mirroring JavaScript-style event loop
/// semantics.
///
/// # Panics
///
/// Panics if the runtime cannot initialize its state for the current thread.
pub fn queue_microtask<F>(task: F)
where
F: FnOnce() + 'static,
@@ -79,6 +116,11 @@ where
.push_back(Box::new(task));
}
/// Schedules a one-shot timer on the current runtime thread.
///
/// # Panics
///
/// Panics if the runtime cannot initialize its state for the current thread.
pub fn set_timeout<F>(delay: Duration, callback: F) -> TimeoutHandle
where
F: FnOnce() + 'static,
@@ -98,10 +140,22 @@ where
}
}
/// Cancels a timeout previously created by [`set_timeout`].
///
/// # Panics
///
/// Panics if called from a different runtime thread than the one that created `handle`.
pub fn clear_timeout(handle: &TimeoutHandle) {
clear_timer(handle.owner, handle.id);
}
/// Schedules a repeating timer on the current runtime thread.
///
/// The callback is invoked once per interval until the handle is cleared.
///
/// # Panics
///
/// Panics if the runtime cannot initialize its state for the current thread.
pub fn set_interval<F>(delay: Duration, callback: F) -> IntervalHandle
where
F: FnMut() + 'static,
@@ -126,10 +180,33 @@ where
}
}
/// Cancels an interval previously created by [`set_interval`].
///
/// # Panics
///
/// Panics if called from a different runtime thread than the one that created `handle`.
pub fn clear_interval(handle: &IntervalHandle) {
clear_timer(handle.owner, handle.id);
}
/// Queues a future on the current runtime thread.
///
/// The future is scheduled immediately and can be awaited through the returned [`JoinHandle`].
///
/// The future will be driven to completion regardless or whether the join handle is polled or dropped, so this function
/// can be used as a convenient way to spawn detached async tasks on the current thread.
///
/// # Examples
///
/// ```
/// # let _ = || {
/// let handle = ruin_runtime::queue_future(async { 42usize });
/// # };
/// ```
///
/// # Panics
///
/// Panics if the runtime cannot initialize its state for the current thread.
pub fn queue_future<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + 'static,
@@ -150,13 +227,21 @@ where
JoinHandle { state }
}
/// Spawns a worker runtime thread.
///
/// `initial_task` is queued onto the worker as its first macrotask. `on_exit` runs on the parent
/// runtime thread after the worker shuts down.
///
/// # Panics
///
/// Panics if the worker thread or its driver cannot be created.
pub fn spawn_worker<Init, Exit>(initial_task: Init, on_exit: Exit) -> WorkerHandle
where
Init: FnOnce() + Send + 'static,
Exit: FnOnce() + 'static,
{
let parent = current_thread();
let (reactor, notifier) = create_reactor().expect("worker reactor should initialize");
let (driver, notifier) = create_driver().expect("worker driver should initialize");
let shared = Arc::new(ThreadShared::new(notifier));
let handle = ThreadHandle {
shared: Arc::clone(&shared),
@@ -175,7 +260,7 @@ where
std::thread::Builder::new()
.name("ruin-runtime-worker".into())
.spawn(move || {
install_thread(shared, reactor, Some(worker_completion));
install_thread(shared, driver, Some(worker_completion));
queue_task(initial_task);
run();
})
@@ -187,6 +272,14 @@ where
}
}
/// Runs the current runtime thread until no work, timers, child workers, or async operations
/// remain.
///
/// This is the main event loop entry point used by the proc-macro entry attributes.
///
/// # Panics
///
/// Panics if runtime initialization fails or if the underlying driver returns an unexpected error.
pub fn run() {
let _ = current_thread();
@@ -223,7 +316,7 @@ pub fn run() {
if has_pending_timers() || state.has_live_children() || state.has_live_async_operations() {
state.shared.closing.store(false, Ordering::Release);
state.reactor.wait().expect("reactor wait should succeed");
state.driver.wait().expect("driver wait should succeed");
continue;
}
@@ -240,16 +333,20 @@ pub fn run() {
}
fn drain_all() {
drain_reactor_events();
drain_driver_events();
drain_remote_tasks();
drain_completed_workers();
}
/// Returns a future that yields back to the runtime scheduler once.
pub fn yield_now() -> YieldNow {
YieldNow { yielded: false }
}
impl ThreadHandle {
/// Queues a macrotask onto this runtime thread.
///
/// Returns `false` if the target thread is already closed.
pub fn queue_task<F>(&self, task: F) -> bool
where
F: FnOnce() + Send + 'static,
@@ -257,6 +354,9 @@ impl ThreadHandle {
self.shared.enqueue_macro(Box::new(task))
}
/// Queues a microtask onto this runtime thread.
///
/// Returns `false` if the target thread is already closed.
pub fn queue_microtask<F>(&self, task: F) -> bool
where
F: FnOnce() + Send + 'static,
@@ -264,6 +364,7 @@ impl ThreadHandle {
self.shared.enqueue_micro(Box::new(task))
}
/// Returns `true` if the target runtime thread has shut down.
pub fn is_closed(&self) -> bool {
self.shared.closed.load(Ordering::Acquire)
}
@@ -282,6 +383,9 @@ impl ThreadHandle {
}
impl WorkerHandle {
/// Queues a macrotask onto the worker thread.
///
/// Returns `false` if the worker has already shut down.
pub fn queue_task<F>(&self, task: F) -> bool
where
F: FnOnce() + Send + 'static,
@@ -289,6 +393,9 @@ impl WorkerHandle {
self.thread.queue_task(task)
}
/// Queues a microtask onto the worker thread.
///
/// Returns `false` if the worker has already shut down.
pub fn queue_microtask<F>(&self, task: F) -> bool
where
F: FnOnce() + Send + 'static,
@@ -296,10 +403,12 @@ impl WorkerHandle {
self.thread.queue_microtask(task)
}
/// Returns `true` once the worker thread has fully exited.
pub fn is_finished(&self) -> bool {
self.completion.finished.load(Ordering::Acquire)
}
/// Returns a generic [`ThreadHandle`] for the worker thread.
pub fn thread(&self) -> ThreadHandle {
self.thread.clone()
}
@@ -328,7 +437,7 @@ impl Future for YieldNow {
}
struct ThreadState {
reactor: Reactor,
driver: Driver,
shared: Arc<ThreadShared>,
worker_completion: Option<Arc<WorkerCompletion>>,
local_microtasks: RefCell<VecDeque<LocalTask>>,
@@ -341,11 +450,11 @@ struct ThreadState {
impl ThreadState {
fn new(
shared: Arc<ThreadShared>,
reactor: Reactor,
driver: Driver,
worker_completion: Option<Arc<WorkerCompletion>>,
) -> Self {
Self {
reactor,
driver,
shared,
worker_completion,
local_microtasks: RefCell::new(VecDeque::new()),
@@ -695,11 +804,11 @@ unsafe fn future_task_drop(data: *const ()) {
fn current_thread() -> &'static ThreadState {
unsafe {
if CURRENT_THREAD.is_null() {
let (reactor, notifier) = create_reactor().expect("runtime reactor should initialize");
let (driver, notifier) = create_driver().expect("runtime driver should initialize");
let shared = Arc::new(ThreadShared::new(notifier));
let state = Box::new(ThreadState::new(shared, reactor, None));
let state = Box::new(ThreadState::new(shared, driver, None));
let state = Box::into_raw(state);
(*state).reactor.bind_current_thread();
(*state).driver.bind_current_thread();
CURRENT_THREAD = state;
}
@@ -713,14 +822,14 @@ fn current_thread_ptr() -> *const ThreadState {
fn install_thread(
shared: Arc<ThreadShared>,
reactor: Reactor,
driver: Driver,
worker_completion: Option<Arc<WorkerCompletion>>,
) {
unsafe {
debug_assert!(CURRENT_THREAD.is_null(), "thread runtime already installed");
let state = Box::new(ThreadState::new(shared, reactor, worker_completion));
let state = Box::new(ThreadState::new(shared, driver, worker_completion));
let state = Box::into_raw(state);
(*state).reactor.bind_current_thread();
(*state).driver.bind_current_thread();
CURRENT_THREAD = state;
}
}
@@ -731,18 +840,18 @@ fn teardown_thread() {
CURRENT_THREAD = ptr::null_mut();
if !state.is_null() {
(*state).reactor.unbind_current_thread();
(*state).driver.unbind_current_thread();
drop(Box::from_raw(state));
}
}
}
fn drain_reactor_events() {
fn drain_driver_events() {
loop {
let ready = current_thread()
.reactor
.driver
.poll()
.expect("reactor poll should succeed");
.expect("driver poll should succeed");
let Some(ready) = ready else {
break;
@@ -751,13 +860,13 @@ fn drain_reactor_events() {
let state = current_thread();
if ready.wake {
let _ = state
.reactor
.driver
.drain_wake()
.expect("wake drain should succeed");
}
if ready.timer {
let _ = state
.reactor
.driver
.drain_timer()
.expect("timer drain should succeed");
dispatch_expired_timers();
@@ -906,9 +1015,9 @@ fn dispatch_expired_timers() {
fn rearm_thread_timer() {
let deadline = current_thread().timers.borrow().peek_deadline();
current_thread()
.reactor
.driver
.rearm_timer(deadline)
.expect("timerfd rearm should succeed");
.expect("driver timer rearm should succeed");
}
fn deadline_from_now(delay: Duration) -> Duration {