Skip to main content

dora_runtime/
lib.rs

1#![warn(unsafe_op_in_unsafe_fn)]
2
3use dora_core::{
4    config::{DataId, OperatorId},
5    descriptor::OperatorConfig,
6};
7use dora_message::daemon_to_node::{NodeConfig, RuntimeConfig};
8use dora_metrics::run_metrics_monitor;
9use dora_node_api::{DoraNode, Event};
10use dora_tracing::TracingBuilder;
11use eyre::{Context, Result, bail};
12use futures::{Stream, StreamExt};
13use futures_concurrency::stream::Merge;
14use operator::{OperatorEvent, StopReason, run_operator};
15
16use std::{
17    collections::{BTreeMap, BTreeSet, HashMap},
18    mem,
19};
20use tokio::{
21    runtime::Builder,
22    sync::{mpsc, oneshot},
23};
24use tokio_stream::wrappers::ReceiverStream;
25mod operator;
26
27pub fn main() -> eyre::Result<()> {
28    let config: RuntimeConfig = {
29        let raw = std::env::var("DORA_RUNTIME_CONFIG")
30            .wrap_err("env variable DORA_RUNTIME_CONFIG must be set")?;
31        serde_yaml::from_str(&raw).context("failed to deserialize runtime config")?
32    };
33    let RuntimeConfig {
34        node: config,
35        operators,
36    } = config;
37    let node_id = config.node_id.clone();
38    #[cfg(feature = "tracing")]
39    {
40        TracingBuilder::new(node_id.as_ref())
41            .with_stdout("warn", false)
42            .build()
43            .wrap_err("failed to set up tracing subscriber")?;
44    }
45
46    let dataflow_descriptor = serde_yaml::from_value(config.dataflow_descriptor.clone())
47        .context("failed to parse dataflow descriptor")?;
48
49    let operator_definition = if operators.is_empty() {
50        bail!("no operators");
51    } else if operators.len() > 1 {
52        bail!("multiple operators are not supported");
53    } else {
54        let mut ops = operators;
55        ops.remove(0)
56    };
57
58    let (operator_events_tx, events) = mpsc::channel(1);
59    let operator_id = operator_definition.id.clone();
60    let operator_events = ReceiverStream::new(events).map(move |event| RuntimeEvent::Operator {
61        id: operator_id.clone(),
62        event,
63    });
64
65    // Use multi-thread scheduler (with 1 worker) because Zenoh requires it
66    // for distributed cross-daemon communication. current_thread panics when
67    // Zenoh tries to spawn background tasks during session init.
68    let tokio_runtime = Builder::new_multi_thread()
69        .worker_threads(1)
70        .enable_all()
71        .build()
72        .wrap_err("Could not build a tokio runtime.")?;
73
74    let mut operator_channels = HashMap::new();
75    let queue_sizes = queue_sizes(&operator_definition.config);
76    let (operator_channel, incoming_events) =
77        operator::channel::channel(tokio_runtime.handle(), queue_sizes);
78    operator_channels.insert(operator_definition.id.clone(), operator_channel);
79
80    tracing::info!("spawning main task");
81    let operator_config = [(
82        operator_definition.id.clone(),
83        operator_definition.config.clone(),
84    )]
85    .into_iter()
86    .collect();
87    let (init_done_tx, init_done) = oneshot::channel();
88    let main_task = std::thread::spawn(move || -> Result<()> {
89        tokio_runtime.block_on(run(
90            operator_config,
91            config,
92            operator_events,
93            operator_channels,
94            init_done,
95        ))
96    });
97
98    let operator_id = operator_definition.id.clone();
99    run_operator(
100        &node_id,
101        operator_definition,
102        incoming_events,
103        operator_events_tx,
104        init_done_tx,
105        &dataflow_descriptor,
106    )
107    .wrap_err_with(|| format!("failed to run operator {operator_id}"))?;
108
109    match main_task.join() {
110        Ok(result) => result.wrap_err("main task failed")?,
111        Err(panic) => std::panic::resume_unwind(panic),
112    }
113
114    Ok(())
115}
116
117fn queue_sizes(
118    config: &OperatorConfig,
119) -> std::collections::BTreeMap<DataId, (usize, dora_message::config::QueuePolicy)> {
120    let mut sizes = BTreeMap::new();
121    for (input_id, input) in &config.inputs {
122        let queue_size = input
123            .queue_size
124            .unwrap_or(dora_message::config::DEFAULT_QUEUE_SIZE);
125        let policy = input.queue_policy.unwrap_or_default();
126        sizes.insert(input_id.clone(), (queue_size, policy));
127    }
128    sizes
129}
130
131#[tracing::instrument(skip(operator_events, operator_channels), level = "trace")]
132async fn run(
133    operators: HashMap<OperatorId, OperatorConfig>,
134    config: NodeConfig,
135    operator_events: impl Stream<Item = RuntimeEvent> + Unpin,
136    mut operator_channels: HashMap<OperatorId, flume::Sender<Event>>,
137    init_done: oneshot::Receiver<Result<()>>,
138) -> eyre::Result<()> {
139    #[cfg(feature = "metrics")]
140    let _meter_provider = run_metrics_monitor(config.node_id.to_string());
141    init_done
142        .await
143        .wrap_err("the `init_done` channel was closed unexpectedly")?
144        .wrap_err("failed to init an operator")?;
145    tracing::info!("All operators are ready, starting runtime");
146
147    let (mut node, mut daemon_events) = DoraNode::init(config)?;
148    let (daemon_events_tx, daemon_event_stream) = flume::bounded(1);
149    tokio::task::spawn_blocking(move || {
150        while let Some(event) = daemon_events.recv() {
151            if daemon_events_tx.send(RuntimeEvent::Event(event)).is_err() {
152                break;
153            }
154        }
155    });
156    let mut events = (operator_events, daemon_event_stream.into_stream()).merge();
157
158    let mut open_operator_inputs: HashMap<_, BTreeSet<_>> = operators
159        .iter()
160        .map(|(id, config)| (id, config.inputs.keys().collect()))
161        .collect();
162
163    while let Some(event) = events.next().await {
164        match event {
165            RuntimeEvent::Operator {
166                id: operator_id,
167                event,
168            } => match event {
169                OperatorEvent::Error(err) => {
170                    bail!(err.wrap_err(format!(
171                        "operator {}/{operator_id} raised an error",
172                        node.id()
173                    )))
174                }
175                OperatorEvent::Panic(payload) => {
176                    let message = payload
177                        .downcast_ref::<&str>()
178                        .map(|s| s.to_string())
179                        .or_else(|| payload.downcast_ref::<String>().cloned())
180                        .unwrap_or_else(|| format!("{payload:?}"));
181                    bail!("operator {operator_id} panicked: {message}");
182                }
183                OperatorEvent::Finished { reason } => {
184                    if let StopReason::ExplicitStopAll = reason {
185                        bail!(
186                            "operator {operator_id} requested StopAll, which is not yet implemented"
187                        );
188                    }
189
190                    let Some(config) = operators.get(&operator_id) else {
191                        tracing::warn!(
192                            "received Finished event for unknown operator `{operator_id}`"
193                        );
194                        continue;
195                    };
196                    let outputs = config
197                        .outputs
198                        .iter()
199                        .map(|output_id| operator_output_id(&operator_id, output_id))
200                        .collect();
201                    let result;
202                    (node, result) = tokio::task::spawn_blocking(move || {
203                        let result = node.close_outputs(outputs);
204                        (node, result)
205                    })
206                    .await
207                    .wrap_err("failed to wait for close_outputs task")?;
208                    result.wrap_err("failed to close outputs of finished operator")?;
209
210                    operator_channels.remove(&operator_id);
211
212                    if operator_channels.is_empty() {
213                        break;
214                    }
215                }
216                OperatorEvent::AllocateOutputSample { len, sample: tx } => {
217                    let sample = node.allocate_data_sample(len).map_err(eyre::Report::from);
218                    if tx.send(sample).is_err() {
219                        tracing::warn!(
220                            "output sample requested, but operator {operator_id} exited already"
221                        );
222                    }
223                }
224                OperatorEvent::Output {
225                    output_id,
226                    parameters,
227                    arrow_array,
228                } => {
229                    let output_id = operator_output_id(&operator_id, &output_id);
230                    let result;
231                    (node, result) = tokio::task::spawn_blocking(move || {
232                        let array = dora_node_api::arrow::array::make_array(arrow_array);
233                        let result = node.send_output(output_id, parameters, array);
234                        (node, result)
235                    })
236                    .await
237                    .wrap_err("failed to wait for send_output task")?;
238                    result.wrap_err("failed to send node output")?;
239                }
240            },
241            RuntimeEvent::Event(Event::Stop(cause)) => {
242                // forward stop event to all operators and close the event channels
243                for (_, channel) in operator_channels.drain() {
244                    let _ = channel.send_async(Event::Stop(cause.clone())).await;
245                }
246            }
247            RuntimeEvent::Event(Event::Reload {
248                operator_id: Some(operator_id),
249            }) => {
250                let Some(operator_channel) = operator_channels.get(&operator_id) else {
251                    tracing::warn!("received Reload event for unknown operator `{operator_id}`");
252                    continue;
253                };
254                let _ = operator_channel
255                    .send_async(Event::Reload {
256                        operator_id: Some(operator_id),
257                    })
258                    .await;
259            }
260            RuntimeEvent::Event(Event::Reload { operator_id: None }) => {
261                tracing::warn!("Reloading runtime nodes is not supported");
262            }
263            RuntimeEvent::Event(Event::Input { id, metadata, data }) => {
264                let Some((operator_id, input_id)) = id.as_str().split_once('/') else {
265                    tracing::warn!("received non-operator input {id}");
266                    continue;
267                };
268                let operator_id = OperatorId::from(operator_id.to_owned());
269                let input_id = DataId::from(input_id.to_owned());
270                let Some(operator_channel) = operator_channels.get(&operator_id) else {
271                    tracing::warn!("received input {id} for unknown operator");
272                    continue;
273                };
274
275                if let Err(err) = operator_channel
276                    .send_async(Event::Input {
277                        id: input_id.clone(),
278                        metadata,
279                        data,
280                    })
281                    .await
282                    .wrap_err_with(|| {
283                        format!("failed to send input `{input_id}` to operator `{operator_id}`")
284                    })
285                {
286                    tracing::warn!("{err}");
287                }
288            }
289            RuntimeEvent::Event(Event::InputClosed { id }) => {
290                let Some((operator_id, input_id)) = id.as_str().split_once('/') else {
291                    tracing::warn!("received InputClosed event for non-operator input {id}");
292                    continue;
293                };
294                let operator_id = OperatorId::from(operator_id.to_owned());
295                let input_id = DataId::from(input_id.to_owned());
296
297                let Some(operator_channel) = operator_channels.get(&operator_id) else {
298                    tracing::warn!("received input {id} for unknown operator");
299                    continue;
300                };
301                if let Err(err) = operator_channel
302                    .send_async(Event::InputClosed {
303                        id: input_id.clone(),
304                    })
305                    .await
306                    .wrap_err_with(|| {
307                        format!(
308                            "failed to send InputClosed({input_id}) to operator `{operator_id}`"
309                        )
310                    })
311                {
312                    tracing::warn!("{err}");
313                }
314
315                if let Some(open_inputs) = open_operator_inputs.get_mut(&operator_id) {
316                    open_inputs.remove(&input_id);
317                    if open_inputs.is_empty() {
318                        // all inputs of the node were closed -> close its event channel
319                        tracing::trace!(
320                            "all inputs of operator {}/{operator_id} were closed -> closing event channel",
321                            node.id()
322                        );
323                        open_operator_inputs.remove(&operator_id);
324                        operator_channels.remove(&operator_id);
325                    }
326                }
327            }
328            RuntimeEvent::Event(Event::Error(err)) => eyre::bail!("received error event: {err}"),
329            RuntimeEvent::Event(other) => {
330                tracing::warn!("received unknown event `{other:?}`");
331            }
332        }
333    }
334
335    mem::drop(events);
336
337    Ok(())
338}
339
340fn operator_output_id(operator_id: &OperatorId, output_id: &DataId) -> DataId {
341    DataId::from(format!("{operator_id}/{output_id}"))
342}
343
344#[derive(Debug)]
345enum RuntimeEvent {
346    Operator {
347        id: OperatorId,
348        event: OperatorEvent,
349    },
350    Event(Event),
351}