Skip to main content

continuo/
execution.rs

1//! Optional executor placement and poll scheduling for runnable generations.
2//!
3//! The default uses the ambient Tokio runtime without an execution adapter.
4//! Custom specifications require an application-installed [`ExecutionDriver`].
5//! This module defines the contract; it creates no threads or schedulers.
6
7use std::any::{TypeId, type_name};
8use std::fmt;
9use std::future::Future;
10use std::num::NonZeroUsize;
11use std::pin::Pin;
12use std::sync::Arc;
13
14use crate::RunContext;
15use crate::registry::{Error, Result};
16
17/// A process-local, typed identity for a shared execution domain.
18///
19/// Applications supply marker types. A domain is not a component dependency,
20/// a service name, an OS thread id or a cross-process identity.
21#[derive(Clone, Eq, Hash, PartialEq)]
22pub struct ExecutionDomain {
23    id: TypeId,
24    type_name: &'static str,
25    instance: Option<Arc<str>>,
26}
27
28impl ExecutionDomain {
29    /// Identify a domain by its marker type. Reusing the type selects the same
30    /// process-local domain; different types remain independent.
31    pub fn of<T: ?Sized + 'static>() -> Self {
32        Self {
33            id: TypeId::of::<T>(),
34            type_name: type_name::<T>(),
35            instance: None,
36        }
37    }
38
39    /// Select a named domain within a marker type. The name is owned; it may
40    /// come from configuration and is never promoted to a static reference.
41    pub fn named<T: ?Sized + 'static>(name: impl Into<Arc<str>>) -> Self {
42        Self { instance: Some(name.into()), ..Self::of::<T>() }
43    }
44}
45
46impl fmt::Debug for ExecutionDomain {
47    fn fmt(
48        &self,
49        f: &mut fmt::Formatter<'_>,
50    ) -> fmt::Result {
51        f.debug_struct("ExecutionDomain")
52            .field("type", &self.type_name)
53            .field("instance", &self.instance)
54            .finish()
55    }
56}
57
58/// Thread topology requested from an execution driver.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60#[non_exhaustive]
61pub enum ExecutorKind {
62    /// A single-threaded executor; placement on an OS thread belongs to the driver.
63    CurrentThread,
64    /// An executor with the requested nonzero number of worker threads.
65    MultiThread { threads: NonZeroUsize },
66}
67
68/// Placement of one runnable generation, independent of fleet dispatch.
69#[derive(Clone, Debug, Default, Eq, PartialEq)]
70#[non_exhaustive]
71pub enum ExecutorSpec {
72    /// Use the executor which starts the runnable.
73    #[default]
74    Inherit,
75    /// Share an application-defined execution domain with other runnables.
76    Shared(ExecutionDomain),
77    /// Give this generation an execution owner with the declared topology.
78    /// Resources that outlive the generation need a shared owner instead.
79    Dedicated(ExecutorKind),
80}
81
82/// Poll urgency, unrelated to component boot or run-start order.
83///
84/// The installed driver must document fairness and starvation behavior.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86#[non_exhaustive]
87pub enum PollPriority {
88    /// Lower urgency, with fairness defined by the installed driver.
89    Low,
90    /// Explicit normal urgency; still requires driver support.
91    Normal,
92    /// Higher urgency, without preempting a future already being polled.
93    High,
94}
95
96/// Selection among ready work, implemented by the installed driver.
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98#[non_exhaustive]
99pub enum SchedulingPolicy {
100    /// Keep the selected executor's ordinary poll scheduling.
101    #[default]
102    Inherit,
103    /// Request driver-defined urgency among ready work.
104    Priority(PollPriority),
105    /// Relative weight; interpretation belongs to the driver's scheduler.
106    Weighted(NonZeroUsize),
107}
108
109/// Execution requirements sampled once before admitting a generation.
110///
111/// Defaults preserve ordinary Tokio spawning. Non-default requirements are
112/// never silently downgraded. Applications can implement `From<LocalPolicy>`
113/// for this type without adding their domain concepts to Continuo.
114///
115/// ```
116/// use continuo::{ExecutionDomain, ExecutionSpec, ExecutorSpec};
117///
118/// struct Background;
119/// let spec = ExecutionSpec::default().with_executor(
120///     ExecutorSpec::Shared(ExecutionDomain::of::<Background>()),
121/// );
122/// assert_ne!(spec, ExecutionSpec::default());
123/// ```
124#[derive(Clone, Debug, Default, Eq, PartialEq)]
125#[non_exhaustive]
126pub struct ExecutionSpec {
127    /// Where this generation executes.
128    pub executor: ExecutorSpec,
129    /// How ready work is selected within that executor.
130    pub scheduling: SchedulingPolicy,
131}
132
133impl ExecutionSpec {
134    /// Set placement without changing poll scheduling.
135    pub fn with_executor(
136        mut self,
137        executor: ExecutorSpec,
138    ) -> Self {
139        self.executor = executor;
140        self
141    }
142
143    /// Set poll scheduling without changing placement.
144    pub fn with_scheduling(
145        mut self,
146        scheduling: SchedulingPolicy,
147    ) -> Self {
148        self.scheduling = scheduling;
149        self
150    }
151}
152
153/// An owned runnable execution. Only custom execution crosses this erasure.
154pub type ExecutionFuture =
155    Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
156
157/// Prepare custom execution before a runnable is marked running.
158///
159/// Preparation must reject unsupported placement or scheduling and must not
160/// start operational work. Initial automatic generations are all prepared
161/// before any is spawned. Continuo supplies no custom driver by default.
162pub trait ExecutionDriver: Send + Sync + 'static {
163    /// Validate the complete specification and return a generation adapter.
164    /// An error rejects admission; it must not fall back to ambient execution.
165    fn prepare(
166        &self,
167        spec: ExecutionSpec,
168    ) -> Result<Arc<dyn PreparedExecution>>;
169
170    /// Reap transferred executions after managed runnable futures have drained.
171    /// This also covers work whose submitting future was explicitly aborted.
172    /// Shared executors stay alive here: providers have not finalized yet.
173    /// The returned future must await owned work without inventing a deadline.
174    fn drain(&self) -> ExecutionFuture {
175        Box::pin(async { Ok(()) })
176    }
177
178    /// Release shared executor owners after runnable drain and provider
179    /// finalization. Their I/O drivers must remain alive while providers
180    /// close resources created by admitted work.
181    /// A targeted service stop or restart does not call this hook.
182    fn finalize(&self) -> ExecutionFuture {
183        Box::pin(async { Ok(()) })
184    }
185}
186
187/// One admitted generation's executor/scheduler adapter.
188///
189/// `execute` must honor the complete prepared specification. Its returned
190/// future owns the submitted work until completion, including cancellation
191/// and drain; it must not report completion while transferred work survives.
192/// Errors and panics on a different executor must be returned to this future.
193/// An adapter owns runtimes it creates and only borrows externally supplied
194/// runtimes. Shared owners must outlive every admitted generation using them.
195///
196/// The context exposes this same adapter to child-work owners. Ordinary
197/// `tokio::spawn` does not acquire a custom poll priority automatically;
198/// adapters and hosted services must explicitly carry the execution policy
199/// into their child submissions. Continuo does not intercept Tokio spawning.
200/// `RunContext::spawn_tracked` submits through `execute_child` on this same
201/// adapter. A generation-owned executor can therefore separate root teardown
202/// from concurrent child submissions.
203pub trait PreparedExecution: Send + Sync + 'static {
204    /// Execute the owned root generation with its frozen cancellation and execution
205    /// context. If this future is dropped, retain ownership of any transferred
206    /// work so [`ExecutionDriver::drain`] can reap it.
207    fn execute(
208        &self,
209        task: ExecutionFuture,
210        context: RunContext,
211    ) -> ExecutionFuture;
212
213    /// Submit a child using the root generation's prepared executor/scheduler.
214    /// The default supports adapters whose submissions share one lifetime.
215    /// Generation-owned adapters override this to retain their reactor until
216    /// root completion while allowing concurrent children on that reactor.
217    fn execute_child(
218        &self,
219        task: ExecutionFuture,
220        context: RunContext,
221    ) -> ExecutionFuture {
222        self.execute(task, context)
223    }
224}
225
226#[derive(Clone, Default)]
227pub(crate) struct ExecutionPlan {
228    pub(crate) spec: ExecutionSpec,
229    pub(crate) executor: Option<Arc<dyn PreparedExecution>>,
230}
231
232impl ExecutionPlan {
233    pub(crate) fn prepare(
234        spec: ExecutionSpec,
235        driver: Option<&dyn ExecutionDriver>,
236    ) -> Result<Self> {
237        if spec == ExecutionSpec::default() {
238            return Ok(Self::default());
239        }
240        let driver = driver.ok_or_else(|| {
241            Error::msg(format!("no execution driver is installed for {spec:?}"))
242        })?;
243        Ok(Self { executor: Some(driver.prepare(spec.clone())?), spec })
244    }
245}
246
247impl fmt::Debug for ExecutionPlan {
248    fn fmt(
249        &self,
250        f: &mut fmt::Formatter<'_>,
251    ) -> fmt::Result {
252        f.debug_struct("ExecutionPlan").field("spec", &self.spec).finish()
253    }
254}
255
256#[cfg(test)]
257#[path = "../tests/execution/unit.rs"]
258mod tests;