Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 35 additions & 39 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ uuid = { version = "1.23", features = ["serde", "v7"] }
futures = { version = "0.3.32", default-features = false, features = ["std", "async-await"] }
fs2 = "0.4.3"
redb = "4.1"
bincode = "1.3.3"
postcard = { version = "1.1.3", default-features = false, features = ["use-std"] }
flume = "0.12.0"
tempfile = "3.27.0"
proptest = "1.11"
Expand Down
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

### Breaking

- **Binary wire format moved from bincode to postcard.** [bincode is unmaintained](https://rustsec.org/advisories/RUSTSEC-2025-0141.html) — development stopped at 1.3.3 and all versions are flagged — so it is not something to carry into 1.0. Every binary plane moves to [postcard](https://docs.rs/postcard), which is serde-based (so the migration is encoding-only, no type changes), actively maintained, and unlike bincode has a **documented, stable wire spec** — the right property for a format 1.0 commits to. Messages shrink 25–27 bytes each (`Metadata` alone: 34 → 27 B) because postcard varint-encodes integers and length prefixes; serialization speed is at parity or slightly better once the encode buffer is pre-sized (`dora_message::to_vec_with_capacity`), and deserialization is unchanged to slightly faster. The JSON planes (CLI ↔ coordinator, coordinator ↔ daemon WebSocket) are untouched. This breaks four things, all of which fail loudly:
- **Mixed-version deployments.** `Metadata::CURRENT_VERSION` is bumped `1` → `2`, so a node and daemon on different sides of this change are rejected at register with a clear version-mismatch error. Upgrade daemon and nodes together.
- **Coordinator persisted store: `SCHEMA_VERSION` bumped `4` → `5`.** Stored records are postcard-encoded now, and postcard is positional rather than self-describing, so v4 rows cannot be read. `RedbStore::open()` rejects a v4-stamped database with a `schema version mismatch` error. **If you have an existing coordinator store** (`~/.dora/` by default; `redb` is the default `--store` backend), `dora coordinator` will refuse to start after upgrading. Delete the store file (its path is named in the error message) to start fresh, or pass `--store memory` to bypass persistence.
- **`.drec` recordings: format version bumped `1` → `2`.** Entry payloads are postcard-encoded. The container framing is unchanged, so a v1 file would otherwise pass the header check and then fail per-entry; the reader now rejects it up front, naming the cause. Re-record with this release — there is no converter.
- **The WebSocket topic-data channel** (`docs/websocket-topic-data-channel.md`) carries raw `Timestamped<InterDaemonEvent>` bytes, now postcard. That channel has no version handshake and both encodings are positional, so a bincode-era third-party subscriber will *misparse* rather than error — match your subscriber to the dora release.

`bincode` is gone from dora's own dependencies; it remains in the tree only transitively via `zenoh-ext`. See [RUSTSEC-2025-0141](https://rustsec.org/advisories/RUSTSEC-2025-0141.html).
- **`dora-operator-api-cxx` operator interface gains `on_input_closed` and `on_stop`**: previously the C++ operator API silently dropped `Event::InputClosed { id }` and `Event::Stop` via a catch-all `_ => Continue` arm — operators had no way to react to upstream input closure or graceful shutdown. The cxx::bridge now declares two additional callbacks that the C++ side must implement:
```cpp
DoraOnInputResult on_input_closed(Operator& op, rust::Str id, OutputSender& output_sender);
Expand Down
1 change: 0 additions & 1 deletion apis/rust/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ eyre = { workspace = true }
serde_yaml = { workspace = true }
tracing = { workspace = true }
flume = { workspace = true }
bincode = { workspace = true }
zenoh = { workspace = true }
zenoh-ext = { workspace = true }
dora-tracing = { workspace = true, optional = true }
Expand Down
11 changes: 6 additions & 5 deletions apis/rust/node/src/daemon_connection/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ use std::{
};

enum Serializer {
Bincode,
Postcard,
SerdeJson,
}
pub fn request(
connection: &mut TcpStream,
request: &Timestamped<DaemonRequest>,
) -> eyre::Result<DaemonReply> {
send_message(connection, request)?;
if request.inner.expects_tcp_bincode_reply() {
receive_reply(connection, Serializer::Bincode)
if request.inner.expects_tcp_binary_reply() {
receive_reply(connection, Serializer::Postcard)
.and_then(|reply| reply.ok_or_else(|| eyre!("server disconnected unexpectedly")))
// Use serde json for message with variable length
} else if request.inner.expects_tcp_json_reply() {
Expand All @@ -33,7 +33,8 @@ fn send_message(
connection: &mut TcpStream,
message: &Timestamped<DaemonRequest>,
) -> eyre::Result<()> {
let serialized = bincode::serialize(&message).wrap_err("failed to serialize DaemonRequest")?;
let serialized = dora_message::encode_presized(message, message.inner.encode_size_hint())
.wrap_err("failed to serialize DaemonRequest")?;
tcp_send(connection, &serialized).wrap_err("failed to send DaemonRequest")?;
Ok(())
}
Expand All @@ -57,7 +58,7 @@ fn receive_reply(
},
};
match serializer {
Serializer::Bincode => bincode::deserialize(&raw)
Serializer::Postcard => dora_message::decode(&raw)
.wrap_err("failed to deserialize DaemonReply")
.map(Some),
Serializer::SerdeJson => serde_json::from_slice(&raw)
Expand Down
8 changes: 4 additions & 4 deletions apis/rust/node/src/event_stream/data_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ mod tests {
use arrow::array::{Array, Float32Array};
use dora_message::node_to_daemon::DataMessage;

/// The daemon/TCP fallback carries the IPC stream as a bincode-serialized
/// The daemon/TCP fallback carries the IPC stream as a postcard-serialized
/// `DataMessage::Vec`. A round-trip through that serialization must preserve
/// both the payload and its 128-byte alignment, so the receiver still
/// decodes zero-copy via `decode_arrow_ipc_zero_copy`.
Expand All @@ -60,9 +60,9 @@ mod tests {
ipc_encode::encode_ipc_into(&data, &mut avec).unwrap();
let message = DataMessage::Vec(avec);

// bincode round-trip = what the node->daemon->node TCP hops do.
let bytes = bincode::serialize(&message).unwrap();
let restored: DataMessage = bincode::deserialize(&bytes).unwrap();
// Same encode/decode entry points the node->daemon->node TCP hops use.
let bytes = dora_message::encode(&message).unwrap();
let restored: DataMessage = dora_message::decode(&bytes).unwrap();
let DataMessage::Vec(avec) = restored;
assert_eq!(
avec.as_ptr() as usize % 128,
Expand Down
14 changes: 7 additions & 7 deletions apis/rust/node/src/event_stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ fn spawn_startup_acker(
node_id.as_ref(),
input_id.as_ref(),
);
let attachment = match bincode::serialize(&metadata) {
let attachment = match dora_message::encode(&metadata) {
Ok(bytes) => bytes,
Err(e) => {
tracing::debug!(input = %input_id, "failed to serialize startup ack ({e})");
Expand Down Expand Up @@ -490,7 +490,7 @@ impl EventStream {
use dora_message::metadata::Metadata;
let metadata = match sample.attachment() {
Some(att) => {
match bincode::deserialize::<Metadata>(&att.to_bytes())
match dora_message::decode::<Metadata>(&att.to_bytes())
{
// A version mismatch that still happens
// to deserialize: reject with a clear
Expand All @@ -514,7 +514,7 @@ impl EventStream {
// A pre-1.0 peer (old ArrowTypeInfo
// sidecar layout) misaligns here; name
// the likely cause so the failure isn't
// a bare bincode error.
// a bare deserialization error.
tracing::warn!(
"zenoh metadata deserialization failed \
(possibly a peer using an incompatible \
Expand Down Expand Up @@ -1921,14 +1921,14 @@ mod tests {
}

/// Regression test for the daemon↔node wire protocol: `NodeEvent`
/// is sent over TCP with bincode, so any field type that uses
/// is sent over TCP with postcard, so any field type that uses
/// `Deserializer::deserialize_any` (like `serde_json::Value`)
/// breaks the channel and kills the node at the next receive.
/// `NodeEvent::ParamUpdate` carries its value as JSON-encoded
/// bytes for that reason. This test pins the invariant so we
/// don't regress back to a `deserialize_any` field.
#[test]
fn node_event_param_update_round_trips_through_bincode() {
fn node_event_param_update_round_trips_through_postcard() {
let cases = [
serde_json::json!(42),
serde_json::json!(1.5),
Expand All @@ -1942,8 +1942,8 @@ mod tests {
key: "rate".into(),
value_json: serde_json::to_vec(&value).unwrap(),
};
let bytes = bincode::serialize(&event).expect("bincode serialize");
let back: NodeEvent = bincode::deserialize(&bytes).expect("bincode deserialize");
let bytes = dora_message::encode(&event).expect("serialize");
let back: NodeEvent = dora_message::decode(&bytes).expect("deserialize");
match back {
NodeEvent::ParamUpdate { key, value_json } => {
assert_eq!(key, "rate");
Expand Down
8 changes: 4 additions & 4 deletions apis/rust/node/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ fn declare_ack_subscribers(
let Some(attachment) = sample.attachment() else {
return;
};
let Ok(metadata) = bincode::deserialize::<Metadata>(&attachment.to_bytes()) else {
let Ok(metadata) = dora_message::decode::<Metadata>(&attachment.to_bytes()) else {
// Not a dora ack (foreign publisher on the ack key): ignore.
return;
};
Expand Down Expand Up @@ -524,7 +524,7 @@ impl StartupHandshake {
continue;
};
let metadata = Metadata::startup_marker(clock.new_timestamp());
let attachment = match bincode::serialize(&metadata) {
let attachment = match dora_message::encode(&metadata) {
Ok(bytes) => bytes,
Err(e) => {
debug!(output = %state.output_id, "failed to serialize startup marker ({e})");
Expand Down Expand Up @@ -1880,7 +1880,7 @@ impl DoraNode {
.expect("a declared publisher implies a zenoh session");

// Serialize metadata as zenoh attachment.
let metadata_bytes = match bincode::serialize(metadata) {
let metadata_bytes = match dora_message::encode(metadata) {
Ok(bytes) => bytes,
Err(e) => {
tracing::warn!(output = %output_id, "failed to serialize metadata ({e}); falling back to daemon path");
Expand Down Expand Up @@ -3005,7 +3005,7 @@ fn publish_schema_once(
metadata
.parameters
.insert(SCHEMA_HASH.to_string(), Parameter::Integer(hash as i64));
bincode::serialize(&metadata).ok()
dora_message::encode(&metadata).ok()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions binaries/cli/src/command/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ fn run_record_proxy(args: Record) -> eyre::Result<()> {
.as_nanos() as u64;

let header = RecordingHeader {
version: 1,
version: dora_recording::FORMAT_VERSION,
start_nanos,
dataflow_id,
descriptor_yaml: yaml_bytes,
Expand Down Expand Up @@ -456,7 +456,7 @@ fn run_record_proxy(args: Record) -> eyre::Result<()> {

match data_rx.recv_timeout(std::time::Duration::from_millis(100)) {
Ok(Ok(payload)) => {
// The payload is already `Timestamped<InterDaemonEvent>` bincode bytes.
// The payload is already `Timestamped<InterDaemonEvent>` postcard bytes.
// Parse it to extract node_id and output_id for the recording entry.
let event = match Timestamped::deserialize_inter_daemon_event(&payload) {
Ok(e) => e,
Expand Down
1 change: 0 additions & 1 deletion binaries/coordinator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ tower-http = { version = "0.7", features = ["cors"] }
indexmap = "2"
itertools = "0.15.0"
zenoh = { workspace = true }
bincode = { workspace = true }
arrow = { workspace = true, features = ["ipc"] }
opentelemetry = { version = "0.32", features = ["metrics"], optional = true }
dora-metrics = { workspace = true, optional = true }
Expand Down
1 change: 0 additions & 1 deletion binaries/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ serde_yaml = { workspace = true }
uuid = { workspace = true }
futures = { workspace = true }
shared_memory_extended = "0.13.0"
bincode = { workspace = true }
aligned-vec = "0.6.4"
ctrlc = "3.5.2"
which = "8"
Expand Down
Loading