Repository navigation
Conversation
launch_ros Node(name=...) passes -r __node:=<name> to the whole process. The fault-service clients, the lifecycle state reader and the subscription executor each create an rclcpp::Node that honoured that rule, so one gateway showed up as four nodes named /ros2_medkit_gateway: ros2 node list warned about duplicate names, rosout logged "Publisher already registered" three times, and ros2 param list printed every parameter four times. The helpers are now built by make_helper_node(): - named _<gateway name><suffix>, so they are hidden from ros2 node list and from discovery (no new Apps; count_peer_nodes no longer needs the helper exception list); - a node-local __node remap overrides the process-wide one. The other process arguments (namespace, remaps, parameter files) still apply; - use_sim_time is copied from the gateway, because a parameters file keyed by the gateway's name does not match the helper; - no parameter services or parameter event publisher: the fault-client node is spun only during its own requests, and a get_parameters call to it timed out; the lifecycle reader has the same shape. New integration tests launch the gateway through launch_ros, once plainly and once in a namespace with use_sim_time and with remap rules that are the only route to the fault manager. They check a single gateway node in the graph, ros2 param list without duplicates, unchanged /apps, hidden helpers in the gateway namespace, /clock subscriptions on the helpers under sim time, and that faults, topic samples and lifecycle states still work. The graph watchdog compiles the lifecycle state reader in, so it now compiles helper_node.cpp too. test_gateway_node.cpp drops redundant c_str() calls flagged by clang-tidy.
bringup.launch.py and gateway.launch.py bind 127.0.0.1. Inside a container on a bridge network that is the container's loopback, so a published port answers with "Connection reset by peer" or an empty reply. The image's default command binds 0.0.0.0. The README, the bringup and Docker tutorials and the troubleshooting page now say so, show the startup line that names the bind address, and give the fix with -p 127.0.0.1:8080:8080 to keep the unauthenticated API off the LAN. The README no longer tells native users to set cors.allowed_origins: the launch files already allow http://localhost:3000 and http://localhost:5173. The pages now state that the origin must match exactly (http://127.0.0.1:3000 and http://localhost:3000 are different origins) and that cors_allowed_origins:= replaces the defaults. The bringup tutorial and the gateway README list that launch argument. The web UI compose example used ros:jazzy, which has no gateway, and served the UI on port 80, an origin the defaults reject; it now uses the gateway image on the host network and the UI on port 3000. The getting started page quotes the gateway's real startup lines.
Executor::cancel() only clears the spinning flag, and spin() sets it again when it starts. A test that starts spin() on its own thread and cancels it later loses the cancel when the thread has not reached spin() yet. spin() then never returns and join() blocks until the ctest timeout. Short cases under parallel load hit this in test_discovery_manager, test_ros2_topic_data_provider and test_ros2_subscription_executor. Wait for is_spinning() after each spin thread starts, in every test that cancels an executor spun on its own thread. Gateway unit suite with ctest -j16: 10 of 40 runs timed out before, 0 of 20 after.
The node_death integration cases slept 5-100 ms after a detector tick and then read what the fake fault manager had received. Under parallel load a report can take longer, so a case sees it one tick late (X2 expected the raise at tick 16 and saw 17), or a late report from the previous case lands in the next one. flush_reports() sends a marker request on the same client and waits for the response. The sink serves one request at a time in arrival order, so every earlier report is recorded by then. SetUp flushes before it clears the record. With a 20 ms delay injected into the sink, 9 of 49 cases failed before and 49 of 49 pass after.
container-size-empty, unnecessary-value-param, named-parameter, inefficient-vector-operation and special-member-functions in test_fault_manager (fault manager), test_ros2_subscription_slot, test_graph_watchdog_plugin and test_opcua_plugin. No behaviour change.
A helper node now has its own name, so rcl gives it its own /rosout publisher. When the context shuts down, rcl_logging_rosout_fini() finalises every /rosout publisher on the thread that shuts down. That thread then writes the helper's type cache while the helper's owner creates or destroys entities on it, for example the subscription worker destroying a slot. TSan reported this race in two integration tests. make_helper_node() now sets enable_rosout(false). The lifecycle state reader logs through the gateway's logger, so its warning still reaches /rosout. Tests: - a unit test that the helper does not publish on /rosout - a unit test that shuts a context down while another thread creates and destroys subscriptions on the helper; under TSan it reports the race when the helper has a /rosout publisher - an integration check that no helper node of a launched gateway publishes on /rosout
test_a_compound_operation_id_runs_on_the_members_gateway sent peer_calibration:calibrate as soon as the class set-up returned. The set-up waits for a different peer operation only, so under TSan the request could reach the aggregate before this operation was merged and get 404. The test now waits for the operation it runs, like the other peer operation tests in this file.
In the RelWithDebInfo build the sanitizer jobs use, GCC 13 reports -Wnull-dereference inside libstdc++ for a std::string built from istreambuf_iterator. Two gateway tests read files that way. They now copy rdbuf() into an ostringstream, which reads the same content without the warning.
# Conflicts: # README.md # src/ros2_medkit_gateway/src/gateway_node.cpp # src/ros2_medkit_gateway/test/test_gateway_node.cpp # src/ros2_medkit_integration_tests/ros2_medkit_test_utils/launch_helpers.py # src/ros2_medkit_plugins/ros2_medkit_graph_watchdog/test/test_node_death_integration.cpp # src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_opcua_plugin.cpp
…ves them The gateway's helper nodes are now _<gateway name><suffix> in the gateway's namespace. is_own_gateway_helper_node matched <gateway><suffix>, plus a root-namespace spelling for two helpers that used to take the process default namespace. It now matches the new names in the gateway's namespace, exactly. The orphan report, the app filter's warning for a declared app bound to a helper node and param_beacon's skip list follow from the predicate. Tests, the internal node filtering docs and the /health example follow the new names.
Runtime discovery qualifies an app id with the namespace when the bare node name exists in more than one namespace, so the id of a hidden node such as /subsystem_a/_ros2_medkit_gateway_sub loses its leading underscore. Gateways that share a node name then served each other's helper nodes as apps. filter_internal_node_apps now also reads the node name from the FQN of a local runtime app. Declared apps are still judged by their id, and peer apps by their id after the peer prefix. The new integration test runs three gateways with one node name, in the root namespace, moved by a node-specific namespace remap and moved by a process-wide namespace, and checks that each lists the three gateways and none of the nine helper nodes. The predicate tests use neighbours that share the helpers' /_<name> prefix, and collision-form ids where a runtime app is meant.
…death check - helper_node.hpp: the leading underscore hides a helper from discovery only while discovery.runtime.filter_internal_nodes is on. - graph_node_list.hpp: the self_fqn parameter lists every input that matches nothing. - param_beacon: every name the helper predicate accepts starts with '_' and is skipped one line earlier, so the predicate call is gone. - test_own_node_apps_namespaced: the docstring says what the file checks. - test_node_death_integration: the reclaim check waits on the flush barrier instead of a sleep.
A host can build a GatewayNode on a private rclcpp::Context and never initialise the default one. Construction then threw: the parameter transport's _param_client_node took the default context, and the subscription executor, the fault-service transport and the lifecycle-state reader each built their SingleThreadedExecutor on it. _param_client_node now takes the gateway's context, and the three helper executors take their helper node's context (executor_options_for). The parameter transport's free spin_until_future_complete already builds its executor on the node's context. test_gateway_private_context initialises only a private context and checks construction, a fault-manager call, a parameter read through the parameter transport, a lifecycle-state read and a subscription through the subscription executor.
_cleanup_package waited up to 10 s for status completed, failed or a 404. A registered package that was never prepared reports pending with phase none, so every such cleanup spent the full 10 s, and the file ran close to its 120 s ctest timeout. The cleanup now waits while the status is inProgress or x-medkit.phase is preparing, executing or deleting: the states in which an operation runs or a DELETE is in flight. Every other state is idle, and DELETE answers 204 for it.
The node-name rule of the internal-node filter applied to every local app whose source the merge pipeline does not protect. A plugin app that keeps a protocol source such as "opcua" was therefore hidden when its node name started with '_', and a runtime app was still judged by its id after the node-name check, so a visible node /_ops/camera whose bare name also runs elsewhere got the id _ops_camera and was hidden. Runtime introspection is the only place that gives an App the source "heuristic", now the shared constant discovery::kRuntimeAppSource. An app with that source is hidden if and only if its node name starts with '_'. Every other app, remote ones included, is judged by its id. Peer apps always arrive as "peer:<name>", so the separate remote guard is gone. A declared app dropped on one of the gateway's own helper nodes is reported for any source other than runtime introspection. The filter tests name every fixture's source, add a plugin app with source "opcua" on /plant/_driver that is kept, give the peer fixture its real "peer:sub_b" source, and use a foreign gateway's helper node as a control. The same-name gateways integration test adds a visible node in /_ops whose name also runs in the root namespace and expects every gateway to list it. The discovery options page states the rule per source, scopes the claim that gateways list the same Apps, and places the warning for declared apps next to the gateway's own helper nodes.
…ion hosts Functions are grouped from the discovered apps before the internal-node filter runs, so a namespace Function kept hosts the gateway does not serve, such as subsystem_a__ros2_medkit_gateway_sub, and a namespace that held only hidden nodes could leave a Function behind. After the filter, every id it removed is dropped from every Function's hosts, unless the id still names a served App or a Component. A Function left with no hosts is not served. A Function declared without hosts stays, as before. The same-name gateways integration test reads Function subsystem_a from every gateway and expects its hosts to be exactly the served apps in that namespace.
…erves
GET /functions/{id}/hosts fell back to the discovery manager when the
Function was not in the entity cache. Discovery still returns Functions the
internal-node filter dropped, so for a namespace that holds only hidden
nodes GET /functions/{id} answered 404 while its hosts route answered 200
with an empty collection.
Both handlers now read the Function from the cache only, the set the gateway
serves and lists. The detail route already required the cache through route
validation, so its fallback could only fire in a refresh race. The cache is
filled before the REST server starts, so no startup path needs the fallback.
The same-name gateways integration test adds a hidden node alone in
/hidden_ns and expects 404 from both Function routes on every gateway.
The Function host pruning dropped any Function that had hosts before the internal-node filter and none after. Aggregation merges a peer's hosts into a local Function of the same id before the filter runs, so a declared Function without hosts of its own was removed when a peer gave it only hidden hosts. Runtime introspection is the only place that gives a Function the source "runtime", now the shared constant discovery::kRuntimeFunctionSource. Pruning still removes filtered ids from every Function's hosts, but drops a Function only when it has that source and lost its last host. A Function from a manifest, a plugin or a peer stays. drop_filtered_function_hosts is declared in gateway_node.hpp so its two cases are pinned without a node: a declared Function that loses its only host stays, and a runtime one is dropped.
The gateway's own node stays an App. When its name starts with an underscore, runtime discovery hides it like any other such node, and a declared App bound to it is judged by its id.
… both Function routes
The snapshot wait polled for the root witness id. While only the /_ops
witness existed its bare name was unique, so it took that same id, and a
refresh between the two Node() calls gave a snapshot without
_ops_same_name_gateways_witness. The wait is now for that id, which exists
only while both witnesses are on the graph.
The Function checks now compare against written-out host sets on both
GET /functions/{id} and GET /functions/{id}/hosts, cover Function _ops,
whose only host is a visible App with an id that starts with '_', and check
the 404 body names the Function as entity-not-found.
…er reported The peer client drops a peer's Apps whose id starts with '_' before the merge, but a Function's hosts arrive from its detail route and were kept whole. merge_functions then unioned them into the aggregator's Function, so a peer that runs with filter_internal_nodes false had its hidden Apps listed as hosts the aggregator does not serve. A peer Function now keeps only the hosts that name an App or a Component the same fetch read from that peer. Two existing in-process tests change with it: the peer client test expects a host that names nothing the peer reported to be dropped, and the host remap test's peer now reports the App its Function hosts. The new test_aggregated_function_hosts integration test runs an aggregator and a peer with the filter off in separate DDS domains, and expects Function line_a on the aggregator to list exactly the peer's visible App on both Function routes.
…s sole origin Host pruning dropped a Function whose source read "runtime" once the filter emptied it. The source does not say who contributed: in hybrid mode a merged Function keeps the first layer's source, so a plugin Function with the same id as a namespace Function read "runtime", and aggregation keeps the local source when a peer merges into a local Function. Both were dropped. Function gains introspection_only. Runtime introspection sets it. The merge pipeline clears it when any other layer contributes the same id, and the entity merger clears it when a peer does. Pruning drops a Function only while it is set and no host is left. The served-Component exception moves into the pruning function, which now takes the served Components. The kRuntimeAppSource and kRuntimeFunctionSource docs now say what the strings do and do not prove. Tests: - In-process: a manifest, a plugin and a peer-merged Function that lose every host stay, a Function only introspection created is dropped, and a host that also names a served Component stays. - test_aggregated_function_hosts adds namespace cell_b, hidden on both sides: the merged Function stays on the aggregator with no hosts. - test_hybrid_plugin_function_hosts runs a hybrid gateway whose test plugin declares a Function with the id runtime discovery derives for a namespace of one hidden node. It stays with no hosts, while a namespace nothing else declares has no Function. The test plugin can now declare a Function with an id and a host from its configuration.
…ere it does not The namespace Function section now says how Function ids derive from namespaces, including root, that a Function this gateway answers itself hosts only Apps and Components it serves, that a peer's Function keeps only hosts the peer reported, and that only a Function runtime discovery alone created disappears when it loses every host. It names the two cases that still list hosts the gateway does not serve: hybrid mode with allow_heuristic_functions, where runtime App ids can name Apps the merge pipeline does not serve, and a Function only a peer contributes, whose detail route is forwarded and answers with the peer's view. The bold sentence about the gateway's own node now says what holds unconditionally: the helper rule does not remove it.
…bers The plugin defines a destructor and no other special member, which cppcoreguidelines-special-member-functions reports. The plugin owns a thread and a publisher, so copy and move are deleted.
…he end of the refresh Function hosts were pruned per mechanism: by the internal-node filter in the refresh, and by the peer client for a peer's hosts. Every other way a host can name something the gateway does not serve escaped: hybrid runtime Functions grouping a node linked to a manifest App, unmanifested_nodes ignore, allow_heuristic_apps false, a namespace blacklist, a manifest host that names nothing. One pass now runs after every filter and merge in refresh_cache. keep_served_function_hosts keeps only the hosts that name an App or a Component in the final served sets, and drops a Function left without hosts only when runtime introspection is its only origin. The per-mechanism prunes are gone: drop_filtered_function_hosts and the peer client's host filter. The provenance moves off the plugin-facing Function type: - Function::introspection_only is removed. - MergeResult::introspection_only_function_ids holds the Function ids only the runtime layer returned. Any other layer's Function with the same id, a plugin's unchanged copy included, takes the id out. - DiscoveryManager::introspection_only_function_ids returns that set in hybrid mode, every discovered id in runtime mode, none from a manifest. - The refresh removes ids a peer contributes to and ids a plugin injects outside hybrid mode. test_unmanifested_policy U11 and U12 told the allow_manifest_override policies apart by the runtime id temp_sensor in Function powertrain. The node is bound to a manifest App, so that id is not served and the pass removes it. They now use rpm_sensor, an undeclared late node served under its runtime id, after a barrier that the gateway serves it. Tests: - Unit: the pass keeps hosts that name a served App or Component, drops a Function only introspection created once no host is left, and keeps one with any other origin. - test_hybrid_plugin_function_hosts runs three gateways. A (warn, blacklist, test plugin) covers linked, blacklisted, hidden-only and root namespaces, a manifest Function hosting nothing, a plugin Function and a plugin's copy of a runtime Function. B covers unmanifested_nodes ignore. C covers runtime_only with an injected plugin Function. - test_aggregated_function_hosts adds a Function only the peer has: its detail is forwarded to the peer, its hosts route is answered locally. - The test plugin can return an unchanged copy of an input Function.
The entity models cross the plugin boundary in IntrospectionInput and NewEntities, and several gained fields while the version stayed 7: the asset identity on Component, the external classification on App and Component, and declared_source and available on the entity models. A plugin built before those changes reads them with the old layout, and the loader accepted it. PLUGIN_API_VERSION is now 8. The rule is written down: a change to the layout of a type that crosses the plugin boundary bumps the version, next to the existing vtable and interface rules. The plugin tutorial's stale "current version is 5" now reads 8, and the v7 design note records the current version. In-tree plugins return PLUGIN_API_VERSION and follow the bump.
discovery-options.rst states the rule: after every filter and merge, a Function keeps only the hosts that name an App or Component the gateway serves, and a Function only runtime discovery created goes once it has no host. The hybrid exception paragraph is gone. The scope sentence keeps forwarded Functions and the fan-out of a Function's resource collections to contributing peers, which answer with their own view. The filter paragraph now says what happens to peer Apps: an id that starts with '_' is never aggregated whatever the setting, and with the setting on the others are judged by id. The paragraph on the gateway's own node keeps its claims true when the node is hidden. manifest-schema.rst says that allow_manifest_override false widens a Function's hosts only by runtime hosts the gateway serves. The aggregation design doc says where peer Function hosts are reduced.
…mes before spinning A demo node blocks SIGINT and SIGTERM and takes them on a sigwait thread, which called executor.cancel() to end executor.spin(). Under load a signal can arrive while the node is still being built. The signal thread then consumes it and calls cancel() before spin() starts, and rclcpp drops a cancel() made before spinning: spin() later sets the spinning flag again and never returns. SIGTERM is not waited for any more, so launch escalated to SIGKILL and the test's exit-code check failed. Seen on test_graph_provider_plugin: SIGINT was sent 1.9 s before the brake pressure sensor logged its start, and the node kept publishing until SIGKILL. The signal thread now also sets a stop flag, and the main thread spins in 100 ms steps until that flag is set, so a stop request made before spinning starts is kept.
The entity model layout changes behind the bump to 8 already shipped in a release with API 7, and this branch changes no plugin-facing header, so the version goes back to 7. The rule stays: a layout change of a type that crosses the plugin boundary bumps the version. The tutorial now names the current version.
… App The peer client drops peer Apps whose id starts with '_', but kept the peer Function hosts that name them. Such a host then bound to another entity with the same id on the aggregator, for example a local '_dup' node that an aggregator with filter_internal_nodes false serves. The peer client now removes those hosts with the Apps. The aggregated hosts test gets a second aggregator with the filter off. It pins that the peer client drops '_' peer Apps whatever the aggregator's setting, and that a dropped peer App id does not bind to a local App of the same id.
…tion host cases - An aggregated Function keeps a host that names a served Component. - Under allow_manifest_override false the merged hosts are exactly the manifest hosts plus the served runtime host. - Positive controls: the runtime_only gateway lists blocked_node and linked_node as hosts, and the warn gateway lists ignored_node. - A manifest Function with the id runtime discovery derives from a namespace that holds only a hidden node stays served with no hosts. - A hybrid gateway with allow_heuristic_apps false serves no visible_ns Function, because it serves no undeclared node.
…s before spinning The ghost node injector spun its leftover node with executor.spin() on a thread and stopped it with cancel() then join(). The node can be stopped right after it is started, before spin() runs, and rclcpp drops a cancel() made before spinning, so join() would wait forever. The thread now spins in 100 ms steps until a stop flag is set, as the demo nodes do. The unresponsive parameter node's comment now describes the demo node helper's spin_once loop.
…Apps
- manifest-schema: hosted_by is optional and takes App and Component ids. An
id that names no manifest entity is rule R009, a warning that fails the
load under the default strict validation. The logged message format is
the real one.
- manifest-schema: a node bound to a manifest App joins a Function's hosts
under its runtime id only when the App's id equals the node's name.
- discovery-options: a peer Function host that names a dropped '_' peer App
is removed with it, and a peer with the filter off can send hidden nodes
under namespace-qualified ids, which are aggregated.
- discovery-options: a declared App whose id does not start with '_' bound
to a gateway node named '_...' is served and carries its parameters.
- aggregation design: /functions/{id}/data reads the served hosts and fans
out to contributing peers, and the peer client drops hosts that name the
peer Apps it drops.
- migration guide: the R009 message is the one the validator logs.
The end-of-file-fixer hook requires a final newline in every text file.
bburda
marked this pull request as ready for review
October 15, 2026 20:35
…uments by reference The six parameter service callbacks took their request and response shared_ptrs by value, which clang-tidy reports as performance-unnecessary-value-param. They now take const references, a callback shape rclcpp accepts.
… gateway read the departure test_06 published the late sample one second after the node left and started asserting as soon as the test process graph listed every node as a leftover. That graph can show the leftovers before the late sample goes out: the announce message, sent earlier for the same participant, can be applied there after the participant's removal. The check then ran while the gateway had not yet refreshed after the departure and still listed the 50 nodes as running. When the check failed at once, tearDown stopped the injector before its delay ran out, so no late sample was ever sent and the gateway logged no leftover warning. The case now does what the scale case does: the late sample waits for a command, GET /apps must stop listing the nodes first, and the case waits for the late sample to be published and acknowledged before it checks. The leftover fixture gets wait_published() for that.
…truction The forwarding, peer client and aggregation manager tests pointed their unreachable peer at a fixed 127.0.0.1:59999 (and 59999 + i), and the OPC UA connect test at localhost:49999. Both are inside the kernel's ephemeral range, so any process, a test server bound to port 0 included, can hold them. When something accepted there, a forward waited out forward_timeout_ms (15 s), and the client in the handler-context wrapper tests gave up first. With nothing there, the connect is refused and the forward answers 502 at once. test_support::RefusedPort binds a loopback socket to a kernel-chosen port and never listens. A connect to it is refused at once, and no other socket can bind the port while the object lives. The forwarding fixture holds one per test, the peer client and aggregation manager tests hold theirs for the life of the binary, and the OPC UA test holds one for its case.
Collaborator
Author
|
Split into a stack of six PRs with one commit each, starting at #722. The stack carries the same changes plus three test fixes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Summary
Fixes found while running the gateway in a container.
-r __node:=<name>to the whole process, so the three helper nodes (fault-service clients, lifecycle state reader, subscription executor) took the gateway's name. They are now created bymake_helper_node(): a hidden name_<gateway name><suffix>, a node-local__noderemap, the gateway's namespace anduse_sim_time, no parameter services and no /rosout publisher. The other process arguments (namespace, remaps, parameter files) still reach them. A helper with its own name got its own /rosout publisher, and context shutdown finalises every /rosout publisher on the thread that shuts down. That thread then wrote the helper's type cache while the subscription worker destroyed entities on the helper, and TSan reported it. The lifecycle state reader now logs through the gateway's logger.server_host:=0.0.0.0is needed.bringup.launch.pyandgateway.launch.pybind127.0.0.1, and in a bridge-network container a published port then answers withConnection reset by peer. README, troubleshooting and the bringup, Docker and web UI tutorials show the startup line to check and the fix. They also say that a CORS origin must match exactly and thatcors_allowed_origins:=replaces the defaults. The web UI compose example used an image without the gateway and served the UI on an origin the defaults reject.spin()on its own thread now waits foris_spinning()before it can cancel the executor: acancel()that comes beforespin()starts is lost, andjoin()then blocks until the ctest timeout. The node_death integration cases wait for fault reports with a service round trip on the same client.test_grouping_entity_aggregationwaits for the peer operation it runs before it sends it. Two gateway tests read files throughrdbuf(), which removes the 16-Wnull-dereferencewarnings they caused in the sanitizer builds.Changes users can see: the helper loggers are named
_ros2_medkit_gateway_*and publish nothing on /rosout, the helpers no longer serve parameters, and withros2 run(the Docker image's default command)/appsno longer lists the helpers as Apps.Issue
Type
Testing
test_gateway_helper_nodesandtest_gateway_helper_nodes_namespacedlaunch the gateway through launch_ros, once plainly and once in a namespace withuse_sim_timeand remap rules. On main they fail with/ros2_medkit_gateway appears 4 times.-p 8080:8080on Docker Engine: the default bringup givescurl: (56) Recv failure: Connection reset by peer,server_host:=0.0.0.0gives HTTP 200.ctest -j16, main timed out in 6 runs and this branch in none.A 300 ms delay before
spin()makestest_discovery_managerhang every time without the fix and pass with it. A 20 ms delay in the node_death fake fault manager fails 9 of 49 cases without the round trip and none with it.colcon teston gateway, fault_manager, graph_watchdog, opcua and integration_tests: 0 failures. clang-tidy on the changed files: no findings. Run on Jazzy only.test_helper_nodechecks that the helper has no /rosout publisher, and shuts a context down while another thread creates and destroys subscriptions on the helper. With the /rosout publisher in place, one round of that test reported the race under TSan in one of two runs, so it now runs ten rounds. Without the publisher, three runs of ten rounds reported nothing.test_gateway_helper_nodeschecks that no helper of a launched gateway publishes on /rosout. Both checks fail without the change.colcon teston gateway and integration_tests, 0 failures. clang-tidy on the changed files: no findings.Checklist