Skip to content

feat(node-cxx): expose the Service and Action patterns to the C++ binding - #2853

Open
caothu2k1 wants to merge 1 commit into
dora-rs:mainfrom
caothu2k1:feat/cxx-service-action-2686
Open

feat(node-cxx): expose the Service and Action patterns to the C++ binding#2853
caothu2k1 wants to merge 1 commit into
dora-rs:mainfrom
caothu2k1:feat/cxx-service-action-2686

Conversation

@caothu2k1

@caothu2k1 caothu2k1 commented Jul 27, 2026

Copy link
Copy Markdown

Implements requests 1 (Service) and 2 (Action) of #2686.

What

The Rust and Python node APIs have first-class Service (request/reply) and
Action (goal/feedback/result) support. The C++ binding had none of it — no
request-id generator, no response passthrough helper, and, the part that
actually matters, no recv_* carrying the framework's timeout and
server-restart fault tolerance. A C++-only downstream had to hand-patch
apis/c++/node/src/lib.rs and rebuild to get any of it.

This mirrors that surface into the cxx bridge, so the generated
dora-node-api.h gains:

Rust C++
DoraNode::new_request_id / new_goal_id new_request_id() / new_goal_id()
DoraNode::send_service_request send_service_request() / send_arrow_service_request()
DoraNode::send_service_response send_service_response()
EventStream::recv_service_response recv_service_response()
EventStream::recv_action_result recv_action_result()
REQUEST_ID / GOAL_ID / GOAL_STATUS Metadata::set_request_id / set_goal_id / set_goal_status (+ getters)
GOAL_STATUS_SUCCEEDED / _ABORTED / _CANCELED goal_status_succeeded() / _aborted() / _canceled()
PatternError DoraPatternStatus

Scope: request 3 is deliberately not here

#2686 bundles three requests. The CUDA/pinned memory-pool transport (3) is
not in this PR. The Python implementation it would mirror
(apis/python/node/src/lib.rs) is ~900 lines of shmem/seqlock/DORADMA/CUDA
logic, and its acceptance criterion requires validating the host-pinned path
on a Jetson with no CUDA IPC — hardware I cannot verify against. Bundling
an unverified port of that into an otherwise reviewable Service/Action change
seemed worse than leaving #2686 open for a follow-up. Happy to take it on
separately if the shape here looks right.

A gap this uncovered

event_as_input returns {id, data} with no metadata, and the only
metadata-carrying accessor (event_as_arrow_input_with_info) exports the
payload through the Arrow C Data Interface. That made the server side of
any correlated exchange impossible from a byte-payload C++ node: it could not
read the incoming request_id to echo back, short of taking on an Arrow C++
dependency. Added event_as_input_with_metadata to close it.

