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}