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::pin::Pin;
8use std::rc::Rc;
9use std::sync::Arc;
10use std::sync::atomic::Ordering;
11use std::task::Wake;
12
13#[cfg(feature = "meta")]
14use dfir_lang::diagnostic::{Diagnostic, Diagnostics, SerdeSpan};
15#[cfg(feature = "meta")]
16use dfir_lang::graph::DfirGraph;
17
18use super::metrics::{DfirMetrics, DfirMetricsIntervals};
19use crate::scheduled::ticks::TickInstant;
20
21/// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
22/// (the external runner). Shared via `Arc` between both.
23///
24/// When external data arrives (e.g., a tokio stream receives a message), the [`Context::waker`]
25/// fires, which sets `can_start_tick` and wakes the [`Dfir::run`](Dfir::run) task so it starts a new tick.
26/// Implements [`Wake`] directly so it can be used as a `Waker` without an extra wrapper.
27#[doc(hidden)]
28pub struct WakeState {
29    /// Set to `true` when external data arrives, signaling that a new tick should run.
30    /// Checked by [`Dfir::run_tick`](Dfir::run_tick) and [`Dfir::run_available`](Dfir::run_available).
31    can_start_tick: std::sync::atomic::AtomicBool,
32    /// Wakes the [`Dfir::run`](Dfir::run) task from its idle `poll_fn` sleep.
33    task_waker: futures::task::AtomicWaker,
34}
35
36impl Default for WakeState {
37    fn default() -> Self {
38        Self {
39            can_start_tick: std::sync::atomic::AtomicBool::new(false),
40            task_waker: futures::task::AtomicWaker::new(),
41        }
42    }
43}
44
45impl Wake for WakeState {
46    fn wake(self: Arc<Self>) {
47        self.wake_by_ref();
48    }
49
50    fn wake_by_ref(self: &Arc<Self>) {
51        self.can_start_tick.store(true, Ordering::Relaxed);
52        self.task_waker.wake();
53    }
54}
55
56/// A lightweight context for inline codegen that avoids the overhead of the full
57/// scheduled graph (no tokio channels, no scheduler queues, no loop machinery).
58///
59/// Exposes methods that operator-generated code calls on both
60/// `df` (for prologues: `request_task`) and
61/// `context` (for iterators: `current_tick`, `schedule_subgraph`, etc.).
62#[doc(hidden)]
63#[derive(Default)]
64pub struct Context {
65    /// Counter for number of ticks run.
66    current_tick: TickInstant,
67    /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
68    /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
69    wake_state: Arc<WakeState>,
70    /// Live-updating DFIR runtime metrics via interior mutability.
71    metrics: Rc<DfirMetrics>,
72    /// Tasks buffered via [`Self::request_task`], spawned by [`Dfir::spawn_tasks`]
73    /// once the runtime is running inside a tokio `LocalSet`.
74    #[cfg(feature = "tokio")]
75    tasks_to_spawn: Vec<Pin<Box<dyn Future<Output = ()> + 'static>>>,
76}
77
78impl Context {
79    /// Create a new inline context with shared wake state and metrics.
80    pub fn new(wake_state: Arc<WakeState>, metrics: Rc<DfirMetrics>) -> Self {
81        Self {
82            current_tick: TickInstant::default(),
83            wake_state,
84            metrics,
85            #[cfg(feature = "tokio")]
86            tasks_to_spawn: Vec::new(),
87        }
88    }
89
90    // --- Methods called as `df.xxx()` in operator prologues ---
91
92    /// Buffers an async task to be spawned later by [`Dfir::spawn_tasks`].
93    ///
94    /// Tasks are deferred because `write_prologue` runs during graph construction,
95    /// which may occur before a tokio `LocalSet` is entered. Buffered tasks are
96    /// drained and spawned via `tokio::task::spawn_local` at the start of
97    /// [`Dfir::run_tick`]. Tasks requested after that point remain buffered until
98    /// the next call to [`Dfir::run_tick`].
99    #[cfg(feature = "tokio")]
100    pub fn request_task<Fut>(&mut self, future: Fut)
101    where
102        Fut: Future<Output = ()> + 'static,
103    {
104        self.tasks_to_spawn.push(Box::pin(future));
105    }
106
107    // --- Methods called as `context.xxx()` in operator iterators ---
108
109    /// Gets the current tick count.
110    pub fn current_tick(&self) -> TickInstant {
111        self.current_tick
112    }
113
114    /// Returns a reference to the runtime metrics.
115    pub fn metrics(&self) -> &Rc<DfirMetrics> {
116        &self.metrics
117    }
118
119    /// Signals that external data has arrived and a new tick should be started.
120    pub fn schedule_subgraph(&self, is_external: bool) {
121        if is_external {
122            self.wake_state.wake_by_ref();
123        }
124    }
125
126    /// Returns a waker that signals external data has arrived.
127    pub fn waker(&self) -> std::task::Waker {
128        std::task::Waker::from(self.wake_state.clone())
129    }
130
131    /// Increments the tick counter.
132    /// Called by the generated tick closure at the end of each tick.
133    #[doc(hidden)]
134    pub fn __end_tick(&mut self) {
135        self.current_tick += crate::scheduled::ticks::TickDuration::SINGLE_TICK;
136    }
137}
138
139/// A wrapper around an inline-codegen tick closure that provides [`Self::run`],
140/// [`Self::run_available`], and [`Self::run_tick`] methods — mirroring the [`Dfir`](super::context::Dfir)
141/// API.
142///
143/// # Design
144///
145/// The inline codegen generates an `async move |df: &mut Context|` closure that captures
146/// dataflow-specific state (handoff buffers, source iterators) and receives the [`Context`]
147/// (tick counter, metrics) by reference each tick. `Dfir` owns both the
148/// closure and the context, and coordinates tick lifecycle and idle/wake behavior.
149///
150/// We use a single opaque closure rather than generating a bespoke struct per dataflow because:
151/// - The closure naturally captures exactly the state it needs with correct lifetimes
152/// - No codegen needed for struct definitions, field accessors, or initialization
153/// - Rust's async closure machinery handles the complex state machine (suspend/resume across
154///   `.await` points) that would be very difficult to replicate in a generated struct
155///
156/// The `Tick` type parameter is bounded by [`TickClosure`] (not `AsyncFnMut` directly) to
157/// support type erasure via [`TickClosureErased`] / [`DfirErased`] for heterogeneous
158/// collections (e.g., the sim runtime storing multiple locations in a `Vec`). The concrete
159/// (non-erased) path used by trybuild and embedded has zero overhead.
160#[doc(hidden)]
161pub struct Dfir<Tick> {
162    /// Async closure which runs a single tick when called.
163    tick_closure: Tick,
164    /// Coordinates waking between [`Context`] (inside the tick closure) and [`Dfir`]
165    /// (the external runner). Shared via `Arc` between both. Implements [`Wake`].
166    wake_state: Arc<WakeState>,
167    /// The inline context, owned by `Dfir` and passed to the tick closure by reference.
168    context: Context,
169    /// See [`Self::meta_graph()`].
170    #[cfg(feature = "meta")]
171    meta_graph: Option<DfirGraph>,
172    /// See [`Self::diagnostics()`].
173    #[cfg(feature = "meta")]
174    diagnostics: Option<Vec<Diagnostic<SerdeSpan>>>,
175}
176
177/// Trait for tick closures — abstracts over both concrete async closures
178/// and type-erased boxed versions ([`TickClosureErased`]).
179///
180/// The `&mut Context` parameter is owned by [`Dfir`] and lent to the
181/// closure each tick, avoiding shared-ownership overhead for the context.
182#[doc(hidden)]
183pub trait TickClosure {
184    /// Call the tick closure. Returns `true` if any subgraph received input data.
185    fn call_tick<'a>(&'a mut self, ctx: &'a mut Context) -> impl Future<Output = bool> + 'a;
186}
187
188impl<F: for<'a> AsyncFnMut(&'a mut Context) -> bool> TickClosure for F {
189    fn call_tick<'a>(&'a mut self, ctx: &'a mut Context) -> impl Future<Output = bool> + 'a {
190        self(ctx)
191    }
192}
193
194/// No-op `TickClosure`.
195#[doc(hidden)]
196pub struct NullTickClosure;
197
198impl TickClosure for NullTickClosure {
199    fn call_tick<'a>(&'a mut self, _ctx: &'a mut Context) -> impl Future<Output = bool> + 'a {
200        std::future::ready(false)
201    }
202}
203
204/// Type-erased tick function for use in heterogeneous collections (e.g., the sim runtime).
205#[doc(hidden)]
206pub struct TickClosureErased(Box<dyn TickClosureErasedInner>);
207
208/// Object-safe inner trait for [`TickClosureErased`]. Needed because `AsyncFnMut` is not
209/// object-safe (GAT return type), but a trait with `&mut self -> Pin<Box<dyn Future + '_>>`
210/// is — the returned future borrows from the trait object which owns the closure.
211trait TickClosureErasedInner {
212    fn call_tick<'a>(
213        &'a mut self,
214        ctx: &'a mut Context,
215    ) -> Pin<Box<dyn Future<Output = bool> + 'a>>;
216}
217
218impl<F: for<'a> AsyncFnMut(&'a mut Context) -> bool> TickClosureErasedInner for F {
219    fn call_tick<'a>(
220        &'a mut self,
221        ctx: &'a mut Context,
222    ) -> Pin<Box<dyn Future<Output = bool> + 'a>> {
223        Box::pin(self(ctx))
224    }
225}
226
227impl TickClosure for TickClosureErased {
228    fn call_tick<'a>(&'a mut self, ctx: &'a mut Context) -> impl Future<Output = bool> + 'a {
229        self.0.call_tick(ctx)
230    }
231}
232
233/// Type alias for a type-erased [`Dfir`] that can be stored in heterogeneous collections.
234/// Created via [`Dfir::into_erased`].
235pub type DfirErased = Dfir<TickClosureErased>;
236
237impl<Tick: TickClosure> Dfir<Tick> {
238    /// Create a new `Dfir` from a tick closure, inline context,
239    /// and meta graph / diagnostics JSON strings.
240    #[doc(hidden)]
241    pub fn new(
242        tick_closure: Tick,
243        context: Context,
244        meta_graph_json: Option<&str>,
245        diagnostics_json: Option<&str>,
246    ) -> Self {
247        #[cfg(not(feature = "meta"))]
248        let _ = (meta_graph_json, diagnostics_json);
249        Self {
250            tick_closure,
251            wake_state: context.wake_state.clone(),
252            context,
253            #[cfg(feature = "meta")]
254            meta_graph: meta_graph_json.map(|json| {
255                let mut meta_graph: DfirGraph =
256                    serde_json::from_str(json).expect("Failed to deserialize graph.");
257                let mut op_inst_diagnostics = Diagnostics::new();
258                meta_graph.insert_node_op_insts_all(&mut op_inst_diagnostics);
259                assert!(
260                    op_inst_diagnostics.is_empty(),
261                    "Expected no diagnostics, got: {:#?}",
262                    op_inst_diagnostics
263                );
264                meta_graph
265            }),
266            #[cfg(feature = "meta")]
267            diagnostics: diagnostics_json.map(|json| {
268                serde_json::from_str(json).expect("Failed to deserialize diagnostics.")
269            }),
270        }
271    }
272
273    /// Return a handle to the meta graph, if set.
274    #[cfg(feature = "meta")]
275    #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
276    pub fn meta_graph(&self) -> Option<&DfirGraph> {
277        self.meta_graph.as_ref()
278    }
279
280    /// Returns any diagnostics generated by the surface syntax macro.
281    #[cfg(feature = "meta")]
282    #[cfg_attr(docsrs, doc(cfg(feature = "meta")))]
283    pub fn diagnostics(&self) -> Option<&[Diagnostic<SerdeSpan>]> {
284        self.diagnostics.as_deref()
285    }
286
287    /// Returns a reference-counted handle to the continually-updated runtime metrics for this DFIR instance.
288    pub fn metrics(&self) -> Rc<DfirMetrics> {
289        Rc::clone(self.context.metrics())
290    }
291
292    /// Gets the current tick (local time) count.
293    pub fn current_tick(&self) -> TickInstant {
294        self.context.current_tick()
295    }
296
297    /// Returns a [`DfirMetricsIntervals`] handle where each call to
298    /// [`DfirMetricsIntervals::take_interval`] ends the current interval and returns its metrics.
299    ///
300    /// The first call to `take_interval` returns metrics since this DFIR instance was created. Each subsequent call to
301    /// `take_interval` returns metrics since the previous call.
302    ///
303    /// Cloning the handle "forks" it from the original, as afterwards each interval may return different metrics
304    /// depending on when exactly `take_interval` is called.
305    pub fn metrics_intervals(&self) -> DfirMetricsIntervals {
306        DfirMetricsIntervals {
307            curr: self.metrics(),
308            prev: None,
309        }
310    }
311}
312
313impl<Tick: TickClosure> Dfir<Tick> {
314    /// Spawns all tasks buffered via [`Context::request_task`].
315    ///
316    /// This drains the buffer, so subsequent calls are no-ops until new tasks are requested.
317    #[cfg(feature = "tokio")]
318    fn spawn_tasks(&mut self) {
319        for task in self.context.tasks_to_spawn.drain(..) {
320            tokio::task::spawn_local(task);
321        }
322    }
323
324    /// Run a single tick. Returns `true` if any subgraph received input data.
325    ///
326    /// Checks both handoff buffers (via `work_done` flag set in generated recv port code)
327    /// and external events (via `can_start_tick` set by wakers/schedule_subgraph).
328    pub async fn run_tick(&mut self) -> bool {
329        #[cfg(feature = "tokio")]
330        self.spawn_tasks();
331        let had_external = self
332            .wake_state
333            .can_start_tick
334            .swap(false, Ordering::Relaxed);
335        let tick_had_work = self.tick_closure.call_tick(&mut self.context).await;
336        had_external || tick_had_work || self.wake_state.can_start_tick.load(Ordering::Relaxed)
337    }
338
339    /// Run a single tick synchronously. Panics if the tick yields (async suspension).
340    /// Returns `true` if work was done (see [`Self::run_tick`]).
341    pub fn run_tick_sync(&mut self) -> bool {
342        let mut fut = std::pin::pin!(self.run_tick());
343        let mut ctx = std::task::Context::from_waker(std::task::Waker::noop());
344        match fut.as_mut().poll(&mut ctx) {
345            std::task::Poll::Ready(result) => result,
346            std::task::Poll::Pending => {
347                panic!("Dfir::run_tick_sync: tick yielded asynchronously.")
348            }
349        }
350    }
351
352    /// Run ticks as long as work is available, then return.
353    #[cfg(feature = "tokio")]
354    pub async fn run_available(&mut self) {
355        // Always run at least one tick.
356        self.wake_state
357            .can_start_tick
358            .store(false, Ordering::Relaxed);
359        loop {
360            self.run_tick().await;
361            let can_start_tick = self
362                .wake_state
363                .can_start_tick
364                .swap(false, Ordering::Relaxed);
365            if !can_start_tick {
366                break;
367            }
368            // Yield between each tick to receive more events.
369            tokio::task::yield_now().await;
370        }
371    }
372
373    /// [`Self::run_available`] but panics if any tick yields asynchronously.
374    pub fn run_available_sync(&mut self) {
375        self.wake_state
376            .can_start_tick
377            .store(false, Ordering::Relaxed);
378        loop {
379            self.run_tick_sync();
380            let can_start_tick = self
381                .wake_state
382                .can_start_tick
383                .swap(false, Ordering::Relaxed);
384            if !can_start_tick {
385                break;
386            }
387        }
388    }
389
390    /// Run forever, processing ticks when work is available and yielding when idle.
391    #[cfg(feature = "tokio")]
392    pub async fn run(&mut self) -> crate::Never {
393        loop {
394            self.run_available().await;
395            // Wait for an external event to wake us.
396            std::future::poll_fn(|cx| {
397                // Register waker first to avoid race: if an event fires between
398                // the check and the register, the waker is already in place.
399                self.wake_state.task_waker.register(cx.waker());
400                if self.wake_state.can_start_tick.load(Ordering::Relaxed) {
401                    std::task::Poll::Ready(())
402                } else {
403                    std::task::Poll::Pending
404                }
405            })
406            .await;
407        }
408    }
409}
410
411impl<Tick: 'static + for<'a> AsyncFnMut(&'a mut Context) -> bool> Dfir<Tick> {
412    /// Type-erase the tick closure for use in heterogeneous collections.
413    ///
414    /// Wraps the concrete async closure in [`TickClosureErased`], which boxes the future
415    /// returned by each tick call. This adds one heap allocation per tick, but enables
416    /// storing multiple `Dfir`s with different closure types in a single `Vec`.
417    ///
418    /// Only needed for the sim runtime path. The trybuild and embedded paths keep the
419    /// concrete type and pay no erasure cost.
420    pub fn into_erased(self) -> DfirErased {
421        Dfir {
422            tick_closure: TickClosureErased(Box::new(self.tick_closure)),
423            wake_state: self.wake_state,
424            context: self.context,
425            #[cfg(feature = "meta")]
426            meta_graph: self.meta_graph,
427            #[cfg(feature = "meta")]
428            diagnostics: self.diagnostics,
429        }
430    }
431}