Notes for reviewers

  • DoraPatternStatus::Matched, not None or Success. X11's X.h
    defines both of those as macros (#define None 0L, #define Success 0),
    so either name would break compilation for any node pulling in X11 —
    directly or via OpenCV / Qt / GTK. Verified by compiling the new examples
    with -include X11/X.h.
  • send_service_request clears request_id when the send fails, so a
    caller that skips the error check cannot block in recv_service_response
    on a correlation that never reached the wire.
  • timeout_ms is clamped. It crosses the bridge as an unbounded u64
    while the Rust helpers compute Instant::now() + timeout internally, which
    panics on overflow — the same defensive stance next_event_timeout already
    takes for this reason.
  • server_node_id is parsed via FromStr, not From<String>, which is
    documented as panicking on invalid characters; a typo in a C++ string
    literal returns InvalidArgument instead of aborting the node.
  • Each failure status also sets a matching DoraEventType on the returned
    event (Timeout / AllInputsClosed / Empty), so callers can branch on
    either field rather than being forced onto the new one.

Testing

New exampleexamples/c++-service-action/, four C++ nodes covering both
patterns, wired into [[example]] cxx-service-action and the nightly
examples job. Verified end-to-end against a live daemon:

[cxx-service-client] sent request 019fa1a8-40d6-...: 0 + 10
[server] 019fa1a8-40d6-...: 0 + 10 = 10
[cxx-service-client] response 019fa1a8-40d6-...: 0 + 10 = 10
[cxx-action-client] sent goal 019fa1a8-40d5-...: countdown from 3
[server] accepted goal 019fa1a8-40d5-...: countdown from 3
[server] result 019fa1a8-40d5-...: succeeded
[cxx-action-client] result 019fa1a8-40d5-...: succeeded
[cxx-action-client] feedback 019fa1a8-40d5-...: 2
[cxx-action-client] feedback 019fa1a8-40d5-...: 1
[cxx-action-client] feedback 019fa1a8-40d5-...: 0

5 correlated service exchanges and 3 action goals, all nodes exiting
Success. The feedback lines landing after the result confirm the
buffering guarantee: recv_action_result stashes non-matching events for the
caller's own loop instead of swallowing them.

Unit tests — 13 new tests in apis/c++/node/src/lib.rs covering the
correlation-key spellings against the framework constants, request-id
overwrite semantics, the id-cleared-on-failure contract, timeout clamping at
u64::MAX, node-id rejection, and each PatternError → status mapping.

Gates run: cargo fmt --all -- --check, cargo clippy --all -- -D warnings, cargo test -p dora-node-api-cxx (15/15), cargo check --examples, plus g++ -Wall -Wextra -fsyntax-only on all four example
sources both with and without X11 headers.

Not run locally: cargo test --all. A full workspace debug build reaches
~50 GB in target/, which does not fit on my disk. CI covers it. Also note
the example harness invokes clang++ to match every other C++ example here;
clang++ isn't installed on my machine, so the end-to-end run above was
compiled with g++ 9.4 — the committed harness is unchanged.

Acceptance criteria from #2686

  • C++ node issues a service request and awaits a REQUEST_ID-correlated response with timeout + fault tolerance
  • C++ node runs the action goal/feedback/result lifecycle
  • Memory pool / Jetson host-pinned path — deferred, see Scope
  • A C++ example mirroring the Rust one
  • Surface generated into dora-node-api.h; existing C++ examples still build

…ding

The Rust and Python node APIs have first-class Service (request/reply) and
Action (goal/feedback/result) support; the C++ binding had none of it. There
was no request-id generator, no response passthrough helper, and no `recv_*`
carrying the framework's timeout and server-restart fault tolerance, so a
C++-only downstream had to hand-patch the cxx bridge to get any of it.

Mirror that surface into `apis/c++/node/src/lib.rs`, so the generated
`dora-node-api.h` gains `new_request_id` / `new_goal_id`,
`send_service_request` (+ Arrow variant), `send_service_response`,
`recv_service_response`, `recv_action_result`, the `goal_status_*`
constants, and typed `Metadata` accessors for the reserved correlation keys.

Also add `event_as_input_with_metadata`. The server side of a correlated
exchange must read the incoming `request_id` to echo it back, and the only
metadata-carrying accessor so far (`event_as_arrow_input_with_info`) exports
the payload through the Arrow C Data Interface -- which would force an Arrow
C++ dependency on byte-payload nodes.

The success status is named `Matched` rather than `None` or `Success`
because X11's `X.h` defines both of those as macros, which would break any
node pulling in X11 directly or via OpenCV / Qt / GTK.

Defensive details at the FFI boundary: `timeout_ms` arrives as an unbounded
`u64` and is clamped so the internal `Instant::now() + timeout` cannot
panic; `server_node_id` is parsed via `FromStr` rather than the
panic-on-invalid `From<String>`; and a failed send clears the returned
`request_id` so a caller that skips the error check cannot block on a
correlation that never reached the wire.

Adds `examples/c++-service-action/` covering both patterns end-to-end,
wired into the nightly examples job, plus 13 unit tests.

Implements requests 1 and 2 of dora-rs#2686. The CUDA/pinned memory-pool transport
(request 3) is left for a follow-up: it mirrors ~900 lines of Python
shmem/DORADMA logic and its acceptance criterion requires validation on a
Jetson without CUDA IPC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@caothu2k1

caothu2k1 commented Jul 27, 2026

Copy link
Copy Markdown
Author

Hi @phil-opp , could you please review this PR?, All CI checks have passed.

Copy link
Copy Markdown
Collaborator

🤖 Fully automated review by Claude — this review was generated end-to-end by an automated agent with no human vetting. Treat it accordingly.

No issues found. I reviewed the cxx bridge surface in apis/c++/node/src/lib.rs, the new event_as_input_with_metadata / input_bytes refactor, the pattern-wait helpers, and the example wiring.

Things I specifically checked and that hold up:

  • clamp_pattern_timeout terminates and the first representable duration sits far below the Instant overflow boundary (halving steps near the boundary are astronomically large), so the clamp-vs-later-now gap is not a real TOCTOU.
  • finish_request correctly clears the id on send failure, and insert_request_id overwrites any caller-supplied id — both match the Rust DoraNode semantics and are covered by tests.
  • pattern_wait_args parses server_node_id via FromStr (not the panicking From<String>) and rejects before awaiting.
  • event_as_input_with_metadata mirrors event_as_input's type handling (UInt8/Null accepted, others Err), so it won't abort on a bad payload.
  • The example is wired into [[example]], the nightly examples job, smoke-all.sh, and example-smoke.rs consistently.

The unit tests are meaningful (behavioral assertions, not constant-echo), and the Matched/X11-macro rationale for the status enum naming is sound.


Generated by Claude Code

@rtr-thaopt

Copy link
Copy Markdown

/trunk merge

@trunk-io

trunk-io Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

An error occurred while submitting your PR to the queue: Only users that are a part of this repo's Trunk organization or have write permissions to the repo can submit a PR to the queue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants