Skip to content

Commit be9cf5d

Browse files
committed
Replace spin_until_future_complete with spin_until_complete, add spin_for method
* Deprecate spin_until_future_complete * Add spin_until_complete * Add spin_for method * Udpdate unit tests Signed-off-by: Hubert Liberacki <[email protected]>
1 parent 8e8c978 commit be9cf5d

File tree

8 files changed

+246
-75
lines changed

8 files changed

+246
-75
lines changed

rclpy/rclpy/__init__.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
After a node is created, items of work can be done (e.g. subscription callbacks) by *spinning* on
3434
the node.
3535
The following functions can be used to process work that is waiting to be executed: :func:`spin`,
36-
:func:`spin_once`, and :func:`spin_until_future_complete`.
36+
:func:`spin_once`, and :func:`spin_until_complete`.
3737
3838
When finished with a previously initialized :class:`.Context` (ie. done using
3939
all ROS nodes associated with the context), the :func:`shutdown` function should be called.
@@ -224,6 +224,52 @@ def spin(node: 'Node', executor: 'Executor' = None) -> None:
224224
executor.remove_node(node)
225225

226226

227+
def spin_for(node: 'Node', executor: 'Executor' = None, duration_sec: float = None) -> None:
228+
"""
229+
Execute block until the context associated with the executor is shutdown or time duration pass.
230+
231+
Callbacks will be executed by the provided executor.
232+
233+
This function blocks.
234+
235+
:param node: A node to add to the executor to check for work.
236+
:param executor: The executor to use, or the global executor if ``None``.
237+
:param timeout_sec: Seconds to wait.
238+
"""
239+
executor = get_global_executor() if executor is None else executor
240+
try:
241+
executor.add_node(node)
242+
executor.spin_once(duration_sec)
243+
finally:
244+
executor.remove_node(node)
245+
246+
247+
def spin_until_complete(
248+
node: 'Node',
249+
condition,
250+
executor: 'Executor' = None,
251+
timeout_sec: float = None
252+
) -> None:
253+
"""
254+
Execute work until the condition is complete.
255+
256+
Callbacks and other work will be executed by the provided executor until ``condition()`` or
257+
``future.done()`` returns ``True`` or the context associated with the executor is shutdown.
258+
259+
:param node: A node to add to the executor to check for work.
260+
:param condition: The callable or future object to wait on.
261+
:param executor: The executor to use, or the global executor if ``None``.
262+
:param timeout_sec: Seconds to wait. Block until the condition is complete
263+
if ``None`` or negative. Don't wait if 0.
264+
"""
265+
executor = get_global_executor() if executor is None else executor
266+
try:
267+
executor.add_node(node)
268+
executor.spin_until_complete(condition, timeout_sec)
269+
finally:
270+
executor.remove_node(node)
271+
272+
227273
def spin_until_future_complete(
228274
node: 'Node',
229275
future: Future,
@@ -241,10 +287,7 @@ def spin_until_future_complete(
241287
:param executor: The executor to use, or the global executor if ``None``.
242288
:param timeout_sec: Seconds to wait. Block until the future is complete
243289
if ``None`` or negative. Don't wait if 0.
290+
291+
Deprecated in favor of spin_until_complete
244292
"""
245-
executor = get_global_executor() if executor is None else executor
246-
try:
247-
executor.add_node(node)
248-
executor.spin_until_future_complete(future, timeout_sec)
249-
finally:
250-
executor.remove_node(node)
293+
spin_until_complete(node, future, executor, timeout_sec)

rclpy/rclpy/executors.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,28 +278,52 @@ def spin(self) -> None:
278278
while self._context.ok() and not self._is_shutdown:
279279
self.spin_once()
280280

281-
def spin_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
282-
"""Execute callbacks until a given future is done or a timeout occurs."""
283-
# Make sure the future wakes this executor when it is done
284-
future.add_done_callback(lambda x: self.wake())
281+
def spin_for(self, duration_sec: float = None) -> None:
282+
"""Execute callbacks until shutdown, or timeout."""
283+
self.spin_until_complete(lambda: False, duration_sec)
284+
285+
def spin_until_complete(self, condition, timeout_sec: float = None) -> None:
286+
"""
287+
Execute callbacks until a given condition is done or a timeout occurs.
288+
289+
Deprecated in favor of spin_until_complete.
290+
"""
291+
# Common conditon for safisfying both Callable and Future
292+
finish_condition = None
293+
if (isinstance(condition, Future)):
294+
# Make sure the future wakes this executor when it is done
295+
condition.add_done_callback(lambda x: self.wake())
296+
def finish_condition(): return condition.done()
297+
elif (callable(condition)):
298+
def finish_condition(): return condition()
299+
else:
300+
raise TypeError("Condition has to be of Future or Callable type")
285301

286302
if timeout_sec is None or timeout_sec < 0:
287-
while self._context.ok() and not future.done() and not self._is_shutdown:
288-
self.spin_once_until_future_complete(future, timeout_sec)
303+
while self._context.ok() and not finish_condition() and not self._is_shutdown:
304+
self.spin_once_until_complete(condition, timeout_sec)
289305
else:
290306
start = time.monotonic()
291307
end = start + timeout_sec
292308
timeout_left = timeout_sec
293309

294-
while self._context.ok() and not future.done() and not self._is_shutdown:
295-
self.spin_once_until_future_complete(future, timeout_left)
310+
while self._context.ok() and not finish_condition() and not self._is_shutdown:
311+
self.spin_once_until_complete(condition, timeout_left)
296312
now = time.monotonic()
297313

298314
if now >= end:
299315
return
300316

301317
timeout_left = end - now
302318

319+
def spin_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
320+
"""
321+
Execute callbacks until a given future is done or a timeout occurs.
322+
323+
Deprecated in favor of spin_until_complete.
324+
"""
325+
self.spin_until_complete(future, timeout_sec)
326+
303327
def spin_once(self, timeout_sec: float = None) -> None:
304328
"""
305329
Wait for and execute a single callback.
@@ -311,6 +335,19 @@ def spin_once(self, timeout_sec: float = None) -> None:
311335
"""
312336
raise NotImplementedError()
313337

338+
def spin_once_until_complete(self, condition, timeout_sec: float = None) -> None:
339+
"""
340+
Wait for and execute a single callback.
341+
342+
This should behave in the same way as :meth:`spin_once`.
343+
If needed by the implementation, it should awake other threads waiting.
344+
345+
:param condition: The executor will wait until this condition is done.
346+
:param timeout_sec: Maximum seconds to wait. Block forever if ``None`` or negative.
347+
Don't wait if 0.
348+
"""
349+
raise NotImplementedError()
350+
314351
def spin_once_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
315352
"""
316353
Wait for and execute a single callback.
@@ -321,6 +358,8 @@ def spin_once_until_future_complete(self, future: Future, timeout_sec: float = N
321358
:param future: The executor will wait until this future is done.
322359
:param timeout_sec: Maximum seconds to wait. Block forever if ``None`` or negative.
323360
Don't wait if 0.
361+
362+
Deprecated in favor of spin_once_until_complete.
324363
"""
325364
raise NotImplementedError()
326365

@@ -711,9 +750,14 @@ def spin_once(self, timeout_sec: float = None) -> None:
711750
if handler.exception() is not None:
712751
raise handler.exception()
713752

714-
def spin_once_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
753+
def spin_once_until_complete(self, condition, timeout_sec: float = None) -> None:
715754
self.spin_once(timeout_sec)
716755

756+
"""Deprecated in favor of spin_once_until_complete"""
757+
758+
def spin_once_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
759+
self.spin_once_until_complete(timeout_sec)
760+
717761

718762
class MultiThreadedExecutor(Executor):
719763
"""
@@ -756,5 +800,11 @@ def _spin_once_impl(
756800
def spin_once(self, timeout_sec: float = None) -> None:
757801
self._spin_once_impl(timeout_sec)
758802

803+
def spin_once_until_complete(self, condition, timeout_sec: float = None) -> None:
804+
self._spin_once_impl(timeout_sec, condition if callable(
805+
condition) else condition.done)
806+
807+
"""Deprecated in favor of spin_once_until_complete"""
808+
759809
def spin_once_until_future_complete(self, future: Future, timeout_sec: float = None) -> None:
760-
self._spin_once_impl(timeout_sec, future.done)
810+
self.spin_once_until_complete(timeout_sec, future.done)

rclpy/test/test_action_client.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def test_send_goal_async(self):
159159
try:
160160
self.assertTrue(ac.wait_for_server(timeout_sec=2.0))
161161
future = ac.send_goal_async(Fibonacci.Goal())
162-
rclpy.spin_until_future_complete(self.node, future, self.executor)
162+
rclpy.spin_until_complete(self.node, future, self.executor)
163163
self.assertTrue(future.done())
164164
goal_handle = future.result()
165165
self.assertTrue(goal_handle.accepted)
@@ -177,7 +177,7 @@ def test_send_goal_async_with_feedback_after_goal(self):
177177
Fibonacci.Goal(),
178178
feedback_callback=self.feedback_callback,
179179
goal_uuid=goal_uuid)
180-
rclpy.spin_until_future_complete(self.node, future, self.executor)
180+
rclpy.spin_until_complete(self.node, future, self.executor)
181181

182182
# Publish feedback after goal has been accepted
183183
self.mock_action_server.publish_feedback(goal_uuid)
@@ -202,7 +202,7 @@ def test_send_goal_async_with_feedback_before_goal(self):
202202
Fibonacci.Goal(),
203203
feedback_callback=self.feedback_callback,
204204
goal_uuid=goal_uuid)
205-
rclpy.spin_until_future_complete(self.node, future, self.executor)
205+
rclpy.spin_until_complete(self.node, future, self.executor)
206206

207207
# Check the feedback was not received
208208
self.assertEqual(self.feedback, None)
@@ -220,14 +220,14 @@ def test_send_goal_async_with_feedback_for_another_goal(self):
220220
Fibonacci.Goal(),
221221
feedback_callback=self.feedback_callback,
222222
goal_uuid=first_goal_uuid)
223-
rclpy.spin_until_future_complete(self.node, future, self.executor)
223+
rclpy.spin_until_complete(self.node, future, self.executor)
224224

225225
# Send another goal, but without a feedback callback
226226
second_goal_uuid = UUID(uuid=list(uuid.uuid4().bytes))
227227
future = ac.send_goal_async(
228228
Fibonacci.Goal(),
229229
goal_uuid=second_goal_uuid)
230-
rclpy.spin_until_future_complete(self.node, future, self.executor)
230+
rclpy.spin_until_complete(self.node, future, self.executor)
231231

232232
# Publish feedback for the second goal
233233
self.mock_action_server.publish_feedback(second_goal_uuid)
@@ -251,7 +251,7 @@ def test_send_goal_async_with_feedback_for_not_a_goal(self):
251251
Fibonacci.Goal(),
252252
feedback_callback=self.feedback_callback,
253253
goal_uuid=goal_uuid)
254-
rclpy.spin_until_future_complete(self.node, future, self.executor)
254+
rclpy.spin_until_complete(self.node, future, self.executor)
255255

