-
Notifications
You must be signed in to change notification settings - Fork 3
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
[EK-30] Fix TOCTTOU race in SingleGoalMultipleActionServers
implementation
#78
Merged
mhidalgo-bdai
merged 6 commits into
main
from
mhidalgo-bdai/no-race-single-goal-multiple-action-server
Mar 19, 2024
Merged
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8d81f3e
Fix TOCTTOU race in SingleGoalMultipleActionServers implementation
mhidalgo-bdai fdda1fb
Fix logging message
mhidalgo-bdai a924c85
Document synchronized utility
mhidalgo-bdai b2f1b1d
Fix tests synchronization
mhidalgo-bdai 637b85f
Add extra comments to tests
mhidalgo-bdai 224a408
Document SingleGoalMultipleActionServers constructor
mhidalgo-bdai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains 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
This file contains 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
148 changes: 78 additions & 70 deletions
148
bdai_ros2_wrappers/test/test_single_goal_multiple_action_servers.py
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,108 +1,116 @@ | ||
# Copyright (c) 2023 Boston Dynamics AI Institute Inc. All rights reserved. | ||
# Copyright (c) 2023-2024 Boston Dynamics AI Institute Inc. All rights reserved. | ||
import array | ||
import time | ||
from threading import Semaphore | ||
from typing import Tuple | ||
|
||
import pytest | ||
from example_interfaces.action import Fibonacci | ||
from rclpy.action.server import GoalStatus, ServerGoalHandle | ||
from rclpy.action.server import ServerGoalHandle | ||
|
||
from bdai_ros2_wrappers.action_client import ActionClientWrapper | ||
from bdai_ros2_wrappers.action import Actionable | ||
from bdai_ros2_wrappers.futures import wait_for_future | ||
from bdai_ros2_wrappers.scope import ROSAwareScope | ||
from bdai_ros2_wrappers.single_goal_multiple_action_servers import SingleGoalMultipleActionServers | ||
|
||
|
||
def execute_callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: | ||
"""Executor callback for a server that does fibonacci""" | ||
sequence = [0, 1] | ||
for i in range(1, goal_handle.request.order): | ||
sequence.append(sequence[i] + sequence[i - 1]) | ||
|
||
goal_handle.succeed() | ||
|
||
result = Fibonacci.Result() | ||
result.sequence = sequence | ||
return result | ||
@pytest.fixture | ||
def action_triplet(ros: ROSAwareScope) -> Tuple[Semaphore, Actionable, Actionable]: | ||
semaphore = Semaphore(0) | ||
|
||
def execute_callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: | ||
nonlocal semaphore | ||
semaphore.acquire() | ||
|
||
def execute_callback_wrong_fib(goal_handle: ServerGoalHandle) -> Fibonacci.Result: | ||
"""Different executor for another server that does fibonacci wrong""" | ||
# time delay to make interrupting easier | ||
time.sleep(1) | ||
sequence = [0, 1] | ||
for i in range(1, goal_handle.request.order): | ||
sequence.append(sequence[i] * sequence[i - 1]) | ||
sequence = [0, 1] | ||
for i in range(1, goal_handle.request.order): | ||
sequence.append(sequence[i] + sequence[i - 1]) | ||
|
||
result = None | ||
if goal_handle.status != GoalStatus.STATUS_ABORTED: | ||
goal_handle.succeed() | ||
result = Fibonacci.Result() | ||
result.sequence = sequence | ||
else: | ||
result = Fibonacci.Result() | ||
result.sequence = [-1] | ||
if not goal_handle.is_cancel_requested: | ||
result.sequence = sequence | ||
goal_handle.succeed() | ||
else: | ||
goal_handle.canceled() | ||
return result | ||
|
||
return result | ||
def reversed_execute_callback(goal_handle: ServerGoalHandle) -> Fibonacci.Result: | ||
nonlocal semaphore | ||
semaphore.acquire() | ||
|
||
sequence = [0, 1] | ||
for i in range(1, goal_handle.request.order): | ||
sequence.append(sequence[i] + sequence[i - 1]) | ||
|
||
result = Fibonacci.Result() | ||
if not goal_handle.is_cancel_requested: | ||
result.sequence = list(reversed(sequence)) | ||
goal_handle.succeed() | ||
else: | ||
goal_handle.canceled() | ||
return result | ||
|
||
@pytest.fixture | ||
def action_triplet( | ||
ros: ROSAwareScope, | ||
) -> Tuple[SingleGoalMultipleActionServers, ActionClientWrapper, ActionClientWrapper]: | ||
action_parameters = [ | ||
(Fibonacci, "fibonacci", execute_callback, None), | ||
(Fibonacci, "fibonacci_wrong", execute_callback_wrong_fib, None), | ||
(Fibonacci, "fibonacci/compute", execute_callback, None), | ||
(Fibonacci, "fibonacci/compute_reversed", reversed_execute_callback, None), | ||
] | ||
assert ros.node is not None | ||
action_server = SingleGoalMultipleActionServers(ros.node, action_parameters) | ||
action_client_a = ActionClientWrapper(Fibonacci, "fibonacci", ros.node) | ||
action_client_b = ActionClientWrapper(Fibonacci, "fibonacci_wrong", ros.node) | ||
return action_server, action_client_a, action_client_b | ||
SingleGoalMultipleActionServers(ros.node, action_parameters) | ||
compute_fibonacci = Actionable(Fibonacci, "fibonacci/compute", ros.node) | ||
compute_fibonacci_reversed = Actionable(Fibonacci, "fibonacci/compute_reversed", ros.node) | ||
return semaphore, compute_fibonacci, compute_fibonacci_reversed | ||
|
||
|
||
def test_actions_in_sequence( | ||
action_triplet: Tuple[SingleGoalMultipleActionServers, ActionClientWrapper, ActionClientWrapper], | ||
action_triplet: Tuple[Semaphore, Actionable, Actionable], | ||
) -> None: | ||
"""Tests out normal operation with multiple action servers and clients""" | ||
_, action_client_a, action_client_b = action_triplet | ||
semaphore, compute_fibonacci, compute_fibonacci_reversed = action_triplet | ||
|
||
semaphore.release() # allow for action A | ||
semaphore.release() # allow for action B | ||
|
||
goal = Fibonacci.Goal() | ||
goal.order = 5 | ||
# use first client | ||
result = action_client_a.send_goal_and_wait("action_request_a", goal=goal, timeout_sec=5) | ||
assert result is not None | ||
result = compute_fibonacci(goal) | ||
expected_result = array.array("i", [0, 1, 1, 2, 3, 5]) | ||
assert result.sequence == expected_result | ||
# use second client | ||
result = action_client_b.send_goal_and_wait("action_request_b", goal=goal, timeout_sec=5) | ||
assert result is not None | ||
expected_result = array.array("i", [0, 1, 0, 0, 0, 0]) | ||
result = compute_fibonacci_reversed(goal) | ||
expected_result = array.array("i", [5, 3, 2, 1, 1, 0]) | ||
assert result.sequence == expected_result | ||
|
||
|
||
def test_action_interruption( | ||
ros: ROSAwareScope, | ||
action_triplet: Tuple[SingleGoalMultipleActionServers, ActionClientWrapper, ActionClientWrapper], | ||
def test_same_action_interruption( | ||
action_triplet: Tuple[Semaphore, Actionable, Actionable], | ||
) -> None: | ||
"""This test should start a delayed request from another client | ||
then make an immediate request to interrupt the last request. | ||
semaphore, compute_fibonacci, _ = action_triplet | ||
|
||
Due to the threading and reliance on sleeps this test might be | ||
tempermental on other machines. | ||
""" | ||
_, action_client_a, action_client_b = action_triplet | ||
goal = Fibonacci.Goal() | ||
goal.order = 5 | ||
action_a = compute_fibonacci.asynchronously(goal) | ||
action_b = compute_fibonacci.asynchronously(goal) | ||
semaphore.release() # allow for action A | ||
semaphore.release() # allow for action B | ||
assert wait_for_future(action_a.finalization, timeout_sec=5.0) | ||
assert action_a.cancelled | ||
assert wait_for_future(action_b.finalization, timeout_sec=5.0) | ||
assert action_b.succeeded | ||
expected_result = array.array("i", [0, 1, 1, 2, 3, 5]) | ||
assert action_b.result.sequence == expected_result | ||
|
||
def deferred_request() -> None: | ||
# time delay to give other action time to get started before interrupting | ||
time.sleep(0.3) | ||
goal = Fibonacci.Goal() | ||
goal.order = 5 | ||
action_client_a.send_goal_and_wait("deferred_action_request", goal=goal, timeout_sec=2) | ||
|
||
assert ros.executor is not None | ||
ros.executor.create_task(deferred_request) | ||
def test_different_action_interruption( | ||
action_triplet: Tuple[Semaphore, Actionable, Actionable], | ||
) -> None: | ||
semaphore, compute_fibonacci, compute_fibonacci_reversed = action_triplet | ||
|
||
# immediately start the request for other goal | ||
goal = Fibonacci.Goal() | ||
goal.order = 5 | ||
result = action_client_b.send_goal_and_wait("action_request", goal=goal, timeout_sec=5) | ||
assert result is None | ||
action_a = compute_fibonacci.asynchronously(goal) | ||
action_b = compute_fibonacci_reversed.asynchronously(goal) | ||
semaphore.release() # allow for action A | ||
semaphore.release() # allow for action B | ||
assert wait_for_future(action_a.finalization, timeout_sec=5.0) | ||
assert action_a.cancelled | ||
assert wait_for_future(action_b.finalization, timeout_sec=5.0) | ||
assert action_b.succeeded | ||
expected_result = array.array("i", [5, 3, 2, 1, 1, 0]) | ||
assert action_b.result.sequence == expected_result |
Oops, something went wrong.
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.
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.
Can you update the docstring with when you would want to set
nosync
toTrue
?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.
Done in 224a408.