Skip to main content

dataflow_rs/program/
link.rs

1//! Linking: compiled workflows into a [`Program`], the immutable thing the
2//! runtime executes.
3
4use super::compile::{CompileOptions, CompiledWorkflow, Compiler};
5use super::fingerprint::Fingerprint;
6use crate::base::{AuthoredPath, ProgramId, StepKind, TaskIx, WorkflowIx};
7use crate::def::Status;
8use crate::function::Registry;
9use std::collections::HashMap;
10use std::fmt;
11use std::sync::Arc;
12
13impl Compiler {
14    /// Links compiled workflows into a [`Program`]. It never fails.
15    ///
16    /// Workflows sort by `priority`, ascending, with input order breaking
17    /// ties; the sorted position is the [`WorkflowIx`]. Tasks are numbered
18    /// from zero over every workflow's task table in that order, setup first,
19    /// so a workflow's tasks are contiguous. Workflows sharing an id are all
20    /// kept, as v3 keeps them.
21    ///
22    /// # Panics
23    ///
24    /// When a workflow was compiled under another registry: its functions,
25    /// operators and secrets need not be the program's. A registry and its
26    /// clones are one registry, so every compiler made from them links the
27    /// others' workflows, until one of them registers a function or a layer,
28    /// which gives it a table of its own.
29    ///
30    /// ```
31    /// use dataflow_rs::Registry;
32    /// use dataflow_rs::def::{ParseOptions, parse};
33    /// use dataflow_rs::program::CompileOptions;
34    /// use serde_json::json;
35    ///
36    /// let compiler = Registry::default().compiler(CompileOptions::default());
37    /// let json = json!({"id": "w", "tasks": [
38    ///     {"id": "t", "function": {"name": "log", "input": {"message": "hi"}}}
39    /// ]});
40    /// let def = parse(&json, ParseOptions::default()).def.unwrap();
41    /// let program = compiler.link([compiler.compile_one(&def).unwrap().workflow]);
42    /// assert_eq!(program.workflows()[0].id, "w");
43    /// assert_eq!(program.tasks()[0].id, "t");
44    /// ```
45    pub fn link(&self, workflows: impl IntoIterator<Item = CompiledWorkflow>) -> Program {
46        let mut workflows: Vec<CompiledWorkflow> = workflows.into_iter().collect();
47        for w in &workflows {
48            assert!(
49                w.registry.ptr_eq(&self.registry),
50                "cannot link workflow `{}`: it was compiled under a different registry from \
51                 this compiler's",
52                w.id
53            );
54        }
55        workflows.sort_by_key(|w| w.priority);
56        let count =
57            u32::try_from(workflows.len()).expect("a program holds at most u32::MAX workflows");
58
59        let mut infos = Vec::with_capacity(workflows.len());
60        let mut workflow_ids = Vec::with_capacity(workflows.len());
61        let mut tasks = Vec::new();
62        let mut task_ids = Vec::new();
63        let mut task_base = Vec::with_capacity(workflows.len());
64        let mut all = Vec::new();
65        let mut by_channel: HashMap<String, Vec<WorkflowIx>> = HashMap::new();
66        for (ix, w) in (0..count).map(WorkflowIx::new).zip(&workflows) {
67            task_base
68                .push(u32::try_from(tasks.len()).expect("a program holds at most u32::MAX tasks"));
69            workflow_ids.push(Arc::from(w.id.as_str()));
70            infos.push(WorkflowInfo {
71                ix,
72                id: w.id.clone(),
73                channel: w.channel.clone(),
74                priority: w.priority,
75                status: w.status,
76                version: w.version,
77                fingerprint: w.fingerprint,
78            });
79            for meta in &w.tasks {
80                let task = TaskIx::new(
81                    u32::try_from(tasks.len()).expect("a program holds at most u32::MAX tasks"),
82                );
83                task_ids.push(Arc::from(meta.id.as_str()));
84                tasks.push(TaskInfo {
85                    ix: task,
86                    id: meta.id.clone(),
87                    function: meta.function.clone(),
88                    workflow: ix,
89                    kind: meta.kind,
90                    path: meta.path.clone(),
91                });
92            }
93            if w.status == Status::Active {
94                all.push(ix);
95                by_channel.entry(w.channel.clone()).or_default().push(ix);
96            }
97        }
98
99        Program {
100            inner: Arc::new(Inner {
101                id: ProgramId::next(),
102                registry: self.registry.clone(),
103                options: self.options.clone(),
104                workflows,
105                infos,
106                workflow_ids,
107                tasks,
108                task_ids,
109                task_base,
110                all,
111                by_channel,
112            }),
113        }
114    }
115}
116
117struct Inner {
118    id: ProgramId,
119    registry: Registry,
120    /// What the program was compiled with, for [`Engine::compiler`](crate::run::Engine::compiler).
121    options: CompileOptions,
122    workflows: Vec<CompiledWorkflow>,
123    infos: Vec<WorkflowInfo>,
124    /// Each workflow's id, shared by the audit entries a run records.
125    workflow_ids: Vec<Arc<str>>,
126    tasks: Vec<TaskInfo>,
127    /// Each task's id, indexed by [`TaskIx`], shared likewise.
128    task_ids: Vec<Arc<str>>,
129    task_base: Vec<u32>,
130    /// Every Active workflow, in `WorkflowIx` order.
131    all: Vec<WorkflowIx>,
132    /// Each channel's Active workflows, in `WorkflowIx` order.
133    by_channel: HashMap<String, Vec<WorkflowIx>>,
134}
135
136/// Compiled workflows linked together: immutable, with dense indices.
137///
138/// An `Arc` inside, so cloning is cheap and the program is `Send + Sync`.
139/// [`Compiler::link`] makes one.
140#[derive(Clone)]
141pub struct Program {
142    inner: Arc<Inner>,
143}
144
145impl Program {
146    /// This program's identity, distinct from every other link's.
147    pub fn id(&self) -> ProgramId {
148        self.inner.id
149    }
150
151    /// Every linked workflow, indexed by [`WorkflowIx`]: the Paused and
152    /// Archived ones too.
153    pub fn workflows(&self) -> &[WorkflowInfo] {
154        &self.inner.infos
155    }
156
157    /// Every task of every workflow, indexed by [`TaskIx`].
158    pub fn tasks(&self) -> &[TaskInfo] {
159        &self.inner.tasks
160    }
161
162    /// The task at `ix`.
163    ///
164    /// # Panics
165    ///
166    /// When `ix` belongs to another program and is out of range.
167    pub fn task(&self, ix: TaskIx) -> &TaskInfo {
168        &self.inner.tasks[ix.index() as usize]
169    }
170
171    /// The registry the program's functions resolve in.
172    pub fn registry(&self) -> &Registry {
173        &self.inner.registry
174    }
175
176    /// The compiled workflow at `ix`.
177    pub(crate) fn compiled(&self, ix: WorkflowIx) -> &CompiledWorkflow {
178        &self.inner.workflows[ix.index() as usize]
179    }
180
181    /// The Active workflows, in `WorkflowIx` order: those on `channel`, or
182    /// every one for `None`. An unknown channel has none.
183    pub(crate) fn active(&self, channel: Option<&str>) -> &[WorkflowIx] {
184        match channel {
185            None => &self.inner.all,
186            Some(c) => self.inner.by_channel.get(c).map_or(&[], Vec::as_slice),
187        }
188    }
189
190    /// The `TaskIx` of the workflow's first task.
191    pub(crate) fn task_base(&self, ix: WorkflowIx) -> u32 {
192        self.inner.task_base[ix.index() as usize]
193    }
194
195    /// The workflow's id and its tasks' ids, by task row, as the shared
196    /// strings an audit entry holds: cloning one is a count, not a copy.
197    pub(crate) fn ids(&self, ix: WorkflowIx) -> (&Arc<str>, &[Arc<str>]) {
198        let base = self.task_base(ix) as usize;
199        let count = self.compiled(ix).tasks.len();
200        (
201            &self.inner.workflow_ids[ix.index() as usize],
202            &self.inner.task_ids[base..base + count],
203        )
204    }
205
206    /// The options the program was compiled with.
207    pub(crate) fn options(&self) -> &CompileOptions {
208        &self.inner.options
209    }
210}
211
212impl fmt::Debug for Program {
213    /// The id and the workflow ids, in order.
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        f.debug_struct("Program")
216            .field("id", &self.inner.id)
217            .field(
218                "workflows",
219                &self.inner.infos.iter().map(|w| &w.id).collect::<Vec<_>>(),
220            )
221            .finish_non_exhaustive()
222    }
223}
224
225/// What a [`Program`] lists about one workflow.
226#[derive(Clone, Debug)]
227#[non_exhaustive]
228pub struct WorkflowInfo {
229    /// Its position in the program.
230    pub ix: WorkflowIx,
231    /// Its id.
232    pub id: String,
233    /// The channel it runs on.
234    pub channel: String,
235    /// Its priority: lower runs first.
236    pub priority: u32,
237    /// Its lifecycle status.
238    pub status: Status,
239    /// Its version.
240    pub version: u32,
241    /// The operators and data keys its expressions use.
242    pub fingerprint: Fingerprint,
243}
244
245/// What a [`Program`] lists about one task.
246#[derive(Clone, Debug)]
247#[non_exhaustive]
248pub struct TaskInfo {
249    /// Its position in the program.
250    pub ix: TaskIx,
251    /// Its id.
252    pub id: String,
253    /// The registered name of the function it calls.
254    pub function: String,
255    /// The workflow it belongs to.
256    pub workflow: WorkflowIx,
257    /// Whether it is a setup step or a body task.
258    pub kind: StepKind,
259    /// Where it sits in the authored workflow.
260    pub path: AuthoredPath,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::def::{ParseOptions, parse};
267    use serde_json::{Value, json};
268
269    fn workflow(id: &str, channel: &str, status: &str, tasks: usize) -> Value {
270        let tasks: Vec<Value> = (0..tasks)
271            .map(|i| json!({"id": format!("t{i}"), "function": {"name": "log", "input": {"message": "m"}}}))
272            .collect();
273        json!({"id": id, "channel": channel, "status": status, "tasks": tasks})
274    }
275
276    fn link(defs: &[Value]) -> Program {
277        let c = Registry::default().compiler(CompileOptions::default());
278        c.link(defs.iter().map(|d| {
279            let def = parse(d, ParseOptions::default()).def.expect("parses");
280            c.compile_one(&def).expect("compiles").workflow
281        }))
282    }
283
284    fn ixs(list: &[WorkflowIx]) -> Vec<u32> {
285        list.iter().map(|ix| ix.index()).collect()
286    }
287
288    #[test]
289    fn active_lists_split_by_channel_and_status() {
290        let program = link(&[
291            workflow("a", "a", "active", 1),
292            workflow("b", "b", "active", 1),
293            workflow("p", "a", "paused", 1),
294            workflow("d", "default", "active", 1),
295            workflow("x", "default", "archived", 1),
296            workflow("a2", "a", "active", 1),
297        ]);
298        assert_eq!(program.workflows().len(), 6);
299        assert_eq!(ixs(program.active(Some("a"))), [0, 5]);
300        assert_eq!(ixs(program.active(Some("b"))), [1]);
301        assert_eq!(ixs(program.active(Some("default"))), [3]);
302        assert_eq!(ixs(program.active(None)), [0, 1, 3, 5]);
303        assert!(program.active(Some("zzz")).is_empty());
304    }
305
306    #[test]
307    fn tasks_of_inactive_workflows_still_have_indices() {
308        let program = link(&[
309            workflow("a", "x", "paused", 2),
310            workflow("b", "x", "active", 3),
311            workflow("e", "x", "active", 1),
312        ]);
313        let bases: Vec<u32> = (0..3)
314            .map(|i| program.task_base(WorkflowIx::new(i)))
315            .collect();
316        assert_eq!(bases, [0, 2, 5]);
317        assert_eq!(program.tasks().len(), 6);
318        let b = WorkflowIx::new(1);
319        assert_eq!(program.compiled(b).id(), "b");
320        assert_eq!(program.task(TaskIx::new(program.task_base(b))).id, "t0");
321        assert_eq!(program.task(TaskIx::new(program.task_base(b))).workflow, b);
322    }
323}