256256
# Publish feedback for a non-existent goal ID
257257
self.mock_action_server.publish_feedback(UUID(uuid=list(uuid.uuid4().bytes)))
@@ -272,9 +272,9 @@ def test_send_goal_multiple(self):
272272
future_0 = ac.send_goal_async(Fibonacci.Goal())
273273
future_1 = ac.send_goal_async(Fibonacci.Goal())
274274
future_2 = ac.send_goal_async(Fibonacci.Goal())
275-
rclpy.spin_until_future_complete(self.node, future_0, executor)
276-
rclpy.spin_until_future_complete(self.node, future_1, executor)
277-
rclpy.spin_until_future_complete(self.node, future_2, executor)
275+
rclpy.spin_until_complete(self.node, future_0, executor)
276+
rclpy.spin_until_complete(self.node, future_1, executor)
277+
rclpy.spin_until_complete(self.node, future_2, executor)
278278
self.assertTrue(future_0.done())
279279
self.assertTrue(future_1.done())
280280
self.assertTrue(future_2.done())
@@ -300,13 +300,13 @@ def test_send_cancel_async(self):
300300

301301
# Send a goal
302302
goal_future = ac.send_goal_async(Fibonacci.Goal())
303-
rclpy.spin_until_future_complete(self.node, goal_future, self.executor)
303+
rclpy.spin_until_complete(self.node, goal_future, self.executor)
304304
self.assertTrue(goal_future.done())
305305
goal_handle = goal_future.result()
306306

307307
# Cancel the goal
308308
cancel_future = goal_handle.cancel_goal_async()
309-
rclpy.spin_until_future_complete(self.node, cancel_future, self.executor)
309+
rclpy.spin_until_complete(self.node, cancel_future, self.executor)
310310
self.assertTrue(cancel_future.done())
311311
self.assertEqual(
312312
cancel_future.result().goals_canceling[0].goal_id,
@@ -321,13 +321,13 @@ def test_get_result_async(self):
321321

322322
# Send a goal
323323
goal_future = ac.send_goal_async(Fibonacci.Goal())
324-
rclpy.spin_until_future_complete(self.node, goal_future, self.executor)
324+
rclpy.spin_until_complete(self.node, goal_future, self.executor)
325325
self.assertTrue(goal_future.done())
326326
goal_handle = goal_future.result()
327327

328328
# Get the goal result
329329
result_future = goal_handle.get_result_async()
330-
rclpy.spin_until_future_complete(self.node, result_future, self.executor)
330+
rclpy.spin_until_complete(self.node, result_future, self.executor)
331331
self.assertTrue(result_future.done())
332332
finally:
333333
ac.destroy()

0 commit comments

Comments
 (0)