Skip to content

Commit a927387

Browse files
jacobperronclalancette
authored andcommitted
Fix race with launch context changes when loading composable nodes (#166)
* Fix race with launch context changes when loading composable nodes This bug was discovered when trying load composable nodes from a GroupAction. The ROS namespace (and presumably other remaps) pushed onto the context stack was popped after the LoadComposableNodes execute() function finished. But because the loading happens asynchronously, we need to make sure we get the necessary information from the context before execute() finishes. Signed-off-by: Jacob Perron <[email protected]> * Add regression tests for LoadComposableNode Signed-off-by: Jacob Perron <[email protected]> * Properly shutdown mock conatiner node Also added some debug logs to the load node action for posterity. Backporting note: As far as I can tell, the *Composable* launch_ros classes in Dashing are not setup to be able to be tested. We would need to add a bunch of additional dependencies to write tests here. I didn't think that was worth it for Dashing, so I just removed the tests. Signed-off-by: Jacob Perron <[email protected]> Signed-off-by: Chris Lalancette <[email protected]>
1 parent 283c4f7 commit a927387

File tree

1 file changed

+68
-46
lines changed

1 file changed

+68
-46
lines changed

launch_ros/launch_ros/actions/load_composable_nodes.py

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ def __init__(
6868

6969
def _load_node(
7070
self,
71-
composable_node_description: ComposableNode,
71+
request: composition_interfaces.srv.LoadNode.Request,
7272
context: LaunchContext
7373
) -> None:
7474
"""
7575
Load node synchronously.
7676
77-
:param composable_node_description: description of composable node to be loaded
77+
:param request: service request to load a node
7878
:param context: current launch context
7979
"""
8080
while not self.__rclpy_load_node_client.wait_for_service(timeout_sec=1.0):
@@ -85,45 +85,14 @@ def _load_node(
8585
)
8686
)
8787
return
88-
request = composition_interfaces.srv.LoadNode.Request()
89-
request.package_name = perform_substitutions(
90-
context, composable_node_description.package
91-
)
92-
request.plugin_name = perform_substitutions(
93-
context, composable_node_description.node_plugin
94-
)
95-
if composable_node_description.node_name is not None:
96-
request.node_name = perform_substitutions(
97-
context, composable_node_description.node_name
98-
)
99-
if composable_node_description.node_namespace is not None:
100-
request.node_namespace = perform_substitutions(
101-
context, composable_node_description.node_namespace
88+
89+
self.__logger.debug(
90+
"Calling the '{}' service with request '{}'".format(
91+
self.__rclpy_load_node_client.srv_name, request
10292
)
103-
# request.log_level = perform_substitutions(context, node_description.log_level)
104-
if composable_node_description.remappings is not None:
105-
for from_, to in composable_node_description.remappings:
106-
request.remap_rules.append('{}:={}'.format(
107-
perform_substitutions(context, list(from_)),
108-
perform_substitutions(context, list(to)),
109-
))
110-
if composable_node_description.parameters is not None:
111-
request.parameters = [
112-
param.to_parameter_msg() for param in to_parameters_list(
113-
context, evaluate_parameters(
114-
context, composable_node_description.parameters
115-
)
116-
)
117-
]
118-
if composable_node_description.extra_arguments is not None:
119-
request.extra_arguments = [
120-
param.to_parameter_msg() for param in to_parameters_list(
121-
context, evaluate_parameters(
122-
context, composable_node_description.extra_arguments
123-
)
124-
)
125-
]
93+
)
12694
response = self.__rclpy_load_node_client.call(request)
95+
self.__logger.debug("Received response '{}'".format(response))
12796
if not response.success:
12897
self.__logger.error(
12998
"Failed to load node '{}' of type '{}' in container '{}': {}".format(
@@ -137,7 +106,7 @@ def _load_node(
137106

138107
def _load_in_sequence(
139108
self,
140-
composable_node_descriptions: List[ComposableNode],
109+
load_node_requests: List[composition_interfaces.srv.LoadNode.Request],
141110
context: LaunchContext
142111
) -> None:
143112
"""
@@ -146,13 +115,13 @@ def _load_in_sequence(
146115
:param composable_node_descriptions: descriptions of composable nodes to be loaded
147116
:param context: current launch context
148117
"""
149-
next_composable_node_description = composable_node_descriptions[0]
150-
composable_node_descriptions = composable_node_descriptions[1:]
151-
self._load_node(next_composable_node_description, context)
152-
if len(composable_node_descriptions) > 0:
118+
next_load_node_request = load_node_requests[0]
119+
load_node_requests = load_node_requests[1:]
120+
self._load_node(next_load_node_request, context)
121+
if len(load_node_requests) > 0:
153122
context.add_completion_future(
154123
context.asyncio_loop.run_in_executor(
155-
None, self._load_in_sequence, composable_node_descriptions, context
124+
None, self._load_in_sequence, load_node_requests, context
156125
)
157126
)
158127

@@ -168,8 +137,61 @@ def execute(
168137
)
169138
)
170139

140+
# Generate load requests before execute() exits to avoid race with context changing
141+
# due to scope change (e.g. if loading nodes from within a GroupAction).
142+
load_node_requests = [
143+
get_composable_node_load_request(node_description, context)
144+
for node_description in self.__composable_node_descriptions
145+
]
146+
171147
context.add_completion_future(
172148
context.asyncio_loop.run_in_executor(
173-
None, self._load_in_sequence, self.__composable_node_descriptions, context
149+
None, self._load_in_sequence, load_node_requests, context
174150
)
175151
)
152+
153+
154+
def get_composable_node_load_request(
155+
composable_node_description: ComposableNode,
156+
context: LaunchContext
157+
):
158+
"""Get the request that will be send to the composable node container."""
159+
request = composition_interfaces.srv.LoadNode.Request()
160+
request.package_name = perform_substitutions(
161+
context, composable_node_description.package
162+
)
163+
request.plugin_name = perform_substitutions(
164+
context, composable_node_description.node_plugin
165+
)
166+
if composable_node_description.node_name is not None:
167+
request.node_name = perform_substitutions(
168+
context, composable_node_description.node_name
169+
)
170+
if composable_node_description.node_namespace is not None:
171+
request.node_namespace = perform_substitutions(
172+
context, composable_node_description.node_namespace
173+
)
174+
if composable_node_description.remappings is not None:
175+
for from_, to in composable_node_description.remappings:
176+
request.remap_rules.append('{}:={}'.format(
177+
perform_substitutions(context, list(from_)),
178+
perform_substitutions(context, list(to)),
179+
))
180+
if composable_node_description.parameters is not None:
181+
request.parameters = [
182+
param.to_parameter_msg() for param in to_parameters_list(
183+
context, evaluate_parameters(
184+
context, composable_node_description.parameters
185+
)
186+
)
187+
]
188+
if composable_node_description.extra_arguments is not None:
189+
request.extra_arguments = [
190+
param.to_parameter_msg() for param in to_parameters_list(
191+
context, evaluate_parameters(
192+
context, composable_node_description.extra_arguments
193+
)
194+
)
195+
]
196+
197+
return request

0 commit comments

Comments
 (0)