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}