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;