swactor/crates/engine/src/engine.rs
Zachery Aaron Shores-Chmielewski df72b4ff60 chore(test): enforce complete local test barrier
Deny workspace warnings and lint suppressions, consolidate Rust and Python coverage under cargo xtask test with 60-second per-test limits, and remove stale flaky, stateful, Docker, and orphaned test artifacts.
2026-08-23 15:51:24 +04:00

343 lines
12 KiB
Rust

//! Composite engine and cloneable scheduler handle.
use parking_lot::{Condvar, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use std::time::Duration;
use crate::backend::{Capabilities, EngineError, ExecutionBackend};
use crate::time::{EngineInstant, Interval, Timeout, Timer};
use swactor::actor::{ActorAddress, Message};
use swactor::runtime::{ExternalSender, Runtime, RuntimeParts};
/// The composite engine: retains a configured core runtime handle and its
/// execution backend, and owns one core-driving loop per worker.
///
/// Construct with [`Engine::new`]; obtain a scheduler handle with
/// [`Engine::handle`].
pub struct Engine {
/// Retained so the engine owns the runtime handle it drives for its full
/// lifetime. Core workers are moved into substrate tasks at construction.
_runtime: Runtime,
backend: Arc<dyn ExecutionBackend>,
}
impl Engine {
/// Construct an engine over `parts` driven by `backend`.
///
/// The runtime parts must be fully configured beforehand; after construction
/// the engine owns every worker and is their sole driver. Construction fails
/// if `backend` does not advertise a capability the engine requires (at
/// minimum, `tasks`).
pub fn new(parts: RuntimeParts, backend: impl ExecutionBackend) -> Result<Self, EngineError> {
let backend: Arc<dyn ExecutionBackend> = Arc::new(backend);
if !backend.capabilities().tasks {
return Err(EngineError::MissingRequiredCapability);
}
let runtime = parts.runtime().clone();
let workers = parts.into_workers();
// Install one core-driving loop per worker. This is substrate-neutral —
// no Tokio feature gate — so core progression does not silently
// disappear when an alternate backend is used (ENGINE_SPEC.md).
crate::core_driver::install(workers, &backend);
Ok(Engine {
_runtime: runtime,
backend,
})
}
/// Return a clonable handle for scheduling engine work.
///
/// The handle holds a *weak* backend reference, so handles — and engine
/// work that captures them — never keep the backend alive. Dropping the
/// [`Engine`] releases the backend (and its owned runtime / core-driver
/// task) once no other strong reference remains (ENGINE_SPEC.md).
pub fn handle(&self) -> EngineHandle {
EngineHandle {
backend: Arc::downgrade(&self.backend),
}
}
}
/// A cloneable scheduler handle.
///
/// Schedules work and reads engine time without exposing the underlying
/// backend; in particular it never hands out a raw `tokio::runtime::Handle`.
/// The handle holds a **weak** backend reference: it does not keep the engine
/// or its backend alive. Using a handle after its engine has been dropped
/// degrades gracefully — scheduled work is dropped, timers never fire, and
/// capability checks report no capabilities — rather than retaining the
/// backend (ENGINE_SPEC.md).
#[derive(Clone)]
pub struct EngineHandle {
backend: Weak<dyn ExecutionBackend>,
}
/// A clonable substrate route for one-shot blocking I/O work.
///
/// Domain actors decide which effect to execute; this handle only moves its
/// mechanics onto the engine backend's dedicated blocking pool.
#[derive(Clone)]
pub struct BlockingWorkSender {
backend: Weak<dyn ExecutionBackend>,
}
impl BlockingWorkSender {
/// Submit one blocking I/O operation without occupying an actor worker.
///
/// Returns the operation unchanged if the owning engine has stopped.
pub fn submit(&self, work: crate::BoxWork) -> Result<(), crate::BoxWork> {
let Some(backend) = self.backend.upgrade() else {
return Err(work);
};
backend.spawn_blocking(work);
Ok(())
}
}
/// Cancellation handle for an engine-owned actor message timer.
///
/// Cancellation is idempotent. A timer may already have fired when cancellation
/// races its deadline, so actor messages should still carry an operation or
/// generation identity that lets the receiver reject stale work.
#[derive(Clone, Debug)]
pub struct ActorTimer {
cancelled: Arc<AtomicBool>,
}
impl ActorTimer {
/// Prevent this timer from delivering future messages.
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
}
/// Report whether cancellation has been requested.
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
}
enum CompletionState<T> {
Pending,
Ready(T),
Consumed,
}
/// One actor-owned terminal observation for a synchronous process entrypoint.
///
/// The waiting thread cannot poll, set a deadline, or advance domain state.
/// An actor decides when the operation is complete and publishes the value.
pub struct ActorCompletion<T> {
inner: Arc<(Mutex<CompletionState<T>>, Condvar)>,
}
impl<T> Clone for ActorCompletion<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<T> Default for ActorCompletion<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> ActorCompletion<T> {
pub fn new() -> Self {
Self {
inner: Arc::new((Mutex::new(CompletionState::Pending), Condvar::new())),
}
}
/// Publish the terminal observation once.
pub fn complete(&self, value: T) -> Result<(), T> {
let (state, ready) = &*self.inner;
let mut state = state.lock();
if !matches!(*state, CompletionState::Pending) {
return Err(value);
}
*state = CompletionState::Ready(value);
ready.notify_all();
Ok(())
}
/// Block the process entrypoint until the owning actor completes.
pub fn wait(&self) -> T {
let (state, ready) = &*self.inner;
let mut state = state.lock();
loop {
if matches!(*state, CompletionState::Pending) {
ready.wait(&mut state);
continue;
}
match std::mem::replace(&mut *state, CompletionState::Consumed) {
CompletionState::Ready(value) => return value,
CompletionState::Consumed => {
panic!("ActorCompletion::wait called after value was consumed")
}
CompletionState::Pending => unreachable!("pending state handled above"),
}
}
}
}
impl EngineHandle {
/// Upgrade to the live backend, or `None` if the owning engine is gone.
fn backend(&self) -> Option<Arc<dyn ExecutionBackend>> {
self.backend.upgrade()
}
/// Schedule `task` as cooperative engine work.
///
/// A no-op once the owning engine has been dropped: the work is discarded
/// rather than keeping the backend alive.
pub fn spawn<F>(&self, task: F)
where
F: Future<Output = ()> + Send + 'static,
{
if let Some(backend) = self.backend() {
backend.spawn(Box::pin(task));
}
}
/// Create a route to the backend's dedicated blocking-I/O pool.
pub fn blocking_work_sender(&self) -> BlockingWorkSender {
BlockingWorkSender {
backend: self.backend.clone(),
}
}
/// Produce a future that completes after `delay`.
///
/// Once the owning engine has been dropped this returns a timer that never
/// fires.
pub fn timer(&self, delay: Duration) -> Timer {
match self.backend() {
Some(backend) => Timer {
inner: backend.timer(delay),
},
None => Timer::closed(),
}
}
/// Produce a future that recurs every `period`.
pub fn interval(&self, period: Duration) -> Interval {
Interval {
period,
backend: self.backend.clone(),
current: None,
}
}
/// Schedule one typed message for delivery after `delay`.
///
/// The engine owns the timer task; domain code receives no future or
/// scheduling callback. The actor receiving `message` owns the deadline
/// decision and should reject stale operation identities.
pub fn send_after<M>(
&self,
delay: Duration,
sender: ExternalSender,
actor: ActorAddress,
message: M,
) -> ActorTimer
where
M: Message,
{
let actor_timer = ActorTimer {
cancelled: Arc::new(AtomicBool::new(false)),
};
let cancelled = Arc::clone(&actor_timer.cancelled);
let timer = self.timer(delay);
self.spawn(async move {
timer.await;
if !cancelled.load(Ordering::Acquire) {
let _ = sender.send_to(actor, message);
}
});
actor_timer
}
/// Schedule a cloned typed message after every `period`.
///
/// Delivery stops after cancellation or when the actor address no longer
/// accepts messages.
pub fn send_every<M>(
&self,
period: Duration,
sender: ExternalSender,
actor: ActorAddress,
message: M,
) -> ActorTimer
where
M: Message,
{
let actor_timer = ActorTimer {
cancelled: Arc::new(AtomicBool::new(false)),
};
let cancelled = Arc::clone(&actor_timer.cancelled);
let handle = self.clone();
let mut interval = Box::pin(handle.interval(period));
self.spawn(std::future::poll_fn(move |cx| {
if cancelled.load(Ordering::Acquire) {
return std::task::Poll::Ready(());
}
match std::future::Future::poll(interval.as_mut(), cx) {
std::task::Poll::Ready(()) => {
if cancelled.load(Ordering::Acquire)
|| sender.send_to(actor, message.clone()).is_err()
{
std::task::Poll::Ready(())
} else {
cx.waker().wake_by_ref();
std::task::Poll::Pending
}
}
std::task::Poll::Pending => std::task::Poll::Pending,
}
}));
actor_timer
}
/// Race `future` against an engine timer.
///
/// Resolves to `Ok` with the future's output if it completes within
/// `duration`, or [`Err(Elapsed)`](crate::Elapsed) when the timer fires
pub fn timeout<F: std::future::Future>(&self, duration: Duration, future: F) -> Timeout<F> {
Timeout::new(self.timer(duration), future)
}
/// Read the engine's monotonic clock.
///
/// Falls back to the real wall clock once the owning engine has been
/// dropped, since the substrate clock is no longer available.
pub fn now(&self) -> EngineInstant {
match self.backend() {
Some(backend) => backend.now(),
None => EngineInstant::now(),
}
}
/// Report the backend's advertised capabilities.
///
/// Reports no capabilities once the owning engine has been dropped.
pub fn capabilities(&self) -> Capabilities {
match self.backend() {
Some(backend) => backend.capabilities(),
None => Capabilities::NONE,
}
}
/// Validate that this engine satisfies `required` before starting work.
///
/// Returns `Err` if the backend cannot provide a requested capability, or
/// if the owning engine has been dropped. Call this before allocating
/// resources, starting background work, or becoming externally visible so
/// that an incompatible engine is rejected early (ENGINE_SPEC.md).
pub fn require(&self, required: Capabilities) -> Result<(), EngineError> {
match self.backend() {
Some(backend) if backend.capabilities().satisfies(required) => Ok(()),
_ => Err(EngineError::MissingRequiredCapability),
}
}
}