Populate message info for intra-process messages - #3271
thomasmoore-torc wants to merge 5 commits into
Conversation
(cherry picked from commit 8f255f3da12d5d0cc8aa5a49f69bb74339226820) (cherry picked from commit 94a3d5856c046e3a7b6bda942394cd8662926c28) Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
(cherry picked from commit f2302935bd1b41a75f56c6a9f4ef5b7fbe4d7e9b) (cherry picked from commit 8a875ee1df60afcdb7ed68ceb27f097acdcdacf0) Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
|
|
||
| rmw_message_info_t message_info{}; | ||
| message_info.from_intra_process = true; | ||
| message_info.publication_sequence_number = publisher_it->second.publication_sequence_number++; |
There was a problem hiding this comment.
what if 2 publishers are publishing to the same topic at the same time?
looks like this sequence counters mutated under a shared (reader) lock is not thread safe?
IMO std::atomic could be used here.
| continue; | ||
| } | ||
|
|
||
| message_info.reception_sequence_number = subscription_it->second.reception_sequence_number++; |
There was a problem hiding this comment.
same racy condition here?
| unique_msg = MessageUniquePtr(ptr); | ||
| // Promote to a shared pointer | ||
| auto unique_msg = std::move(std::get<MessageUniquePtr>(data.message)); | ||
| data.message = MessageSharedPtr(unique_msg.release()); |
There was a problem hiding this comment.
the message could be allocated via the allocator but will be destroyed with plain delete, this looks like it leads to mismatch allocate/deallocate consistency? eventually Undefined Behaivor.
| data.message = MessageSharedPtr(unique_msg.release()); | |
| data.message = MessageSharedPtr(std::move(unique_msg)); |
| return nullptr; | ||
| } | ||
| auto data = this->buffer_->consume(); | ||
| if (data.message.index() == std::variant_npos) { |
There was a problem hiding this comment.
the check never fires for empty buffer? index() returns variant_npos only for a variant that is valueless by exception.
| }, message); | ||
| trigger_guard_condition(); | ||
| } | ||
| buffer_->add(std::move(data)); |
There was a problem hiding this comment.
the gurad condition will be triggered before the data is in the queue? i think this needs to be moved to before triggering the guard condition? at least, previous code keeps it in that way.
|
@thomasmoore-torc i agree with you.
especially this behavior is not transparent and consistent for the applicaiton. got several comments above to be addressed. please have them checked out! |
Addresses five issues raised by @fujitatomoya on the PR: - publication_sequence_number and reception_sequence_number were plain uint64_t members mutated (via postfix ++) from do_intra_process_publish(), which only holds a shared (reader) lock on mutex_. Concurrent publishes could race on these counters. Made both std::atomic<uint64_t>; since std::atomic has no copy/move assignment, PublisherData/SubscriptionData gained explicit constructors and their insertion sites switched to try_emplace() instead of default-construct-then-assign. - IntraProcessBuffer::add_impl() (SharedPtr case) promoted a MessageUniquePtr to a MessageSharedPtr via `MessageSharedPtr(unique_msg.release())`, which drops the unique_ptr's (possibly allocator-aware) deleter in favor of plain delete -- an allocate/deallocate mismatch. Changed to `MessageSharedPtr(std::move(unique_msg))`, which preserves the original deleter via shared_ptr's converting constructor. - SubscriptionIntraProcess::take_data() checked `data.message.index() == std::variant_npos` to detect an empty buffer, but variant_npos only occurs for a valueless-by-exception variant. A default-constructed Data (what an empty buffer's consume() returns) holds a null pointer in its default alternative instead, so the check never fired. The original pre-refactor code checked pointer nullness directly (`if (!shared_msg) return nullptr;` / `if (!unique_msg) return nullptr;`); restored that via `std::visit` across both alternatives. - SubscriptionIntraProcessBuffer::provide_intra_process_message() called trigger_guard_condition() before buffer_->add(), unlike the sibling provide_intra_process_data() which does it in the correct order. This left a window where a consumer woken by the guard condition could find the buffer still empty. Reordered to add() then trigger. Signed-off-by: Thomas Moore <thomas.moore@torc.ai> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each test was verified to fail against the pre-fix code and pass against the fix in the previous commit: - test_intra_process_buffer.cpp: shared_buffer_add_preserves_custom_deleter adds a unique_ptr with a tracking deleter to a SharedPtr-typed buffer and confirms the *original* deleter runs when the promoted shared_ptr's refcount hits zero, not a plain `delete`. - test_subscription_intra_process.cpp (new file, plus scaffolding to construct a real SubscriptionIntraProcess directly, without a full Node/executor): take_data_returns_nullptr_when_buffer_is_empty covers the variant_npos check; provide_intra_process_message_adds_data_before_triggering patches rcl_trigger_guard_condition (via mocking_utils) to observe whether the buffer already has data at the moment the guard condition actually fires. - test_publisher.cpp: concurrent_publish_produces_unique_sequence_numbers publishes from 8 threads at once and checks every delivered publication_sequence_number is unique. Against the pre-fix plain uint64_t counter this fails reliably (8/8 runs); this is a stronger regression guard than a typical data-race test since the race window here is wide enough to hit consistently under load, not just occasionally. Signed-off-by: Thomas Moore <thomas.moore@torc.ai> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rclcpp: Populate message info for intra-process messages
Branch:
intra-process-message-info(this repo:ros2/rclcpp)Related PR: #3272 — "Iterate over IntraProcessBuffer instead of creating a vector" (
intra-process-reduce-copies). See Landing order below — no functional dependency, but the two touch overlapping code.Summary
Subscriptions that receive a message via intra-process delivery currently get a default-constructed, empty
rmw_message_info_t— no sequence number, no publisher GID, nofrom_intra_processflag. The same subscription receiving the same message via inter-process (rmw) delivery gets a fully populated one. Anything relying onmessage_info(deduplicating by publisher GID, checkingfrom_intra_process, etc.) behaves inconsistently depending on whether a given publish happens to stay in-process or not.This populates
message_infofor intra-process messages too, so subscribers see consistent, meaningful info regardless of delivery path.Changes
Populate message info for intra-process messages— threads anrmw_message_info_talongside the message through the intra-process publish path (Publisher::do_intra_process_publish/do_intra_process_ros_message_publish_and_return_shared→IntraProcessManager→IntraProcessBuffer→ subscription).IntraProcessBufferstorage changes from a bareshared_ptr/unique_ptrto a bundledIntraProcessBufferData(message +rmw_message_info_t), added with a copy constructor that deep-copies the unique-ptr alternative (needed soget_all_data()-based paths, e.g. transient-local replay, keep working with move-only storage).do_transient_local_publishto correctly convert between shared/unique storage and shared/unique subscription requests viastd::visitinstead of assuming the two always match.Move IntraProcessBufferData to its own header— pulls the new struct out intointra_process_buffer_data.hppto keepintra_process_buffer.hppfocused and avoid a circular-ish coupling between buffer and data definitions.Update intra-process unit tests— updatestest_intra_process_buffer.cppandtest_intra_process_manager.cppfor the newData/IntraProcessBufferDataAPI and adds coverage for message-info propagation across both shared- and unique-ownership subscriptions.Compatibility
Internal to
rclcpp's intra-process implementation (rclcpp::experimental::*); no public API signature changes. Behavior change: intra-process subscribers now observe non-defaultmessage_infowhere they previously saw an empty one.Testing
Built and tested against
rollingin aros:rolling-ros-basecontainer viacolcon build/colcon testforrclcpp(3091 tests, 0 failures), plusuncrustify/cpplintlint targets. All green.Landing order
intra-process-reduce-copies(opened separately) touches the same files (intra_process_buffer.hpp,intra_process_manager.hpp, and both intra-process test files) as part of an unrelated copy-reduction optimization. The two branches are independently buildable and testable, but not free to merge in either order without a rebase — real textual conflicts exist between them (confirmed via a trial merge). No functional dependency either way; whichever of the two merges second will need to reconcile against the other.🤖 Generated with Claude Code