tenferro_runtime/runtime/event_domain.rs
1use std::any::Any;
2use std::fmt;
3use std::sync::Arc;
4
5use super::EventDomainId;
6
7/// Classifies the runtime boundary at which event-domain provenance is
8/// validated.
9///
10/// The value is semantic control data for EventDomainError; provider
11/// function names are deliberately not part of this contract.
12///
13/// # Examples
14///
15/// ```
16/// use tenferro_runtime::EventDomainOperation;
17///
18/// assert_eq!(EventDomainOperation::Enqueue, EventDomainOperation::Enqueue);
19/// ```
20#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21pub enum EventDomainOperation {
22 /// A driver is beginning a run for a frozen domain.
23 BeginRun,
24 /// A run is admitting dependencies and a launch.
25 Enqueue,
26 /// A run is being retired after all submitted work.
27 Drain,
28 /// The scheduler is host-bridging a source completion for a transfer.
29 TransferBridge,
30 /// The scheduler is validating a returned completion token.
31 ValidateCompletion,
32}
33
34impl fmt::Display for EventDomainOperation {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 let name = match self {
37 Self::BeginRun => "begin run",
38 Self::Enqueue => "enqueue",
39 Self::Drain => "drain",
40 Self::TransferBridge => "transfer bridge",
41 Self::ValidateCompletion => "validate completion",
42 };
43 formatter.write_str(name)
44 }
45}
46
47/// Structured event-domain admission and provenance failure.
48///
49/// Every provenance mismatch retains the expected and actual domains together
50/// with the operation and optional scheduled-node context; a missing-driver
51/// failure identifies the unavailable domain directly. Providers use the same
52/// vocabulary for direct admission failures.
53///
54/// # Examples
55///
56/// ```
57/// use tenferro_runtime::runtime::EventDomainError;
58///
59/// fn inspect(error: &EventDomainError) {
60/// let _ = error;
61/// }
62/// ```
63#[derive(Debug, thiserror::Error)]
64#[non_exhaustive]
65pub enum EventDomainError {
66 /// The scheduler reached a domain for which no driver run was registered.
67 #[error("event domain {domain:?} has no registered driver")]
68 MissingDriver {
69 /// Event domain whose driver run was unavailable.
70 domain: EventDomainId,
71 },
72 /// A driver returned a run for a domain other than the requested domain.
73 #[error(
74 "{operation} node {node_index:?} returned run domain {actual:?}, expected {expected:?}"
75 )]
76 RunDomainMismatch {
77 /// Operation validating the run.
78 operation: EventDomainOperation,
79 /// Scheduled node, when the validation belongs to a node.
80 node_index: Option<usize>,
81 /// Domain requested by the runtime.
82 expected: EventDomainId,
83 /// Domain reported by the driver run.
84 actual: EventDomainId,
85 },
86 /// A dependency token was not from the domain admitted at this boundary.
87 #[error(
88 "{operation} node {node_index:?} received dependency origin {actual:?}, expected {expected:?}"
89 )]
90 DependencyDomainMismatch {
91 /// Operation validating the dependency.
92 operation: EventDomainOperation,
93 /// Scheduled node, when the validation belongs to a node.
94 node_index: Option<usize>,
95 /// Domain that may admit the dependency.
96 expected: EventDomainId,
97 /// Origin reported by the dependency token.
98 actual: EventDomainId,
99 },
100 /// A driver returned a completion token with the wrong origin.
101 #[error(
102 "{operation} node {node_index:?} returned completion origin {actual:?}, expected {expected:?}"
103 )]
104 CompletionTokenDomainMismatch {
105 /// Operation validating the completion.
106 operation: EventDomainOperation,
107 /// Scheduled node, when the validation belongs to a node.
108 node_index: Option<usize>,
109 /// Completion domain assigned by the schedule.
110 expected: EventDomainId,
111 /// Origin reported by the returned token.
112 actual: EventDomainId,
113 },
114 /// A public event-domain run panicked while being explicitly retired.
115 #[error("{operation} for event domain {domain:?} panicked: {message}")]
116 DrainPanicked {
117 /// Operation that contained the provider panic.
118 operation: EventDomainOperation,
119 /// Domain whose run was being retired.
120 domain: EventDomainId,
121 /// Safe diagnostic text extracted from the panic payload.
122 message: String,
123 },
124 /// A token has the right origin but cannot be admitted by this provider.
125 #[error(
126 "{operation} node {node_index:?} cannot admit token type {token_type} for event domain {actual:?} (expected {expected:?})"
127 )]
128 IncompatibleTokenType {
129 /// Operation validating the token.
130 operation: EventDomainOperation,
131 /// Scheduled node, when the validation belongs to a node.
132 node_index: Option<usize>,
133 /// Domain the run admits.
134 expected: EventDomainId,
135 /// Origin reported by the token.
136 actual: EventDomainId,
137 /// Provider-neutral diagnostic name of the token type.
138 token_type: &'static str,
139 },
140 /// The scheduler's host bridge could not wait for a source completion.
141 #[error(
142 "{operation} node {node_index:?} failed waiting for dependency origin {actual:?} before destination domain {expected:?}: {source}"
143 )]
144 DependencyWaitFailed {
145 /// Operation validating the transfer bridge.
146 operation: EventDomainOperation,
147 /// Scheduled node containing the failed bridge.
148 node_index: Option<usize>,
149 /// Destination domain that was not launched.
150 expected: EventDomainId,
151 /// Origin domain whose token failed to wait.
152 actual: EventDomainId,
153 /// Original host-wait failure.
154 #[source]
155 source: crate::error::BoxError,
156 },
157}
158
159/// Opaque completion token produced by one event-domain run.
160///
161/// `wait` is repeatable and safe to call concurrently. A schedule may fan one
162/// completion out to multiple foreign event domains, so implementations must
163/// not consume the token on the first wait.
164///
165/// # Examples
166///
167/// ```
168/// use tenferro_runtime::runtime::EventToken;
169///
170/// fn inspect(token: &dyn EventToken) -> tenferro_runtime::Result<()> {
171/// let _origin = token.origin();
172/// let _opaque = token.as_any();
173/// token.wait()?;
174/// token.wait()
175/// }
176/// ```
177pub trait EventToken: fmt::Debug + Send + Sync + 'static {
178 /// Return the token as [`Any`] for driver-specific inspection.
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use tenferro_runtime::{EventDomainDriver, EventDomainId};
184 ///
185 /// fn inspect(
186 /// driver: &dyn EventDomainDriver,
187 /// domain: EventDomainId,
188 /// ) -> tenferro_runtime::Result<()> {
189 /// let mut run = driver.begin_run(domain)?;
190 /// let mut launch = || Ok(());
191 /// let token = run.enqueue(&[], &mut launch)?;
192 /// let _opaque = token.as_any();
193 /// Ok(())
194 /// }
195 /// ```
196 fn as_any(&self) -> &dyn Any;
197
198 /// Return the exact frozen event domain that produced this token.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use tenferro_runtime::{EventDomainId, EventToken};
204 ///
205 /// fn inspect(token: &dyn EventToken, expected: EventDomainId) {
206 /// assert_eq!(token.origin(), expected);
207 /// }
208 /// ```
209 fn origin(&self) -> EventDomainId;
210
211 /// Wait on the host until this completion becomes ready.
212 ///
213 /// The runtime scheduler uses this repeatable host bridge only after it
214 /// classifies a scheduled cross-domain transfer dependency. Destination
215 /// drivers must not use it as a generic foreign-token fallback.
216 ///
217 /// # Errors
218 ///
219 /// Returns a typed backend or runtime error when the native completion
220 /// reports failure.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use tenferro_runtime::EventToken;
226 ///
227 /// fn wait_repeatably(token: &dyn EventToken) -> tenferro_runtime::Result<()> {
228 /// token.wait()?;
229 /// token.wait()
230 /// }
231 /// ```
232 fn wait(&self) -> crate::Result<()>;
233}
234
235/// Per-execution state owned by one event domain.
236///
237/// # Examples
238///
239/// ```
240/// use tenferro_runtime::{EventDomainId, EventDomainRun};
241///
242/// fn submit(
243/// run: &mut dyn EventDomainRun,
244/// domain: EventDomainId,
245/// ) -> tenferro_runtime::Result<()> {
246/// assert_eq!(run.domain(), domain);
247/// let mut launch = || Ok(());
248/// run.enqueue(&[], &mut launch)?;
249/// run.drain()
250/// }
251/// ```
252pub trait EventDomainRun: fmt::Debug + Send {
253 /// Return the exact domain admitted by this run.
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// use tenferro_runtime::{EventDomainId, EventDomainRun};
259 ///
260 /// fn inspect(run: &dyn EventDomainRun, expected: EventDomainId) {
261 /// assert_eq!(run.domain(), expected);
262 /// }
263 /// ```
264 fn domain(&self) -> EventDomainId;
265
266 /// Accept the single-domain coalesced submission fast path when supported.
267 ///
268 /// The scheduler calls this at most once per run, before any launch, and
269 /// only after it has proven that every scheduled location and node of the
270 /// run uses this exact domain and that the schedule contains no transfer
271 /// node. Returning `true` promises that same-stream submission order alone
272 /// orders this run's nodes: the scheduler then submits each launch through
273 /// this run without dependency tokens and records no per-node completion,
274 /// keeping only the run's own [`Self::drain`] retirement.
275 ///
276 /// Returning `false` (the default) keeps the per-node completion contract,
277 /// so an unknown provider behaves exactly as before. A run must not return
278 /// `true` unless it really keeps only submission order for ordering, because
279 /// a node would otherwise be allowed to run before the node it depends on.
280 /// Operation-level barriers that a launch needs for raw or vendor work are
281 /// unaffected.
282 ///
283 /// # Examples
284 ///
285 /// ```
286 /// use tenferro_runtime::EventDomainRun;
287 ///
288 /// fn keeps_per_node_completions(run: &mut dyn EventDomainRun) -> bool {
289 /// !run.enable_single_domain_coalescing()
290 /// }
291 /// ```
292 fn enable_single_domain_coalescing(&mut self) -> bool {
293 false
294 }
295
296 /// Enqueue one launch after its dependency tokens.
297 ///
298 /// # Errors
299 ///
300 /// Returns a typed runtime error when a dependency token is incompatible
301 /// with this domain, native queue submission fails, or `launch` returns an
302 /// error.
303 ///
304 /// The implementation must not invoke `launch` when dependency admission
305 /// or waiting fails. After dependencies are admitted, it invokes `launch`
306 /// exactly once without holding a driver lock. The closure submits work to
307 /// the native queue; it does not wait for that work to complete.
308 ///
309 /// # Examples
310 ///
311 /// ```
312 /// use tenferro_runtime::EventDomainRun;
313 ///
314 /// fn submit(run: &mut dyn EventDomainRun) -> tenferro_runtime::Result<()> {
315 /// let mut launched = false;
316 /// let mut launch = || {
317 /// launched = true;
318 /// Ok(())
319 /// };
320 /// run.enqueue(&[], &mut launch)?;
321 /// assert!(launched);
322 /// Ok(())
323 /// }
324 /// ```
325 fn enqueue(
326 &mut self,
327 dependencies: &[Arc<dyn EventToken>],
328 launch: &mut dyn FnMut() -> crate::Result<()>,
329 ) -> crate::Result<Arc<dyn EventToken>>;
330
331 /// Wait for all work already enqueued in this run.
332 ///
333 /// # Errors
334 ///
335 /// Returns the first driver-defined typed error, such as
336 /// [`crate::Error::Internal`], when a queued completion reports failure or
337 /// the native queue cannot be synchronized.
338 ///
339 /// Success and error are both retirement boundaries: before returning, the
340 /// run must ensure that no previously enqueued work can access tensors,
341 /// tokens, or native resources retained by the caller. An error reports the
342 /// completion failure; it must not mean that work is still using those
343 /// resources. Implementations must not unwind from this method.
344 ///
345 /// Draining observes already-progressing work. It must not require another
346 /// event domain's drain call to start that work.
347 ///
348 /// Dropping a run must perform equivalent best-effort retirement when
349 /// explicit draining is skipped by panic unwinding. Neither explicit
350 /// draining nor implicit retirement may unwind into the caller.
351 fn drain(&mut self) -> crate::Result<()>;
352}
353
354/// Runtime-owned factory for per-execution event-domain state.
355///
356/// # Examples
357///
358/// ```
359/// use tenferro_runtime::{EventDomainDriver, EventDomainId};
360///
361/// fn begin(
362/// driver: &dyn EventDomainDriver,
363/// domain: EventDomainId,
364/// ) -> tenferro_runtime::Result<()> {
365/// let mut run = driver.begin_run(domain)?;
366/// run.drain()
367/// }
368/// ```
369pub trait EventDomainDriver: fmt::Debug + Send + Sync + 'static {
370 /// Report whether this provider proves retirement before a scoped call returns.
371 ///
372 /// The default is conservative: asynchronous queues and providers with
373 /// unknown retirement semantics cannot admit borrowed scoped inputs.
374 fn supports_scoped_read_only(&self) -> bool {
375 false
376 }
377
378 /// Begin one isolated execution run for exactly `domain`.
379 ///
380 /// # Errors
381 ///
382 /// Returns a driver-defined typed error, such as [`crate::Error::Internal`],
383 /// when native queue, stream, or per-run event-state allocation fails, or
384 /// when the domain cannot admit a new run.
385 fn begin_run(&self, domain: EventDomainId) -> crate::Result<Box<dyn EventDomainRun>>;
386}
387
388/// Completion token returned by the blocking immediate domain.
389#[derive(Debug)]
390struct ReadyEventToken {
391 origin: EventDomainId,
392}
393
394impl EventToken for ReadyEventToken {
395 fn as_any(&self) -> &dyn Any {
396 self
397 }
398
399 fn origin(&self) -> EventDomainId {
400 self.origin
401 }
402
403 fn wait(&self) -> crate::Result<()> {
404 Ok(())
405 }
406}
407
408/// Blocking event-domain driver used by synchronous CPU engines.
409///
410/// # Examples
411///
412/// ```
413/// use tenferro_runtime::runtime::ImmediateEventDomainDriver;
414///
415/// let _driver = ImmediateEventDomainDriver::new();
416/// ```
417#[derive(Clone, Copy, Debug, Default)]
418pub struct ImmediateEventDomainDriver;
419
420impl ImmediateEventDomainDriver {
421 /// Construct a blocking immediate driver.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 /// use tenferro_runtime::runtime::ImmediateEventDomainDriver;
427 ///
428 /// let _driver = ImmediateEventDomainDriver::new();
429 /// ```
430 #[must_use]
431 pub const fn new() -> Self {
432 Self
433 }
434}
435
436impl EventDomainDriver for ImmediateEventDomainDriver {
437 fn supports_scoped_read_only(&self) -> bool {
438 true
439 }
440
441 fn begin_run(&self, domain: EventDomainId) -> crate::Result<Box<dyn EventDomainRun>> {
442 Ok(Box::new(ImmediateEventDomainRun {
443 domain,
444 coalesced: false,
445 shared_token: None,
446 }))
447 }
448}
449
450#[derive(Debug)]
451struct ImmediateEventDomainRun {
452 domain: EventDomainId,
453 /// Set when the scheduler proved that this run is the only domain and has
454 /// no transfer node. Every launch retires before `enqueue` returns here, so
455 /// submission order alone already orders the run's nodes and the per-node
456 /// completion tokens carry no information.
457 coalesced: bool,
458 /// The token every coalesced completion reports, created on first use.
459 ///
460 /// A coalesced run's token only carries the domain, so one shared token
461 /// answers every node instead of one allocation per node.
462 shared_token: Option<Arc<dyn EventToken>>,
463}
464
465impl EventDomainRun for ImmediateEventDomainRun {
466 fn domain(&self) -> EventDomainId {
467 self.domain
468 }
469
470 fn enable_single_domain_coalescing(&mut self) -> bool {
471 self.coalesced = true;
472 true
473 }
474
475 fn enqueue(
476 &mut self,
477 dependencies: &[Arc<dyn EventToken>],
478 launch: &mut dyn FnMut() -> crate::Result<()>,
479 ) -> crate::Result<Arc<dyn EventToken>> {
480 for dependency in dependencies {
481 let actual = dependency.origin();
482 if actual != self.domain {
483 return Err(crate::Error::from(
484 EventDomainError::DependencyDomainMismatch {
485 operation: EventDomainOperation::Enqueue,
486 node_index: None,
487 expected: self.domain,
488 actual,
489 },
490 ));
491 }
492 dependency.wait()?;
493 }
494 launch()?;
495 if self.coalesced {
496 let token = self.shared_token.get_or_insert_with(|| {
497 Arc::new(ReadyEventToken {
498 origin: self.domain,
499 }) as Arc<dyn EventToken>
500 });
501 return Ok(Arc::clone(token));
502 }
503 Ok(Arc::new(ReadyEventToken {
504 origin: self.domain,
505 }))
506 }
507
508 fn drain(&mut self) -> crate::Result<()> {
509 Ok(())
510 }
511}
512
513#[cfg(test)]
514mod tests;