-
Notifications
You must be signed in to change notification settings - Fork 38
fix(gateway): log a shared plugin entity's ownership transfer once #702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| #include <dlfcn.h> | ||
| #include <httplib.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <rclcpp/rclcpp.hpp> | ||
| #include <regex> | ||
| #include <unordered_set> | ||
|
|
@@ -614,13 +615,32 @@ void PluginManager::register_entity_ownership(const std::string & plugin_name, | |
| for (const auto & eid : entity_ids) { | ||
| auto it = entity_ownership_.find(eid); | ||
| if (it != entity_ownership_.end() && it->second != plugin_name) { | ||
| RCLCPP_WARN(logger(), "Entity '%s' ownership transferred from plugin '%s' to '%s'", eid.c_str(), | ||
| it->second.c_str(), plugin_name.c_str()); | ||
| // Plugins that all publish this ID pass it along on every refresh, so | ||
| // log each pair of them once. | ||
| const auto & [first, second] = std::minmax(it->second, plugin_name); | ||
| if (reported_ownership_conflicts_.emplace(eid, first, second).second) { | ||
| RCLCPP_WARN(logger(), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When an id already has an owner from the previous refresh, the first transfer seen is "previous owner -> first publisher in load order", the reverse of where the id ends up, and the sorted pair then suppresses the real direction for good: with plugin_b owning shared_area and plugin_a starting to publish it, the only line ever logged is "transferred from plugin_b to plugin_a" while plugin_b keeps the id and its copy is served. With four publishers the same mechanism adds a "plugin_d to plugin_a" line one refresh after the first three. Log where the outcome is known instead: collect each id's publishers during the refresh and let finish_ownership_refresh() emit one line per id naming the publishers and the final owner, deduplicated on the id plus its publisher set. |
||
| "Entity '%s' ownership transferred from plugin '%s' to '%s'. When several plugins publish an " | ||
| "entity, the one registered last in a refresh owns it. Not logged again for these two plugins " | ||
| "while the entity has an owner", | ||
| eid.c_str(), it->second.c_str(), plugin_name.c_str()); | ||
| } | ||
| } | ||
| entity_ownership_[eid] = plugin_name; | ||
| } | ||
| } | ||
|
|
||
| void PluginManager::finish_ownership_refresh() { | ||
| std::unique_lock<std::shared_mutex> lock(plugins_mutex_); | ||
| for (auto it = reported_ownership_conflicts_.begin(); it != reported_ownership_conflicts_.end();) { | ||
| if (entity_ownership_.count(std::get<0>(*it)) == 0) { | ||
| it = reported_ownership_conflicts_.erase(it); | ||
| } else { | ||
| ++it; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void PluginManager::clear_entity_ownership(const std::string & plugin_name) { | ||
| std::unique_lock<std::shared_mutex> lock(plugins_mutex_); | ||
| for (auto it = entity_ownership_.begin(); it != entity_ownership_.end();) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Copyright 2026 bburda | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| #pragma once | ||
|
|
||
| // LogCapture - records rcutils log output for tests that assert what was | ||
| // logged, or how often. | ||
|
|
||
| #include <algorithm> | ||
| #include <atomic> | ||
| #include <cstdarg> | ||
| #include <cstdio> | ||
| #include <iterator> | ||
| #include <mutex> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include "rcutils/logging.h" | ||
|
|
||
| namespace ros2_medkit_gateway::test { | ||
|
|
||
| /// Captures every log line while alive and puts the previous output handler | ||
| /// back on every exit path, so a failing case cannot swallow the output of | ||
| /// later ones or leave rclcpp's handler replaced. | ||
| /// | ||
| /// The handler is a plain C function pointer with no user-data slot, so the | ||
| /// live capture is reached through a static. The pointer is atomic because | ||
| /// the handler is process-global and other threads log too. | ||
| class LogCapture { | ||
| public: | ||
| LogCapture() : previous_(rcutils_logging_get_output_handler()) { | ||
| active().store(this); | ||
| rcutils_logging_set_output_handler(&LogCapture::handler); | ||
| } | ||
| ~LogCapture() { | ||
| rcutils_logging_set_output_handler(previous_); | ||
| active().store(nullptr); | ||
| } | ||
| LogCapture(const LogCapture &) = delete; | ||
| LogCapture & operator=(const LogCapture &) = delete; | ||
| LogCapture(LogCapture &&) = delete; | ||
| LogCapture & operator=(LogCapture &&) = delete; | ||
|
|
||
| /// Captured lines, each as "<logger name>: <message>", that contain `needle`. | ||
| std::vector<std::string> matching(const std::string & needle) const { | ||
| std::lock_guard<std::mutex> lk(mutex_); | ||
| std::vector<std::string> out; | ||
| std::copy_if(lines_.begin(), lines_.end(), std::back_inserter(out), [&needle](const std::string & line) { | ||
| return line.find(needle) != std::string::npos; | ||
| }); | ||
| return out; | ||
| } | ||
|
|
||
| private: | ||
| static std::atomic<LogCapture *> & active() { | ||
| static std::atomic<LogCapture *> current{nullptr}; | ||
| return current; | ||
| } | ||
|
|
||
| static void handler(const rcutils_log_location_t * /*location*/, int /*severity*/, const char * name, | ||
| rcutils_time_point_value_t /*timestamp*/, const char * format, va_list * args) { | ||
| char buf[1024]; | ||
| va_list copy; | ||
| va_copy(copy, *args); | ||
| // The format string arrives through the handler signature, so there is no | ||
| // literal to check. GCC exempts va_list formatters from | ||
| // -Wformat-nonliteral, clang does not. Scoped to the single call. | ||
| #pragma GCC diagnostic push | ||
| #pragma GCC diagnostic ignored "-Wformat-nonliteral" | ||
| vsnprintf(buf, sizeof(buf), format, copy); | ||
| #pragma GCC diagnostic pop | ||
| va_end(copy); | ||
| LogCapture * capture = active().load(); | ||
| if (capture == nullptr) { | ||
| return; | ||
| } | ||
| std::lock_guard<std::mutex> lk(capture->mutex_); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Between active().load() and this lock nothing stops ~LogCapture() on the test thread from restoring the handler and destroying the object, so a WARN from another thread (the REST server thread or an executor callback in the notify test) locks a freed mutex. Guard the pointer and the push with one static mutex and take that same mutex in the destructor around active().store(nullptr), which also makes mutex_ redundant. |
||
| capture->lines_.push_back(std::string(name != nullptr ? name : "") + ": " + buf); | ||
| } | ||
|
|
||
| rcutils_logging_output_handler_t previous_; | ||
| mutable std::mutex mutex_; | ||
| std::vector<std::string> lines_; | ||
| }; | ||
|
|
||
| } // namespace ros2_medkit_gateway::test | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Outside hybrid discovery" hides that hybrid does the opposite: PluginLayer defaults to MergePolicy::ENRICHMENT, so the merge pipeline keeps the field values of the plugin loaded first and only fills gaps from later plugins, while routing goes to the plugin loaded last. State that here, since this paragraph justifies last-wins by the served copy.