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