Skip to main content

dora_message/
daemon_to_node.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    net::SocketAddr,
4    path::PathBuf,
5};
6
7use std::sync::Arc;
8
9use crate::{
10    DataflowId,
11    config::NodeRunConfig,
12    descriptor::OperatorDefinition,
13    id::{DataId, NodeId, OperatorId},
14    metadata::Metadata,
15};
16
17pub use crate::common::{DataMessage, SharedMemoryId, Timestamped};
18
19// Passed via env variable
20#[derive(Debug, serde::Serialize, serde::Deserialize)]
21pub struct RuntimeConfig {
22    pub node: NodeConfig,
23    pub operators: Vec<OperatorDefinition>,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct NodeConfig {
28    pub dataflow_id: DataflowId,
29    pub node_id: NodeId,
30    pub run_config: NodeRunConfig,
31    pub daemon_communication: Option<DaemonCommunication>,
32    pub dataflow_descriptor: serde_yaml::Value,
33    pub dynamic: bool,
34    pub write_events_to: Option<PathBuf>,
35    /// Number of times this node has been restarted. 0 on first run.
36    #[serde(default)]
37    pub restart_count: u32,
38    /// Per-output data-plane routing for the startup handshake, computed by the
39    /// daemon from the **actual** placement of the dataflow's nodes at spawn
40    /// time (the descriptor's `deploy` section is intent, not placement — label
41    /// scheduling can resolve differently).
42    ///
43    /// `None` means the node was spawned by an older daemon that doesn't
44    /// provide routing; the node then keeps every output on the reliable daemon
45    /// path (correct, just without the direct-zenoh fast path).
46    #[serde(default)]
47    pub output_routing: Option<BTreeMap<DataId, OutputRouting>>,
48}
49
50/// Data-plane routing for one node output — see
51/// [`NodeConfig::output_routing`].
52#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
53pub struct OutputRouting {
54    /// Some consumer of this output runs under another daemon. All sends must
55    /// then go through this node's daemon so its inter-daemon forwarding can
56    /// reach them (dora #2738) — the direct node-to-node zenoh mesh is
57    /// same-machine only.
58    #[serde(default)]
59    pub daemon_only: bool,
60    /// The static same-daemon consumers whose startup acks the producer must
61    /// collect before switching this output from the reliable daemon path to
62    /// the direct node-to-node zenoh path. Dynamic consumers are never
63    /// required: they join at arbitrary times (or never), and nothing may wait
64    /// on them.
65    ///
66    /// The producer only collects these during a bounded startup window; an
67    /// acker that is slow to answer costs this output the fast path for the
68    /// whole run rather than upgrading it mid-stream (dora-rs/dora#2891).
69    #[serde(default)]
70    pub required_ackers: BTreeSet<RequiredAcker>,
71}
72
73/// Identity of one consumer input that must ack a producer's startup markers
74/// before the producer may switch the corresponding output to the direct zenoh
75/// path — see [`OutputRouting::required_ackers`].
76#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
77pub struct RequiredAcker {
78    pub node_id: NodeId,
79    pub input_id: DataId,
80}
81
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
83pub enum DaemonCommunication {
84    Tcp { socket_addr: SocketAddr },
85    Interactive,
86}
87
88#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
89#[must_use]
90#[allow(clippy::large_enum_variant)]
91#[non_exhaustive]
92pub enum DaemonReply {
93    Result(Result<(), String>),
94    NextEvents(Vec<Timestamped<NodeEvent>>),
95    NodeConfig {
96        result: Result<NodeConfig, String>,
97    },
98    /// Reply to [`DaemonRequest::ExtensionLoad`]. `None` means the key is not
99    /// in the table — either never stored, or already dropped.
100    ExtensionValue {
101        value: Option<Vec<u8>>,
102    },
103    Empty,
104    /// Opaque reply to [`crate::node_to_daemon::DaemonRequest::ExtensionRequest`],
105    /// produced by the extension's daemon half. dora does not interpret it.
106    ///
107    /// Appended last so existing variants keep their postcard indices: the
108    /// Python node API ships separately (PyPI) from the daemon, so a
109    /// mixed-version pair must not misdecode older replies.
110    ExtensionReply {
111        #[serde(with = "crate::bulk_bytes::vec")]
112        payload: Vec<u8>,
113    },
114}
115
116impl DaemonReply {
117    /// Bulk bytes this reply will contribute to its encoding, for
118    /// [`crate::encode_presized`].
119    ///
120    /// `NextEvents` is a batch, so this sums the per-event hints rather than
121    /// relying on the single flat envelope allowance `encode_presized` adds:
122    /// the daemon drains up to `NODE_EVENT_CHANNEL_CAPACITY` events into one
123    /// reply, and a batch of a few dozen would otherwise realloc several times
124    /// on envelopes alone.
125    pub fn encode_size_hint(&self) -> usize {
126        match self {
127            DaemonReply::NextEvents(events) => {
128                events.iter().map(|e| e.inner.encode_size_hint()).sum()
129            }
130            // The extension value is opaque bytes handed straight back, so
131            // its own length is the whole hint.
132            DaemonReply::ExtensionValue { value } => value.as_ref().map_or(0, |bytes| bytes.len()),
133            DaemonReply::Result(_) | DaemonReply::NodeConfig { .. } | DaemonReply::Empty => 0,
134            DaemonReply::ExtensionReply { payload } => payload.len(),
135        }
136    }
137}
138
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
140#[non_exhaustive]
141#[allow(clippy::large_enum_variant)]
142pub enum NodeEvent {
143    Stop,
144    Reload {
145        operator_id: Option<OperatorId>,
146    },
147    Input {
148        id: DataId,
149        metadata: Arc<Metadata>,
150        data: Option<Arc<DataMessage>>,
151    },
152    InputClosed {
153        id: DataId,
154    },
155    /// Notifies a node that a previously closed input has recovered and will receive data again.
156    InputRecovered {
157        id: DataId,
158    },
159    /// Notifies a node that an upstream node has restarted.
160    ///
161    /// Sent to downstream nodes when a node with a restart policy successfully
162    /// restarts after a failure.
163    NodeRestarted {
164        id: NodeId,
165    },
166    /// Notifies a node that all its inputs have been closed.
167    ///
168    /// This event is only sent to nodes that have at least one input.
169    AllInputsClosed,
170    /// A runtime parameter has been updated.
171    ///
172    /// Sent when `dora param set` changes a parameter for this node.
173    ///
174    /// `value_json` carries JSON-encoded bytes rather than `serde_json::Value`:
175    /// this message is serialized with postcard on the daemon↔node TCP channel,
176    /// and `serde_json::Value::deserialize` uses `deserialize_any`, which
177    /// postcard (like any non-self-describing format) does not support.
178    ParamUpdate {
179        key: String,
180        value_json: Vec<u8>,
181    },
182    /// A runtime parameter has been deleted.
183    ///
184    /// Sent when `dora param delete` removes a parameter for this node.
185    ParamDeleted {
186        key: String,
187    },
188    /// An upstream node has failed.
189    ///
190    /// Sent to downstream nodes when an upstream node exits with a
191    /// non-zero exit code.
192    NodeFailed {
193        affected_input_ids: Vec<DataId>,
194        error: String,
195        source_node_id: NodeId,
196    },
197    /// An extension key this node stored or loaded has been dropped, by
198    /// another node or by the daemon reclaiming it.
199    ///
200    /// Delivered out of band: the event stream consumes it rather than
201    /// surfacing it to user code, so a language binding polls
202    /// `drain_dropped_extension_keys()` instead.
203    ExtensionDropped {
204        namespace: String,
205        key: String,
206    },
207}
208
209impl NodeEvent {
210    /// Bulk bytes this event will contribute to the encoding of the
211    /// [`DaemonReply`] that wraps it.
212    ///
213    /// Includes a flat per-event allowance for the `Timestamped` wrapper,
214    /// `Metadata` and ids, because these are batched: see
215    /// [`DaemonReply::encode_size_hint`].
216    pub fn encode_size_hint(&self) -> usize {
217        /// Measured at ~72 bytes for a typical `Input` (timestamp 25 +
218        /// `Metadata` 27 + tags and ids); rounded up so a batch of small events
219        /// still lands in one allocation.
220        const PER_EVENT_ENVELOPE: usize = 128;
221
222        let payload = match self {
223            NodeEvent::Input { data, .. } => data.as_ref().map_or(0, |d| d.len()),
224            NodeEvent::Stop
225            | NodeEvent::Reload { .. }
226            | NodeEvent::InputClosed { .. }
227            | NodeEvent::InputRecovered { .. }
228            | NodeEvent::NodeRestarted { .. }
229            | NodeEvent::AllInputsClosed
230            | NodeEvent::ParamUpdate { .. }
231            | NodeEvent::ParamDeleted { .. }
232            | NodeEvent::NodeFailed { .. } => 0,
233            NodeEvent::ExtensionDropped { namespace, key } => namespace.len() + key.len(),
234        };
235        payload.saturating_add(PER_EVENT_ENVELOPE)
236    }
237}