Skip to content

Commit bc40252

Browse files
committed
Fixes Action.*_async futures never complete
Per rclpy:1123 If two seperate client server actions are running in seperate executors the future given to the ActionClient will never complete due to a race condition This fixes the calls to rcl handles potentially leading to deadlock scenarios by adding locks to there references Co-authored-by: Aditya Agarwal <[email protected]> Co-authored-by: Jonathan Blixt <[email protected]> Signed-off-by: Jonathan Blixt <[email protected]>
1 parent 43198cb commit bc40252

File tree

2 files changed

+85
-68
lines changed

2 files changed

+85
-68
lines changed

rclpy/rclpy/action/client.py

+75-63
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ def __init__(
182182
self._node.add_waitable(self)
183183
self._logger = self._node.get_logger().get_child('action_client')
184184

185+
self._lock = threading.Lock()
186+
185187
def _generate_random_uuid(self):
186188
return UUID(uuid=list(uuid.uuid4().bytes))
187189

@@ -229,51 +231,57 @@ def _remove_pending_result_request(self, future):
229231
# Start Waitable API
230232
def is_ready(self, wait_set):
231233
"""Return True if one or more entities are ready in the wait set."""
232-
ready_entities = self._client_handle.is_ready(wait_set)
233-
self._is_feedback_ready = ready_entities[0]
234-
self._is_status_ready = ready_entities[1]
235-
self._is_goal_response_ready = ready_entities[2]
236-
self._is_cancel_response_ready = ready_entities[3]
237-
self._is_result_response_ready = ready_entities[4]
234+
with self._lock:
235+
ready_entities = self._client_handle.is_ready(wait_set)
236+
self._is_feedback_ready = ready_entities[0]
237+
self._is_status_ready = ready_entities[1]
238+
self._is_goal_response_ready = ready_entities[2]
239+
self._is_cancel_response_ready = ready_entities[3]
240+
self._is_result_response_ready = ready_entities[4]
238241
return any(ready_entities)
239242

240243
def take_data(self):
241244
"""Take stuff from lower level so the wait set doesn't immediately wake again."""
242245
data = {}
243246
if self._is_goal_response_ready:
244-
taken_data = self._client_handle.take_goal_response(
245-
self._action_type.Impl.SendGoalService.Response)
246-
# If take fails, then we get (None, None)
247-
if all(taken_data):
248-
data['goal'] = taken_data
247+
with self._lock:
248+
taken_data = self._client_handle.take_goal_response(
249+
self._action_type.Impl.SendGoalService.Response)
250+
# If take fails, then we get (None, None)
251+
if all(taken_data):
252+
data['goal'] = taken_data
249253

250254
if self._is_cancel_response_ready:
251-
taken_data = self._client_handle.take_cancel_response(
252-
self._action_type.Impl.CancelGoalService.Response)
253-
# If take fails, then we get (None, None)
254-
if all(taken_data):
255-
data['cancel'] = taken_data
255+
with self._lock:
256+
taken_data = self._client_handle.take_cancel_response(
257+
self._action_type.Impl.CancelGoalService.Response)
258+
# If take fails, then we get (None, None)
259+
if all(taken_data):
260+
data['cancel'] = taken_data
256261

257262
if self._is_result_response_ready:
258-
taken_data = self._client_handle.take_result_response(
259-
self._action_type.Impl.GetResultService.Response)
260-
# If take fails, then we get (None, None)
261-
if all(taken_data):
262-
data['result'] = taken_data
263+
with self._lock:
264+
taken_data = self._client_handle.take_result_response(
265+
self._action_type.Impl.GetResultService.Response)
266+
# If take fails, then we get (None, None)
267+
if all(taken_data):
268+
data['result'] = taken_data
263269

264270
if self._is_feedback_ready:
265-
taken_data = self._client_handle.take_feedback(
266-
self._action_type.Impl.FeedbackMessage)
267-
# If take fails, then we get None
268-
if taken_data is not None:
269-
data['feedback'] = taken_data
271+
with self._lock:
272+
taken_data = self._client_handle.take_feedback(
273+
self._action_type.Impl.FeedbackMessage)
274+
# If take fails, then we get None
275+
if taken_data is not None:
276+
data['feedback'] = taken_data
270277

271278
if self._is_status_ready:
272-
taken_data = self._client_handle.take_status(
273-
self._action_type.Impl.GoalStatusMessage)
274-
# If take fails, then we get None
275-
if taken_data is not None:
276-
data['status'] = taken_data
279+
with self._lock:
280+
taken_data = self._client_handle.take_status(
281+
self._action_type.Impl.GoalStatusMessage)
282+
# If take fails, then we get None
283+
if taken_data is not None:
284+
data['status'] = taken_data
277285

278286
return data
279287

@@ -354,12 +362,14 @@ async def execute(self, taken_data):
354362

355363
def get_num_entities(self):
356364
"""Return number of each type of entity used in the wait set."""
357-
num_entities = self._client_handle.get_num_entities()
365+
with self._lock:
366+
num_entities = self._client_handle.get_num_entities()
358367
return NumberOfEntities(*num_entities)
359368

360369
def add_to_wait_set(self, wait_set):
361370
"""Add entities to wait set."""
362-
self._client_handle.add_to_waitset(wait_set)
371+
with self._lock:
372+
self._client_handle.add_to_waitset(wait_set)
363373

364374
def __enter__(self):
365375
return self._client_handle.__enter__()
@@ -437,23 +447,23 @@ def send_goal_async(self, goal, feedback_callback=None, goal_uuid=None):
437447
request = self._action_type.Impl.SendGoalService.Request()
438448
request.goal_id = self._generate_random_uuid() if goal_uuid is None else goal_uuid
439449
request.goal = goal
440-
sequence_number = self._client_handle.send_goal_request(request)
441-
if sequence_number in self._pending_goal_requests:
442-
raise RuntimeError(
443-
'Sequence ({}) conflicts with pending goal request'.format(sequence_number))
450+
future = Future()
451+
with self._lock:
452+
sequence_number = self._client_handle.send_goal_request(request)
453+
if sequence_number in self._pending_goal_requests:
454+
raise RuntimeError(
455+
'Sequence ({}) conflicts with pending goal request'.format(sequence_number))
456+
self._pending_goal_requests[sequence_number] = future
457+
self._goal_sequence_number_to_goal_id[sequence_number] = request.goal_id
458+
future.add_done_callback(self._remove_pending_goal_request)
459+
# Add future so executor is aware
460+
self.add_future(future)
444461

445462
if feedback_callback is not None:
446463
# TODO(jacobperron): Move conversion function to a general-use package
447464
goal_uuid = bytes(request.goal_id.uuid)
448465
self._feedback_callbacks[goal_uuid] = feedback_callback
449466

450-
future = Future()
451-
self._pending_goal_requests[sequence_number] = future
452-
self._goal_sequence_number_to_goal_id[sequence_number] = request.goal_id
453-
future.add_done_callback(self._remove_pending_goal_request)
454-
# Add future so executor is aware
455-
self.add_future(future)
456-
457467
return future
458468

459469
def _cancel_goal(self, goal_handle):
@@ -495,16 +505,17 @@ def _cancel_goal_async(self, goal_handle):
495505

496506
cancel_request = CancelGoal.Request()
497507
cancel_request.goal_info.goal_id = goal_handle.goal_id
498-
sequence_number = self._client_handle.send_cancel_request(cancel_request)
499-
if sequence_number in self._pending_cancel_requests:
500-
raise RuntimeError(
501-
'Sequence ({}) conflicts with pending cancel request'.format(sequence_number))
502-
503508
future = Future()
504-
self._pending_cancel_requests[sequence_number] = future
505-
future.add_done_callback(self._remove_pending_cancel_request)
506-
# Add future so executor is aware
507-
self.add_future(future)
509+
with self._lock:
510+
sequence_number = self._client_handle.send_cancel_request(cancel_request)
511+
if sequence_number in self._pending_cancel_requests:
512+
raise RuntimeError(
513+
'Sequence ({}) conflicts with pending cancel request'.format(sequence_number))
514+
515+
self._pending_cancel_requests[sequence_number] = future
516+
future.add_done_callback(self._remove_pending_cancel_request)
517+
# Add future so executor is aware
518+
self.add_future(future)
508519

509520
return future
510521

@@ -547,17 +558,18 @@ def _get_result_async(self, goal_handle):
547558

548559
result_request = self._action_type.Impl.GetResultService.Request()
549560
result_request.goal_id = goal_handle.goal_id
550-
sequence_number = self._client_handle.send_result_request(result_request)
551-
if sequence_number in self._pending_result_requests:
552-
raise RuntimeError(
553-
'Sequence ({}) conflicts with pending result request'.format(sequence_number))
554-
555561
future = Future()
556-
self._pending_result_requests[sequence_number] = future
557-
self._result_sequence_number_to_goal_id[sequence_number] = result_request.goal_id
558-
future.add_done_callback(self._remove_pending_result_request)
559-
# Add future so executor is aware
560-
self.add_future(future)
562+
with self._lock:
563+
sequence_number = self._client_handle.send_result_request(result_request)
564+
if sequence_number in self._pending_result_requests:
565+
raise RuntimeError(
566+
'Sequence ({}) conflicts with pending result request'.format(sequence_number))
567+
568+
self._pending_result_requests[sequence_number] = future
569+
self._result_sequence_number_to_goal_id[sequence_number] = result_request.goal_id
570+
future.add_done_callback(self._remove_pending_result_request)
571+
# Add future so executor is aware
572+
self.add_future(future)
561573

562574
return future
563575

rclpy/rclpy/action/server.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ async def _execute_goal_request(self, request_header_and_message):
310310
try:
311311
# If the client goes away anytime before this, sending the goal response may fail.
312312
# Catch the exception here and go on so we don't crash.
313-
self._handle.send_goal_response(request_header, response_msg)
313+
with self._lock:
314+
self._handle.send_goal_response(request_header, response_msg)
314315
except RCLError:
315316
self._logger.warn('Failed to send goal response (the client may have gone away)')
316317
return
@@ -390,7 +391,8 @@ async def _execute_cancel_request(self, request_header_and_message):
390391
try:
391392
# If the client goes away anytime before this, sending the goal response may fail.
392393
# Catch the exception here and go on so we don't crash.
393-
self._handle.send_cancel_response(request_header, cancel_response)
394+
with self._lock:
395+
self._handle.send_cancel_response(request_header, cancel_response)
394396
except RCLError:
395397
self._logger.warn('Failed to send cancel response (the client may have gone away)')
396398

@@ -407,7 +409,8 @@ async def _execute_get_result_request(self, request_header_and_message):
407409
'Sending result response for unknown goal ID: {0}'.format(goal_uuid))
408410
result_response = self._action_type.Impl.GetResultService.Response()
409411
result_response.status = GoalStatus.STATUS_UNKNOWN
410-
self._handle.send_result_response(request_header, result_response)
412+
with self._lock:
413+
self._handle.send_result_response(request_header, result_response)
411414
return
412415

413416
# There is an accepted goal matching the goal ID, register a callback to send the
@@ -427,7 +430,8 @@ def _send_result_response(self, request_header, future):
427430
try:
428431
# If the client goes away anytime before this, sending the result response may fail.
429432
# Catch the exception here and go on so we don't crash.
430-
self._handle.send_result_response(request_header, future.result())
433+
with self._lock:
434+
self._handle.send_result_response(request_header, future.result())
431435
except RCLError:
432436
self._logger.warn('Failed to send result response (the client may have gone away)')
433437

@@ -503,7 +507,8 @@ async def execute(self, taken_data):
503507

504508
def get_num_entities(self):
505509
"""Return number of each type of entity used in the wait set."""
506-
num_entities = self._handle.get_num_entities()
510+
with self._lock:
511+
num_entities = self._handle.get_num_entities()
507512
return NumberOfEntities(
508513
num_entities[0],
509514
num_entities[1],

0 commit comments

Comments
 (0)