Skip to main content

dataflow_rs/run/
engine.rs

1//! The engine: a [`Program`] and its config, run over messages.
2
3use super::RunStatus;
4use super::builder::EngineBuilder;
5use super::cancel::{Limits, timers_available};
6use super::error_context::ErrorContext;
7use super::interp::{CaptureMode, Exit, Frame, RunEnv, Watch, run_workflow};
8use super::message::{Message, finish_audit, keeps_entries};
9use super::observers::Observers;
10use super::options::RunOptions;
11use super::record;
12use super::report::{Record, Run};
13use super::trace::Capture;
14use crate::def::Rollout;
15use crate::expr::Path;
16use crate::observe::{
17    Event, Observer, Selection, StepResult, TraceStep, WorkflowExit, WorkflowRecord,
18};
19use crate::program::{Compiler, Program};
20use crate::state::{RunContext, Tuning};
21use chrono::Utc;
22use datavalue::OwnedDataValue;
23use std::fmt;
24use std::sync::Arc;
25
26/// What an [`Engine`] is configured with beside its program. A reload,
27/// [`Engine::with_program`], keeps it.
28#[derive(Clone, Default)]
29#[non_exhaustive]
30pub struct EngineConfig {
31    /// The observer every run of the engine reports to, beside a run's own
32    /// ([`RunOptions::observer`](RunOptions::observer())). `None` by
33    /// default.
34    pub observer: Option<Arc<dyn Observer>>,
35    /// Where the records of each call that added any are mirrored in the
36    /// context, and how many are kept. `None` by default: nothing is
37    /// mirrored, and nothing is reset at run start.
38    pub error_context: Option<ErrorContext>,
39}
40
41impl EngineConfig {
42    /// Reports every run of the engine to `o`.
43    #[must_use]
44    pub fn observer(mut self, o: Arc<dyn Observer>) -> Self {
45        self.observer = Some(o);
46        self
47    }
48
49    /// Mirrors every run's failure records by `ec`.
50    #[must_use]
51    pub fn error_context(mut self, ec: ErrorContext) -> Self {
52        self.error_context = Some(ec);
53        self
54    }
55}
56
57impl fmt::Debug for EngineConfig {
58    /// The observer as a presence, and the error context:
59    /// `EngineConfig { observer: present, error_context: None, .. }`.
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let observer = if self.observer.is_some() {
62            "present"
63        } else {
64            "absent"
65        };
66        f.debug_struct("EngineConfig")
67            .field("observer", &format_args!("{observer}"))
68            .field("error_context", &self.error_context)
69            .finish_non_exhaustive()
70    }
71}
72
73/// A program and its config: what runs messages.
74///
75/// An `Arc` inside, so cloning is cheap and a clone runs the same program.
76/// [`with_program`](Self::with_program) is a reload: the engine it returns
77/// runs the new program, and this one, with any run in flight, keeps the old.
78///
79/// ```
80/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
81/// use dataflow_rs::datavalue::OwnedDataValue;
82/// use dataflow_rs::{Engine, Message};
83/// use serde_json::json;
84///
85/// let engine = Engine::builder()
86///     .workflow_json(&json!({"id": "w", "tasks": [
87///         {"id": "t", "function": {"name": "map", "input": {
88///             "mappings": [{"path": "data.greeting", "logic": "hello"}]}}}
89///     ]}))
90///     .build()
91///     .unwrap();
92/// let mut message = Message::from_json(&json!({}));
93/// let run = engine.run(&mut message).await;
94/// assert_eq!(run.matched(), 1);
95/// let greeting = OwnedDataValue::from(&json!({"greeting": "hello"}));
96/// assert_eq!(message.context().data(), &greeting);
97/// # });
98/// ```
99#[derive(Clone)]
100pub struct Engine {
101    inner: Arc<Inner>,
102}
103
104struct Inner {
105    program: Program,
106    config: EngineConfig,
107}
108
109/// The `metadata` keys the engine writes on every run: the progress after
110/// every call, which cross-workflow chaining reads, and the run's stamps,
111/// written before any op.
112pub(crate) mod slots {
113    pub(crate) const PROGRESS: &str = "progress";
114    pub(crate) const PROCESSED_AT: &str = "processed_at";
115    pub(crate) const ENGINE_VERSION: &str = "engine_version";
116    pub(crate) const CHANNEL: &str = "channel";
117    /// Every one of them.
118    pub(crate) const ALL: [&str; 4] = [PROGRESS, PROCESSED_AT, ENGINE_VERSION, CHANNEL];
119}
120
121/// `metadata.<key>`.
122pub(crate) fn metadata(key: &str) -> Path {
123    format!("metadata.{key}")
124        .parse()
125        .expect("a metadata key is a path")
126}
127
128impl Engine {
129    /// An engine running `program` with `cfg`.
130    pub fn new(program: Program, cfg: EngineConfig) -> Self {
131        Self {
132            inner: Arc::new(Inner {
133                program,
134                config: cfg,
135            }),
136        }
137    }
138
139    /// A builder starting from the default registry, for a host that does
140    /// not quarantine: its `build` fails if any workflow is rejected.
141    pub fn builder() -> EngineBuilder {
142        EngineBuilder::new()
143    }
144
145    /// Runs `msg` with [`RunOptions::default`].
146    pub async fn run(&self, msg: &mut Message) -> Run {
147        // Borrowed for the whole run: a temporary would make the run's
148        // future carry the options.
149        static DEFAULT: RunOptions<'static> = RunOptions::DEFAULT;
150        self.run_with(msg, &DEFAULT).await
151    }
152
153    /// Runs `msg` through the program's Active workflows, those of
154    /// `opts.channel` when it names one, in priority order.
155    ///
156    /// The run reads the clock once: `metadata.processed_at`, every audit
157    /// entry and every error record carry that reading. Only an observer
158    /// asking for [`TIMINGS`](crate::observe::Interest::TIMINGS), or a trace
159    /// asking for [`timings`](crate::observe::TraceOptions::timings()), has the
160    /// clock read again, for the elapsed times it is given. It writes
161    /// `metadata.engine_version`, and `metadata.channel` under a channel,
162    /// before any workflow runs, and then, with an [`ErrorContext`]
163    /// configured, removes its path's key: the records there are the run's
164    /// own. A workflow whose rollout does not admit `opts.routing_bucket` is
165    /// skipped; no bucket is admitted by every rollout. A workflow that stops
166    /// the run ends it, and the [`Run`] says how:
167    /// [`TimedOut`](RunStatus::TimedOut) once `opts.deadline` passed,
168    /// [`Cancelled`](RunStatus::Cancelled) once `opts.cancel` was cancelled,
169    /// and otherwise [`Stopped`](RunStatus::Stopped) with the error that
170    /// stopped it, whatever its kind: a child run's timeout that a function
171    /// returns before this run's own deadline passed is its task's failure,
172    /// not this run's. An `Err` a function returns once `opts.deadline` has
173    /// passed, or `opts.cancel` was cancelled, is the run's limit's.
174    ///
175    /// With [`opts.trace`](RunOptions::trace()) set, the [`Run`] carries the
176    /// run's [`Trace`](crate::observe::Trace): the context after the stamps
177    /// and the reset, then a step for every recorded call, skipped call or
178    /// workflow and sweep, the step that stopped the run included: the call
179    /// a limit cut short, or, for a stop as a sweep starts, the engine's
180    /// step for the counter it wrote. Its replay is the message's context on
181    /// every status.
182    pub async fn run_with(&self, msg: &mut Message, opts: &RunOptions<'_>) -> Run {
183        let program = &self.inner.program;
184        // Built in a block: a local the run borrowed would stay in its
185        // future beside the `Watch` it moved into.
186        let mut watch = {
187            let observers = Observers::attach(self.inner.config.observer.as_deref(), opts.observer);
188            Watch {
189                timed: observers.timed_tasks(),
190                observers,
191                trace: None,
192                error_context: self.inner.config.error_context.as_ref(),
193            }
194        };
195        let started = watch.observers.timed().then(record::now);
196        let now = Utc::now();
197        let now_str = now.to_rfc3339();
198        let mut now_text: Option<Arc<str>> = None;
199        let parts = msg.parts();
200        stamp(parts.context, now_str, opts.channel);
201        // A message brings no records in at the error context's path.
202        if let Some(ec) = &self.inner.config.error_context {
203            ec.reset(parts.context);
204        }
205        // `initial` is the context as the stamps and the reset left it.
206        if let Some(options) = &opts.trace {
207            let trace = Capture::new(options.clone(), parts.context);
208            watch.timed |= trace.timings();
209            watch.trace = Some(Box::new(trace));
210        }
211
212        let mut rc = RunContext::new(parts.context, Tuning::default());
213        let active = program.active(opts.channel);
214        watch.observers.emit(Event::RunStarted {
215            program: program.id(),
216            message_id: parts.id,
217            channel: opts.channel,
218            considered: active.len(),
219        });
220        // A call's changes are worth recording only in a mode that keeps
221        // an entry for them.
222        let capture_audit = opts.capture_changes && keeps_entries(opts.audit);
223        let mut records = Vec::with_capacity(active.len());
224        // Every 64th op of the run checks the limits, across workflows.
225        let mut ops = 0;
226        // Whether this run's runtime can race a deadline with a timer: the
227        // runtime context is the run's, so the answer is the run's.
228        let timers = opts.deadline.is_some() && timers_available();
229        // A bucket above 99 is served as 99, as in 3.x: the bucket space is
230        // `0..100`, and `[x, 100)` is the range that serves the rest.
231        let bucket = opts.routing_bucket.map(|b| b.min(Rollout::MAX_BUCKET));
232        // How the run ended, once a workflow stopped it: the loop's value,
233        // so the run's future holds no status across its workflows.
234        let stopped = 'run: {
235            for &ix in active {
236                let wf = program.compiled(ix);
237                if let (Some(rollout), Some(bucket)) = (wf.rollout, bucket)
238                    && !rollout.accepts(bucket)
239                {
240                    if let Some(trace) = watch.trace.as_deref_mut() {
241                        let (workflow_id, _) = program.ids(ix);
242                        trace.push(TraceStep::new(ix, workflow_id, StepResult::Skipped));
243                    }
244                    records.push(Record {
245                        ix,
246                        selection: Selection::SkippedByRollout,
247                        sweeps: 0,
248                        exit: None,
249                    });
250                    continue;
251                }
252                let (workflow_id, task_ids) = program.ids(ix);
253                let mut env = RunEnv {
254                    program: program.id(),
255                    workflow_ix: ix,
256                    task_base: program.task_base(ix),
257                    channel: opts.channel,
258                    routing_bucket: bucket,
259                    layers: opts.layers,
260                    limits: Limits {
261                        deadline: opts.deadline,
262                        cancel: opts.cancel,
263                        timers,
264                    },
265                    ops,
266                    exprs: program.registry().exprs(),
267                    payload: parts.payload,
268                    errors: &mut *parts.errors,
269                    audit: &mut *parts.audit,
270                    mode: opts.audit,
271                    capture: CaptureMode::new(capture_audit, &watch),
272                    watch: &mut watch,
273                    now,
274                    now_text: &mut now_text,
275                    changes: Vec::new(),
276                    workflow_id,
277                    task_ids,
278                    failed: false,
279                    frame: Frame::default(),
280                };
281                let (exit, selection) = run_workflow(&mut rc, wf, &mut env).await;
282                ops = env.ops;
283                let ran = matches!(selection, Selection::Ran);
284                let (exit, stop) = match exit {
285                    Exit::Completed => (WorkflowExit::Completed, None),
286                    Exit::Halted => (WorkflowExit::Halted, None),
287                    Exit::Failed => (WorkflowExit::Failed, None),
288                    Exit::Stopped(e) => (WorkflowExit::Stopped, Some(RunStatus::Stopped(e))),
289                    // Only the run's own limits make it `TimedOut` or
290                    // `Cancelled`: a task's `Err` of either kind, a child run's
291                    // propagated, stops it as any failure does, unless one of
292                    // them had passed when the call returned it.
293                    Exit::Limited(stop) => (WorkflowExit::Stopped, Some(stop.status())),
294                };
295                let considered = Record {
296                    ix,
297                    selection,
298                    sweeps: env.frame.sweeps,
299                    exit: ran.then_some(exit),
300                };
301                let workflow_started = env.frame.started;
302                if ran && watch.observers.workflows() {
303                    let view = WorkflowRecord {
304                        id: workflow_id,
305                        index: ix,
306                        selection: &considered.selection,
307                        sweeps: considered.sweeps,
308                        exit: considered.exit,
309                    };
310                    watch.observers.emit(Event::WorkflowFinished {
311                        record: &view,
312                        elapsed: workflow_started.map(record::since),
313                    });
314                }
315                records.push(considered);
316                if let Some(status) = stop {
317                    break 'run Some(status);
318                }
319            }
320            None
321        };
322        let stats = rc.finish();
323        finish_audit(parts.audit, opts.audit);
324        let status = match stopped {
325            Some(status) => status,
326            None if !parts.errors.is_empty() => RunStatus::CompletedWithErrors,
327            None => RunStatus::Completed,
328        };
329        watch.observers.emit(Event::RunFinished {
330            message_id: parts.id,
331            status: &status,
332            errors: parts.errors.len(),
333            elapsed: started.map(record::since),
334        });
335        Run::new(
336            program.clone(),
337            status,
338            records,
339            stats,
340            watch.trace.map(|t| Box::new(t.finish())),
341        )
342    }
343
344    /// A compiler with the program's registry and options, whose workflows
345    /// link into a program this engine can run.
346    pub fn compiler(&self) -> Compiler {
347        let program = &self.inner.program;
348        program.registry().compiler(program.options().clone())
349    }
350
351    /// An engine running `program` with this engine's config: a reload. This
352    /// engine is unchanged.
353    #[must_use]
354    pub fn with_program(&self, program: Program) -> Self {
355        Self::new(program, self.inner.config.clone())
356    }
357
358    /// The program this engine runs.
359    pub fn program(&self) -> &Program {
360        &self.inner.program
361    }
362
363    /// The config this engine runs with.
364    #[doc(hidden)]
365    pub fn config(&self) -> &EngineConfig {
366        &self.inner.config
367    }
368}
369
370/// Writes the run's stamps, owned: no view exists yet. Each replaces its key
371/// in place or is appended, as v3's `set_processing_metadata`.
372fn stamp(context: &mut OwnedDataValue, now: String, channel: Option<&str>) {
373    let OwnedDataValue::Object(roots) = context else {
374        return;
375    };
376    let Some((_, OwnedDataValue::Object(meta))) = roots.iter_mut().find(|(k, _)| k == "metadata")
377    else {
378        return;
379    };
380    let need = 2 + usize::from(channel.is_some());
381    if meta.capacity() < meta.len() + need {
382        meta.reserve(need);
383    }
384    match meta.iter_mut().find(|(k, _)| k == slots::PROCESSED_AT) {
385        Some((_, slot)) => *slot = OwnedDataValue::String(now),
386        None => meta.push((slots::PROCESSED_AT.to_owned(), OwnedDataValue::String(now))),
387    }
388    put_meta_str(meta, slots::ENGINE_VERSION, crate::VERSION);
389    if let Some(channel) = channel {
390        put_meta_str(meta, slots::CHANNEL, channel);
391    }
392}
393
394fn put_meta_str(meta: &mut Vec<(String, OwnedDataValue)>, key: &str, text: &str) {
395    match meta.iter_mut().find(|(k, _)| k == key) {
396        Some((_, OwnedDataValue::String(s))) => {
397            if s != text {
398                s.clear();
399                s.push_str(text);
400            }
401        }
402        Some((_, slot)) => *slot = OwnedDataValue::String(text.to_owned()),
403        None => meta.push((key.to_owned(), OwnedDataValue::String(text.to_owned()))),
404    }
405}
406
407impl fmt::Debug for Engine {
408    /// The program and the config.
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        f.debug_struct("Engine")
411            .field("program", &self.inner.program)
412            .field("config", &self.inner.config)
413            .finish()
414    }
415}