Repository navigation
fix: six independent correctness fixes (C/C++ APIs, record, ROS2 bridge, tensor-pool, type registry) - #3660
fix: six independent correctness fixes (C/C++ APIs, record, ROS2 bridge, tensor-pool, type registry)#3660phil-opp wants to merge 6 commits into
Conversation
…rrow_output fails early `send_arrow_output_impl` moved the caller's `ArrowArray`/`ArrowSchema` out and zeroed them before validating the output id or taking the node lock. An invalid id or a poisoned lock then returned an error after the array had already been released, contradicting the documented "ownership transfers on success" contract: a caller that released the structs after the error dereferenced a NULL `release`, and a caller that retried with the same structs hit an `assert!` inside arrow's schema import, which aborts the process across the cxx bridge. An already released struct passed in directly hit the same abort (or a use-after-free when the format pointer dangled). Validate the id, reject released structs and take the lock before moving anything out, and move the owned metadata instead of cloning its map on every send. The C++ docs now spell out when ownership transfers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
…operator output ids C node API: `init_dora_context_from_env` is documented to return NULL on failure, but `free_dora_context`, `free_dora_event` and `dora_next_event` dereferenced their argument unconditionally. C cleanup code that frees unconditionally (as `examples/cmake-dataflow/node-c-api` does) crashed on a failed init instead of exiting with the logged init error. NULL is now a no-op for the free functions (matching `free(NULL)`) and end-of-stream for `dora_next_event`. C operator API: `dora_send_operator_output` converted its `char const *` id with safer-ffi's `char_p_ref::to_str`, which is `from_utf8_unchecked`; the `ffi_export` shim only checks for null. A non-UTF-8 id from C therefore built an invalid `str` (UB). It is now validated and rejected with an error, matching the C node API's `dora_send_output`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
Both recorders (the `dora-record-node` binary and `dora record --proxy`) computed each entry's `timestamp_offset_nanos` as wall-clock now minus wall-clock start. `SystemTime` is not monotonic, and replay sleeps for the difference between consecutive offsets, so a clock step during a capture corrupts pacing: - a forward step (NTP/GPS sync on a board that booted near 1970, which the recorder's own docs call out) made every later offset decades larger, and replay then waits that long before the next message; - a backward step saturated offsets to 0 or made them go backwards, and replay emitted that span as an unpaced burst. Add `dora_recording::offset_nanos(start, now)`, which measures elapsed `Instant` time, and use it in both recorders. The wall clock is still written to the header's `start_nanos`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
…iber threads With two or more DDS topic subscriptions, `run_multi_subscriber` ran each subscription on its own blocking thread and joined them all after the event loop ended. A subscriber thread only noticed that the merged receiver was gone on its next `tx.send`, i.e. when its topic delivered another message, and the ROS stream itself never ends while the ROS node (owned by the caller) is alive. So on a quiet topic `join()` blocked forever and the bridge ignored `Stop` until the daemon force-killed it at the grace deadline. The single-subscriber and Zenoh paths don't use threads and were unaffected. Drop the threads: merge the subscription streams with `select_all` on the calling thread's executor, the same way `run_single_subscriber` does, so leaving the event loop simply drops them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
… spawn
When the daemon has a machine id it passes it to its nodes as
`DORA_MACHINE_ID`, and the node API then names pool segments
`dora_pool_{machine_id}_{dataflow_id}_{node_id}_{counter}`. The
spawn-time orphan sweep (`cleanup_orphans`) only matched the unqualified
`dora_pool_{dataflow_id}_` prefix, so in any deployment with a machine
id the segments that crashed nodes left in /dev/shm were never
reclaimed. The startup sweep does not cover them either, since it only
runs when cross-machine pools are enabled.
Pass the daemon's machine id through `sweep_orphans_for_dataflow` and
match both prefixes. Another machine's qualified names still never match,
and the existing per-node ownership check still applies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
…mespace
`TypeRegistry::load_from_dir` reserved `std/` by checking only the
file's directory, but builds the URN as `{dir}/{stem}/{key}`, or just
`{stem}/{key}` for a top-level file. A user's `types/std.yml` therefore
passed the check and registered `std/<key>` URNs; since `insert`
replaces, it could silently override built-in types (e.g. redefine
`std/core/v1/Float32` as Float64) for `dora validate` / `dora build`.
`add_user_type` already rejects such URNs.
Check the derived URN prefix instead of the directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
🤖 Automated review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. Reviewed head 1. This should be split.
2.
3. Two fixes aren't guarded by a real regression test.
4. User-visible behaviour change (
Tests. I ran each of these with only the non-test code reverted, and each fails:
All pass at the head. Generated by Claude Code |
|
🤖 Automated follow-up review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. Head The mdBook guide still documents the old
Suggested fix: apply the same wording change and release-on-error lines to the guide. This matters more than usual because the contract change lands on the 1.0-frozen C++ surface, and the guide is where most users will read it. (Verified by reading Generated by Claude Code |
|
🤖 Automated follow-up review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. Head
Suggestion: keep only the released-struct guard, and leave the id/lock checks after the import as on The earlier points (split the PR, the missing record/ROS2 regression tests, the Generated by Claude Code |
|
🤖 Fully automated review by Claude Code. No human checked this; it's advisory only. I found one new issue at The hang fix moves all ROS2 decoding onto one thread, and a smaller fix that keeps decoding parallel exists.
For a The hang came only from the trailing Generated by Claude Code |
|
🤖 Fully automated review by Claude Code. No human checked this; it's advisory only. Head The changed ownership rule also applies, silently, to
Failure scenario: a C++ service client calls This is another reason for the earlier suggestion to keep only the released-struct guard and leave the id/lock checks after the import, so the 1.0 contract stays "consumed unless a pointer is null" for both functions. If the contract change is kept, the service variant needs the same documentation and changelog entry. Generated by Claude Code |
|
🤖 Fully automated review by Claude Code (no human checked this; advisory only). Reviewed at 4111973. Head is unchanged. This pass checked the six fixes against the other open PRs. I found one new overlap: #3720 already contains the metadata part of fix 1. #3720 ( No other PR duplicates the rest. I fetched #3693, #3678, #3717, #3690, #3681, #3718, #3645 and #3721 and checked them against this PR:
Each of these merges cleanly with this branch. Generated by Claude Code |
|
🤖 Automated review (fully automated Claude Code review, not checked by a human; advisory only). Reviewed head One new point, from a PR opened since the last review: #3730 is a standalone copy of fix 2 (
I found this with Generated by Claude Code |
|
🤖 Automated review (Claude Code) — this is a fully automated review with no human in the loop; treat it as advisory only. Head
The other open points from earlier reviews still apply: split the PR, the Generated by Claude Code |
|
Review by Claude: suggestion to drop one fix and split the rest Thanks for collecting these fixes! Five of the six are still needed on current
A small note for the split PRs' descriptions: some earlier review comments call the record fix "fix 2", while this PR's body numbers it fix 3, so it's worth referring to commits by hash. Generated by Claude Code |
Six independent bug fixes, one commit each, so they can be reviewed (or split out) commit by commit. Each commit message explains the full failure mode.
Changes
node-api-cxx —
send_arrow_outputleaves the caller's Arrow structs alone on early failure (apis/c++/node/src/lib.rs,docs/api-cxx.md)The caller's
ArrowArray/ArrowSchemawere moved out and zeroed before the output id was validated and the lock taken. On an early error the caller either dereferenced a NULLrelease, or, on retry, hit anassert!in arrow's import that aborts across the cxx bridge. The checks (valid id, struct not already released, lock acquired) now run before anything is moved. The metadata is moved instead of cloned.C APIs — tolerate NULL handles; reject non-UTF-8 operator output ids (
apis/c/node,apis/rust/operator/types)free_dora_context,free_dora_eventanddora_next_eventdereferenced NULL.init_dora_context_from_envis documented to return NULL on failure, so a failed init crashed any cleanup that frees unconditionally. NULL is now a no-op for the free functions (likefree(NULL)) and end-of-stream fordora_next_event.dora_send_operator_outputusedchar_p_ref::to_str(from_utf8_unchecked), so a non-UTF-8 id caused UB. The id is now validated and rejected with an error.record — entry offsets use the monotonic clock (
libraries/recording,binaries/record-node,binaries/cli/src/command/record.rs)Offsets were wall-clock deltas, so an NTP/GPS step during capture broke replay pacing: a forward step meant waits of decades, a backward step meant an unpaced burst. A shared
dora_recording::offset_nanosnow measuresInstanttime and both recorders use it. The header still records the wall-clock start.ros2-bridge-node — no hang on
Stopwith ≥2 DDS subscriptions (binaries/ros2-bridge-node/src/main.rs)Each subscription ran on its own blocking thread, and all threads were joined after the event loop. On a quiet topic the join never returned, so the daemon had to force-kill the bridge at the grace deadline. The threads are gone: the streams are merged with
select_allon the caller's executor, the same wayrun_single_subscriberdoes it.tensor-pool — spawn-time orphan sweep covers machine-qualified segment names (
libraries/extensions/tensor-pool,binaries/daemon/src/dataflow_lifecycle.rs)When the daemon has a machine id, segments are named
dora_pool_{machine_id}_{dataflow_id}_…. The sweep only matcheddora_pool_{dataflow_id}_, so /dev/shm segments left by crashed nodes leaked in any deployment with a machine id. The daemon's machine id is now passed through and both prefixes are matched. Another machine's segments still never match.core — a top-level
types/std.ymlcan't claim the reservedstd/namespace (libraries/core/src/types.rs)The reservation checked the file's directory, but a top-level
std.ymlproducesstd/<key>URNs, which could silently override built-in types. The check now runs on the derived URN prefix.Known follow-up (not in this PR): the pool segment-name format is written in one place and parsed by hand in two (
cleanup_orphans,is_machine_segment). A sharedparse_pool_shmem_namein the core tensor-pool crate would stop the two sweeps from drifting apart again.Validation
Class: B
cargo fmt --all -- --check: ✅cargo clippy --all --all-targets(Python crates excluded)-D warnings: ✅cargo test --all(standard exclusions,--no-fail-fast): 2490 passed, 21 failed. All 21 failures are environmental: the test container can't open IPv6 sockets (Can not create a new TCP listener bound to tcp/[::]:0 … Address family not supported by protocol). They are all Zenoh-session tests indora-core(topics::tests::unreachable_peer_teardown_bounded),dora-daemon(endpoint_exchange, reconnect/finish tests),dora-node-api(zenoh_schema_cache) anddora-ros2-bridge(rmw_zenoh).unreachable_peer_teardown_boundedfails identically on unmodifiedorigin/mainin the same container. I expect CI to be green on these.cargo check --examples: ✅send_arrow_output_error_keeps_caller_ownership,send_arrow_output_rejects_released_structs(fix 1)null_context_and_event_are_tolerated,dora_send_operator_output_rejects_non_utf8_id(fix 2)offset_nanos_is_monotonic_elapsed_time(fix 3)load_user_types_top_level_std_file_rejected(fix 6)cleanup_orphans_*tests, which still pass; none of them creates a machine-qualified segment.make qa-fastas a whole (its fmt/clippy halves were run individually).🤖 Generated with Claude Code
https://claude.ai/code/session_01BpkxYmUYGvJ3bfGkkzqBhw