Skip to content

Commit

Permalink
Change cancel method to never raise (#420)
Browse files Browse the repository at this point in the history
* Change cancel method to never raise

* Fix missing reference in Sphinx docs

* Fix missing word in changelog entry

* Clarify the 'cancel' change

* Reword description of cancel
  • Loading branch information
mdickinson committed Jul 18, 2021
1 parent e556014 commit 37c259f
Show file tree
Hide file tree
Showing 10 changed files with 73 additions and 49 deletions.
7 changes: 7 additions & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ of Traits Futures.
background task types.
* The ``state`` trait of the ``~.TraitsExecutor`` is now read-only;
previously, it was writable.
* The ``cancel`` method of a future no longer raises :exc:`RuntimeError` when a
future is not cancellable. Instead, it communicates the information via its
return value. If a future is already done, or has previously been cancelled,
calling ``cancel`` on that future does not change the state of the future,
and returns ``False``. Otherwise it changes the future's state to
``CANCELLING`` state, requests cancellation of the associated task, and
returns ``True``.
* The ``ITaskSpecification.background_task`` method has been renamed to
``task``.
* The ``ITaskSpecification.future`` method now requires a cancellation callback
Expand Down
3 changes: 1 addition & 2 deletions docs/source/guide/examples/background_processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ def _square_fired(self):

def _cancel_all_fired(self):
for future in self.current_futures:
if future.cancellable:
future.cancel()
future.cancel()

def _clear_finished_fired(self):
for future in list(self.current_futures):
Expand Down
3 changes: 1 addition & 2 deletions docs/source/guide/examples/slow_squares.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ def _square_fired(self):

def _cancel_all_fired(self):
for future in self.current_futures:
if future.cancellable:
future.cancel()
future.cancel()

def _clear_finished_fired(self):
for future in list(self.current_futures):
Expand Down
21 changes: 13 additions & 8 deletions docs/source/guide/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,19 @@ For |ProgressFuture|, the |cancel| method causes a running
task to abort the next time that task calls ``progress``. No further
progress results are received after calling |cancel|.

In all cases, a future may only be cancelled if its state is one of |WAITING|
or |EXECUTING|. Attempting to cancel a future in another state will raise a
|RuntimeError|. Calling |cancel| immediately puts the future into
|CANCELLING| state, and the state is updated to |CANCELLED| once the future has
finished executing. No results or exception information are received from a
future in |CANCELLING| state. A cancelled future will never reach |FAILED|
state, and will never record information from a background task exception that
occurs after the |cancel| call.
In all cases, a task may only be cancelled if the state of the associated
future is either |WAITING| or |EXECUTING|. When |cancel| is called on a future
in one of these two states, the future's state is changed to |CANCELLING|,
a cancellation request is sent to the associated task, and the call returns
``True``. When |cancel| is called on a future in another state, the call has
no effect, and returns ``False``.

A successful |cancel| immediately puts the future into |CANCELLING| state, and
the state is updated to |CANCELLED| once the future has finished executing. No
results or exception information are received from a future in |CANCELLING|
state. A cancelled future will never reach |FAILED| state, and will never
record information from a background task exception that occurs after the
|cancel| call.


Stopping the executor
Expand Down
34 changes: 22 additions & 12 deletions traits_futures/base_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
Base class providing common pieces of the Future machinery.
"""

import logging

from traits.api import (
Any,
Bool,
Expand Down Expand Up @@ -39,6 +41,8 @@
)
from traits_futures.i_future import IFuture

logger = logging.getLogger(__name__)

# Messages sent by the BaseTask, and interpreted by BaseFuture.

#: Custom message from the future. The argument is a pair
Expand Down Expand Up @@ -221,20 +225,26 @@ def cancel(self):
A task in ``WAITING`` or ``EXECUTING`` state will immediately be moved
to ``CANCELLING`` state. If the task is not in ``WAITING`` or
``EXECUTING`` state, this function will raise ``RuntimeError``.
``EXECUTING`` state, this function does nothing.
Raises
------
RuntimeError
If the task has already completed or cancellation has already
been requested.
.. versionchanged:: 0.3.0
This method no longer raises for a task that isn't cancellable.
In previous versions, :exc:`RuntimeError` was raised.
Returns
-------
cancelled : bool
True if the task was cancelled, False if the task was not
cancellable.
"""
if not self.cancellable:
raise RuntimeError(
"Can only cancel a waiting or executing task. "
"Task state is {}".format(self.state)
)
self._user_cancelled()
if self.cancellable:
self._user_cancelled()
logger.debug(f"{self} cancelled")
return True
else:
logger.debug(f"{self} not cancellable; state is {self.state}")
return False

def receive(self, message):
"""
Expand Down
17 changes: 11 additions & 6 deletions traits_futures/i_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,18 @@ def cancel(self):
A task in ``WAITING`` or ``EXECUTING`` state will immediately be moved
to ``CANCELLING`` state. If the task is not in ``WAITING`` or
``EXECUTING`` state, this function will raise ``RuntimeError``.
``EXECUTING`` state, this function does nothing.
Raises
------
RuntimeError
If the task has already completed or cancellation has already
been requested.
.. versionchanged:: 0.3.0
This method no longer raises for a task that isn't cancellable.
In previous versions, :exc:`RuntimeError` was raised.
Returns
-------
cancelled : bool
True if the task was cancelled, False if the task was not
cancellable.
"""

@abc.abstractmethod
Expand Down
20 changes: 11 additions & 9 deletions traits_futures/tests/background_call_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ def test_cannot_cancel_after_success(self):
self.wait_until_done(future)

self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

self.assertResult(future, 8)
self.assertNoException(future)
Expand All @@ -208,8 +208,8 @@ def test_cannot_cancel_after_failure(self):
self.wait_until_done(future)

self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

self.assertNoResult(future)
self.assertException(future, ZeroDivisionError)
Expand All @@ -223,10 +223,12 @@ def test_cannot_cancel_after_cancel(self):
listener = CallFutureListener(future=future)

self.assertTrue(future.cancellable)
future.cancel()
cancelled = future.cancel()
self.assertTrue(cancelled)
self.assertFalse(future.cancellable)
cancelled = future.cancel()
self.assertFalse(cancelled)
self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()

self.wait_until_done(future)

Expand Down Expand Up @@ -254,8 +256,8 @@ def test_double_cancel_variant(self):
test_ready.set()

self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

self.wait_until_done(future)

Expand Down
8 changes: 4 additions & 4 deletions traits_futures/tests/background_iteration_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,17 @@ def test_double_cancel(self):
future.cancel()
self.assertFalse(future.cancellable)

with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

def test_completed_cancel(self):
future = submit_iteration(self.executor, squares, 0, 10)

self.wait_until_done(future)

self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

def test_generator_closed_on_cancellation(self):
resource_acquired = self._context.event()
Expand Down
4 changes: 2 additions & 2 deletions traits_futures/tests/background_progress_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ def test_double_cancellation(self):
future.cancel()

self.assertFalse(future.cancellable)
with self.assertRaises(RuntimeError):
future.cancel()
cancelled = future.cancel()
self.assertFalse(cancelled)

def test_cancel_raising_task(self):
signal = self._context.event()
Expand Down
5 changes: 1 addition & 4 deletions traits_futures/traits_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,7 @@ def _cancel_tasks(self):
logger.debug(f"{self} cancelling incomplete tasks")
cancel_count = 0
for wrapper in self._wrappers:
future = wrapper.future
if future.cancellable:
future.cancel()
cancel_count += 1
cancel_count += wrapper.future.cancel()
logger.debug(f"{self} cancelled {cancel_count} tasks")

def _stop_router(self):
Expand Down

0 comments on commit 37c259f

Please sign in to comment.