Skip to main content

dfir_rs/scheduled/
context.rs

1//! Module for the inline DFIR runtime context and execution engine.
2//!
3//! Provides [`Context`] (the lightweight operator context) and
4//! [`Dfir`] (the dataflow execution wrapper).
5
6use std::future::Future;
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::rc::Rc;
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12use std::task::Wake;
13
14#[cfg(feature = "meta")]
15use dfir_lang::diagnostic::{Diagnostic, Diagnostics, SerdeSpan};
16#[cfg(feature = "meta")]
17use dfir_lang::graph::DfirGraph;
18
19use super::metrics::{DfirMetrics, DfirMetricsIntervals, MetricsState};
20use crate::scheduled::ticks::TickInstant;
21
22/// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
23/// (the external runner). Shared via `Arc` between both.
24///
25/// When external data arrives (e.g., a tokio stream receives a message), the [`Context::waker`]
26/// fires, which sets `can_start_tick` and wakes the [`Dfir::run`](Dfir::run) task so it starts a new tick.
27/// Implements [`Wake`] directly so it can be used as a `Waker` without an extra wrapper.
28#[doc(hidden)]
29pub struct WakeState {
30    /// Set to `true` when external data arrives, signaling that a new tick should run.
31    /// Checked by [`Dfir::run_tick`](Dfir::run_tick) and [`Dfir::run_available`](Dfir::run_available).
32    can_start_tick: std::sync::atomic::AtomicBool,
33    /// Wakes the [`Dfir::run`](Dfir::run) task from its idle `poll_fn` sleep.
34    task_waker: futures::task::AtomicWaker,
35}
36
37impl Default for WakeState {
38    fn default() -> Self {
39        Self {
40            can_start_tick: std::sync::atomic::AtomicBool::new(false),
41            task_waker: futures::task::AtomicWaker::new(),
42        }
43    }
44}
45
46impl Wake for WakeState {
47    fn wake(self: Arc<Self>) {
48        self.wake_by_ref();
49    }
50
51    fn wake_by_ref(self: &Arc<Self>) {
52        self.can_start_tick.store(true, Ordering::Relaxed);
53        self.task_waker.wake();
54    }
55}
56
57/// A lightweight context for inline codegen that avoids the overhead of the full
58/// scheduled graph (no tokio channels, no scheduler queues, no loop machinery).
59///
60/// Exposes methods that operator-generated code calls on both
61/// `df` (for prologues: `request_task`) and
62/// `context` (for iterators: `current_tick`, `schedule_subgraph`, etc.).
63///
64/// # Metrics typestate
65///
66/// The `M` type parameter is a [`MetricsState`] typestate which statically records whether this
67/// context tracks runtime metrics:
68///
69/// - `Context<DfirMetrics>` (the default) is created via [`Context::new_with_metrics`] and holds
70///   a shared `Rc<DfirMetrics>` handle, accessible via [`Context::metrics`].
71/// - `Context<()>` is created via [`Context::new`] and holds no metrics state whatsoever. The
72///   `metrics()` accessor does not exist on it, so it is a compile error to try to read metrics
73///   from a context which never tracked them.
74pub struct Context<M: MetricsState = DfirMetrics> {
75    /// Counter for number of ticks run.
76    current_tick: TickInstant,
77    /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
78    /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
79    wake_state: Arc<WakeState>,
80    /// Live-updating DFIR runtime metrics via interior mutability (`Rc<DfirMetrics>`), or `()`
81    /// when metrics are not tracked.
82    metrics: M::Handle,
83    /// Tasks buffered via [`Self::request_task`], spawned via [`tokio::spawn`] by
84    /// [`Dfir::spawn_tasks`] once the runtime is running inside a tokio runtime.
85    ///
86    /// Futures are required to be `Send` so that `Context` (and therefore [`Dfir`]) can be moved
87    /// between threads before being run, and so that the tasks can be spawned onto any tokio
88    /// runtime (no `LocalSet` required).
89    #[cfg(feature = "tokio")]
90    tasks_to_spawn: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
91}
92
93impl Context<()> {
94    /// Create a new inline context with a fresh wake state and **no** metrics tracking.
95    ///
96    /// The returned `Context<()>` statically contains no metrics state. Use
97    /// [`Context::new_with_metrics`] to create a `Context<DfirMetrics>` which tracks metrics.
98    pub fn new() -> Self {
99        Self::with_metrics_handle(())
100    }
101}
102
103impl Default for Context<()> {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl Context<DfirMetrics> {
110    /// Create a new inline context with a fresh wake state and the given (pre-populated) metrics.
111    pub fn new_with_metrics(metrics: DfirMetrics) -> Self {
112        Self::with_metrics_handle(Rc::new(metrics))
113    }
114
115    /// Returns a reference to the runtime metrics.
116    pub fn metrics(&self) -> &Rc<DfirMetrics> {
117        &self.metrics
118    }
119}
120
121impl<M: MetricsState> Context<M> {
122    fn with_metrics_handle(metrics: M::Handle) -> Self {
123        Self {
124            current_tick: TickInstant::default(),
125            wake_state: Arc::default(),
126            metrics,
127            #[cfg(feature = "tokio")]
128            tasks_to_spawn: Vec::new(),
129        }
130    }
131
132    // --- Methods called as `df.xxx()` in operator prologues ---
133
134    /// Buffers an async task to be spawned later by `Dfir::spawn_tasks`.
135    ///
136    /// Tasks are deferred because `write_prologue` runs during graph construction,
137    /// which may occur before a tokio runtime is entered. Buffered tasks are
138    /// drained and spawned via [`tokio::spawn`] at the start of
139    /// [`Dfir::run_tick`]. Tasks requested after that point remain buffered until
140    /// the next call to [`Dfir::run_tick`].
141    ///
142    /// The future must be `Send` so that the `Context` remains `Send` and so the task can be
143    /// spawned onto any tokio runtime.
144    #[cfg(feature = "tokio")]
145    pub fn request_task<Fut>(&mut self, future: Fut)
146    where
147        Fut: Future<Output = ()> + Send + 'static,
148    {
149        self.tasks_to_spawn.push(Box::pin(future));
150    }
151
152    // --- Methods called as `context.xxx()` in operator iterators ---
153
154    /// Gets the current tick count.
155    pub fn current_tick(&self) -> TickInstant {
156        self.current_tick
157    }
158
159    /// Signals that external data has arrived and a new tick should be started.
160    pub fn schedule_subgraph(&self, is_external: bool) {
161        if is_external {
162            self.wake_state.wake_by_ref();
163        }
164    }
165
166    /// Returns a waker that signals external data has arrived.
167    pub fn waker(&self) -> std::task::Waker {
168        std::task::Waker::from(self.wake_state.clone())
169    }
170
171    /// Increments the tick counter.
172    /// Called by the generated tick closure at the end of each tick.
173    #[doc(hidden)]
174    pub fn __end_tick(&mut self) {
175        self.current_tick += crate::scheduled::ticks::TickDuration::SINGLE_TICK;
176    }
177}
178
179/// An executable DFIR dataflow, as created by
180/// [`dfir_syntax!`](crate::dfir_syntax). Provides the [`Self::run`],
181/// [`Self::run_available`], and [`Self::run_tick`] family of methods to
182/// execute the dataflow.
183///
184/// # Design
185///
186/// The inline codegen generates an `async move |df: &mut Context<M>|` closure that captures
187/// dataflow-specific state (handoff buffers, source iterators) and receives the [`Context`]
188/// (tick counter, metrics) by reference each tick. `Dfir` owns both the
189/// closure and the context, and coordinates tick lifecycle and idle/wake behavior.
190///
191/// We use a single opaque closure rather than generating a bespoke struct per dataflow because:
192/// - The closure naturally captures exactly the state it needs with correct lifetimes
193/// - No codegen needed for struct definitions, field accessors, or initialization
194/// - Rust's async closure machinery handles the complex state machine (suspend/resume across
195///   `.await` points) that would be very difficult to replicate in a generated struct
196///
197/// The `Tick` type parameter is bounded by [`TickClosure`] (not `AsyncFnMut` directly) to
198/// support type erasure via [`TickClosureErased`] / [`DfirErased`] for heterogeneous
199/// collections (e.g., the sim runtime storing multiple locations in a `Vec`). The concrete
200/// (non-erased) path used by trybuild and embedded has zero overhead.
201///
202/// # Metrics
203///
204/// Whether this `Dfir` tracks runtime metrics is determined statically by
205/// [`TickClosure::Metrics`] (see [`MetricsState`]). The [`Self::metrics`] and
206/// [`Self::metrics_intervals`] accessors only exist when `Tick::Metrics = DfirMetrics`. When
207/// returning a `Dfir` from a function via `impl TickClosure`, use
208/// `Dfir<impl TickClosure<Metrics = DfirMetrics>>` if callers need to access the metrics.
209pub struct Dfir<Tick: TickClosure> {
210    /// Async closure which runs a single tick when called.
211    tick_closure: Tick,
212    /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
213    /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
214    wake_state: Arc<WakeState>,
215    /// The inline context, owned by `Dfir` and passed to the tick closure by reference.
216    context: Context<Tick::Metrics>,
217    /// See [`Self::meta_graph()`].
218    ///
219    /// Stored as the serialized JSON string rather than a deserialized [`DfirGraph`], since
220    /// `DfirGraph` contains `proc_macro2` types which are `!Send`. Deserialized on demand.
221    #[cfg(feature = "meta")]
222    meta_graph_json: Option<String>,
223    /// See [`Self::diagnostics()`].
224    #[cfg(feature = "meta")]
225    diagnostics: Option<Vec<Diagnostic<SerdeSpan>>>,
226}
227
228/// Trait for tick closures — abstracts over both concrete async closures (wrapped in
229/// [`TickClosureFn`]) and type-erased boxed versions ([`TickClosureErased`]).
230///
231/// The `&mut Context` parameter is owned by [`Dfir`] and lent to the
232/// closure each tick, avoiding shared-ownership overhead for the context.
233///
234/// The [`Self::Metrics`] associated type carries the [`MetricsState`] typestate of the context the
235/// closure expects, so that [`Dfir<Tick>`] can be parameterized by the closure alone.
236pub trait TickClosure {
237    /// Whether the context passed to this closure tracks metrics. See [`MetricsState`].
238    type Metrics: MetricsState;
239
240    /// Call the tick closure. Returns `true` if any subgraph received input data.
241    #[doc(hidden)]
242    fn call_tick<'a>(
243        &'a mut self,
244        ctx: &'a mut Context<Self::Metrics>,
245    ) -> impl Future<Output = bool> + 'a;
246}
247
248/// Wraps an `AsyncFnMut(&mut Context<M>) -> bool` closure as a [`TickClosure`].
249///
250/// A blanket `impl<M, F: AsyncFnMut(&mut Context<M>)> TickClosure for F` is not allowed
251/// since `M` would only be constrained via the `AsyncFnMut` bound (E0207), so the generated code
252/// wraps the tick closure in this newtype which carries `M` explicitly.
253#[doc(hidden)]
254pub struct TickClosureFn<M, F> {
255    func: F,
256    _metrics: PhantomData<fn() -> M>,
257}
258
259impl<M, F> TickClosureFn<M, F>
260where
261    M: MetricsState,
262    F: for<'a> AsyncFnMut(&'a mut Context<M>) -> bool,
263{
264    /// Wrap the given async tick closure.
265    pub fn new(func: F) -> Self {
266        Self {
267            func,
268            _metrics: PhantomData,
269        }
270    }
271}
272
273impl<M, F> TickClosure for TickClosureFn<M, F>
274where
275    M: MetricsState,
276    F: for<'a> AsyncFnMut(&'a mut Context<M>) -> bool,
277{
278    type Metrics = M;
279
280    fn call_tick<'a>(&'a mut self, ctx: &'a mut Context<M>) -> impl Future<Output = bool> + 'a {
281        (self.func)(ctx)
282    }
283}
284
285/// No-op `TickClosure`. Used by `dfir_syntax!` as a stand-in when codegen fails, so its
286/// `Metrics` matches the macro's default (metrics tracking enabled).
287#[doc(hidden)]
288pub struct NullTickClosure;
289
290impl TickClosure for NullTickClosure {
291    type Metrics = DfirMetrics;
292
293    fn call_tick<'a>(
294        &'a mut self,
295        _ctx: &'a mut Context<DfirMetrics>,
296    ) -> impl Future<Output = bool> + 'a {
297        std::future::ready(false)
298    }
299}
300
301/// Type-erased tick function for use in heterogeneous collections (e.g., the sim runtime).
302#[doc(hidden)]
303pub struct TickClosureErased<M: MetricsState = DfirMetrics>(Box<dyn TickClosureErasedInner<M>>);
304
305/// Object-safe inner trait for [`TickClosureErased`]. Needed because [`TickClosure::call_tick`]
306/// returns `impl Future` (not object-safe), but a trait with `&mut self -> Pin<Box<dyn Future + '_>>`
307/// is — the returned future borrows from the trait object which owns the closure.
308trait TickClosureErasedInner<M: MetricsState> {
309    fn call_tick<'a>(
310        &'a mut self,
311        ctx: &'a mut Context<M>,
312    ) -> Pin<Box<dyn Future<Output = bool> + 'a>>;
313}
314
315impl<Tick: TickClosure> TickClosureErasedInner<Tick::Metrics> for Tick {
316    fn call_tick<'a>(
317        &'a mut self,
318        ctx: &'a mut Context<Tick::Metrics>,
319    ) -> Pin<Box<dyn Future<Output = bool> + 'a>> {
320        Box::pin(TickClosure::call_tick(self, ctx))
321    }
322}
323
324impl<M: MetricsState> TickClosure for TickClosureErased<M> {
325    type Metrics = M;
326
327    fn call_tick<'a>(&'a mut self, ctx: &'a mut Context<M>) -> impl Future<Output = bool> + 'a {
328        self.0.call_tick(ctx)
329    }
330}
331
332/// Type alias for a type-erased [`Dfir`] that can be stored in heterogeneous collections.
333/// Created via [`Dfir::into_erased`].
334///
335/// `M` is the [`MetricsState`] of the erased dataflow: `DfirMetrics` (the default) if metrics
336/// are tracked, or `()` if not.
337pub type DfirErased<M = DfirMetrics> = Dfir<TickClosureErased<M>>;
338
339impl<Tick: TickClosure> Dfir<Tick> {
340    /// Create a new `Dfir` from a tick closure, inline context,
341    /// and meta graph / diagnostics JSON strings.
342    #[doc(hidden)]
343    pub fn new(
344        tick_closure: Tick,
345        context: Context<Tick::Metrics>,
346        meta_graph_json: Option<&str>,
347        diagnostics_json: Option<&str>,
348    ) -> Self {
349        #[cfg(not(feature = "meta"))]
350        let _ = (meta_graph_json, diagnostics_json);
351        Self {
352            tick_closure,
353            wake_state: context.wake_state.clone(),
354            context,
355            #[cfg(feature = "meta")]
356            meta_graph_json: meta_graph_json.map(str::to_owned),
357            #[cfg(feature = "meta")]
358            diagnostics: diagnostics_json.map(|json| {
359                serde_json::from_str(json).expect("Failed to deserialize diagnostics.")
360            }),
361        }
362    }
363
364    /// Returns the meta graph, if set.
365    ///
366    /// The graph is deserialized from its JSON representation on each call, so callers which need
367    /// it multiple times should store the result.
368    #[cfg(feature = "meta")]
369    #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
370    pub fn meta_graph(&self) -> Option<DfirGraph> {
371        self.meta_graph_json.as_deref().map(|json| {
372            let mut meta_graph: DfirGraph =
373                serde_json::from_str(json).expect("Failed to deserialize graph.");
374            let mut op_inst_diagnostics = Diagnostics::new();
375            meta_graph.insert_node_op_insts_all(&mut op_inst_diagnostics);
376            assert!(
377                op_inst_diagnostics.is_empty(),
378                "Expected no diagnostics, got: {:#?}",
379                op_inst_diagnostics
380            );
381            meta_graph
382        })
383    }
384
385    /// Returns any diagnostics generated by the surface syntax macro.
386    #[cfg(feature = "meta")]
387    #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
388    pub fn diagnostics(&self) -> Option<&[Diagnostic<SerdeSpan>]> {
389        self.diagnostics.as_deref()
390    }
391
392    /// Gets the current tick (local time) count.
393    pub fn current_tick(&self) -> TickInstant {
394        self.context.current_tick()
395    }
396}
397
398/// Metrics accessors, only available when metrics tracking is enabled (`Tick::Metrics = DfirMetrics`).
399impl<Tick: TickClosure<Metrics = DfirMetrics>> Dfir<Tick> {
400    /// Returns a reference-counted handle to the continually-updated runtime metrics for this DFIR instance.
401    ///
402    /// Only available when metrics tracking is enabled, see [`MetricsState`].
403    pub fn metrics(&self) -> Rc<DfirMetrics> {
404        Rc::clone(self.context.metrics())
405    }
406
407    /// Returns a [`DfirMetricsIntervals`] handle where each call to
408    /// [`DfirMetricsIntervals::take_interval`] ends the current interval and returns its metrics.
409    ///
410    /// The first call to `take_interval` returns metrics since this DFIR instance was created. Each subsequent call to
411    /// `take_interval` returns metrics since the previous call.
412    ///
413    /// Cloning the handle "forks" it from the original, as afterwards each interval may return different metrics
414    /// depending on when exactly `take_interval` is called.
415    ///
416    /// Only available when metrics tracking is enabled, see [`MetricsState`].
417    pub fn metrics_intervals(&self) -> DfirMetricsIntervals {
418        DfirMetricsIntervals {
419            curr: self.metrics(),
420            prev: None,
421        }
422    }
423}
424
425impl<Tick: TickClosure> Dfir<Tick> {
426    /// Spawns all tasks buffered via [`Context::request_task`] onto the current tokio runtime.
427    ///
428    /// This drains the buffer, so subsequent calls are no-ops until new tasks are requested.
429    ///
430    /// # Panics
431    ///
432    /// Panics if there are buffered tasks and this is called outside of a tokio runtime.
433    #[cfg(feature = "tokio")]
434    fn spawn_tasks(&mut self) {
435        for task in self.context.tasks_to_spawn.drain(..) {
436            tokio::spawn(task);
437        }
438    }
439
440    /// Run a single tick. Returns `true` if any subgraph received input data.
441    ///
442    /// Checks both handoff buffers (via `work_done` flag set in generated recv port code)
443    /// and external events (via `can_start_tick` set by `waker`/`schedule_subgraph`).
444    pub async fn run_tick(&mut self) -> bool {
445        #[cfg(feature = "tokio")]
446        self.spawn_tasks();
447        let had_external = self
448            .wake_state
449            .can_start_tick
450            .swap(false, Ordering::Relaxed);
451        let tick_had_work = self.tick_closure.call_tick(&mut self.context).await;
452        had_external || tick_had_work || self.wake_state.can_start_tick.load(Ordering::Relaxed)
453    }
454
455    /// Run a single tick synchronously. Panics if the tick yields (async suspension).
456    /// Returns `true` if work was done (see [`Self::run_tick`]).
457    pub fn run_tick_sync(&mut self) -> bool {
458        let mut fut = std::pin::pin!(self.run_tick());
459        let mut ctx = std::task::Context::from_waker(std::task::Waker::noop());
460        match fut.as_mut().poll(&mut ctx) {
461            std::task::Poll::Ready(result) => result,
462            std::task::Poll::Pending => {
463                panic!("Dfir::run_tick_sync: tick yielded asynchronously.")
464            }
465        }
466    }
467
468    /// Run ticks as long as work is available, then return.
469    #[cfg(feature = "tokio")]
470    pub async fn run_available(&mut self) {
471        // Always run at least one tick.
472        self.wake_state
473            .can_start_tick
474            .store(false, Ordering::Relaxed);
475        loop {
476            self.run_tick().await;
477            let can_start_tick = self
478                .wake_state
479                .can_start_tick
480                .swap(false, Ordering::Relaxed);
481            if !can_start_tick {
482                break;
483            }
484            // Yield between each tick to receive more events.
485            tokio::task::yield_now().await;
486        }
487    }
488
489    /// [`Self::run_available`] but panics if any tick yields asynchronously.
490    pub fn run_available_sync(&mut self) {
491        self.wake_state
492            .can_start_tick
493            .store(false, Ordering::Relaxed);
494        loop {
495            self.run_tick_sync();
496            let can_start_tick = self
497                .wake_state
498                .can_start_tick
499                .swap(false, Ordering::Relaxed);
500            if !can_start_tick {
501                break;
502            }
503        }
504    }
505
506    /// Run forever, processing ticks when work is available and yielding when idle.
507    #[cfg(feature = "tokio")]
508    pub async fn run(&mut self) -> crate::Never {
509        loop {
510            self.run_available().await;
511            // Wait for an external event to wake us.
512            std::future::poll_fn(|cx| {
513                // Register waker first to avoid race: if an event fires between
514                // the check and the register, the waker is already in place.
515                self.wake_state.task_waker.register(cx.waker());
516                if self.wake_state.can_start_tick.load(Ordering::Relaxed) {
517                    std::task::Poll::Ready(())
518                } else {
519                    std::task::Poll::Pending
520                }
521            })
522            .await;
523        }
524    }
525}
526
527impl<Tick: TickClosure + 'static> Dfir<Tick> {
528    /// Type-erase the tick closure for use in heterogeneous collections.
529    ///
530    /// Wraps the concrete tick closure in [`TickClosureErased`], which boxes the future
531    /// returned by each tick call. This adds one heap allocation per tick, but enables
532    /// storing multiple `Dfir`s with different closure types in a single `Vec`.
533    ///
534    /// The [`MetricsState`] typestate is preserved: the result is `DfirErased<Tick::Metrics>`.
535    ///
536    /// Only needed for the sim runtime path. The trybuild and embedded paths keep the
537    /// concrete type and pay no erasure cost.
538    pub fn into_erased(self) -> DfirErased<Tick::Metrics> {
539        Dfir {
540            tick_closure: TickClosureErased(Box::new(self.tick_closure)),
541            wake_state: self.wake_state,
542            context: self.context,
543            #[cfg(feature = "meta")]
544            meta_graph_json: self.meta_graph_json,
545            #[cfg(feature = "meta")]
546            diagnostics: self.diagnostics,
547        }
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use std::future::ready;
554
555    use static_assertions::assert_impl_all;
556
557    use super::{Context, Dfir, TickClosure};
558
559    struct SendTick;
560
561    impl TickClosure for SendTick {
562        type Metrics = ();
563
564        fn call_tick<'a>(
565            &'a mut self,
566            _ctx: &'a mut Context<()>,
567        ) -> impl Future<Output = bool> + 'a {
568            ready(false)
569        }
570    }
571
572    assert_impl_all!(Context<()>: Send);
573    assert_impl_all!(Dfir<SendTick>: Send);
574}