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