Skip to content

Commit

Permalink
Merge pull request #835 from oremanj/unboundcxl
Browse files Browse the repository at this point in the history
Add support for unbound cancel scopes
  • Loading branch information
njsmith committed Jan 28, 2019
2 parents 4756a82 + 5f96b78 commit 960aadc
Show file tree
Hide file tree
Showing 25 changed files with 378 additions and 212 deletions.
2 changes: 1 addition & 1 deletion docs/source/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Deprecations and Removals
~~~~~~~~~~~~~~~~~~~~~~~~~

- Attempting to explicitly raise :exc:`trio.Cancelled` will cause a :exc:`RuntimeError`.
:meth:`cancel_scope.cancel() <trio.The cancel scope interface.cancel>` should
:meth:`cancel_scope.cancel() <trio.CancelScope.cancel>` should
be used instead. (`#342 <https://github.com/python-trio/trio/issues/342>`__)


Expand Down
86 changes: 21 additions & 65 deletions docs/source/reference-core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ whether this scope caught a :exc:`Cancelled` exception::
The ``cancel_scope`` object also allows you to check or adjust this
scope's deadline, explicitly trigger a cancellation without waiting
for the deadline, check if the scope has already been cancelled, and
so forth – see :func:`open_cancel_scope` below for the full details.
so forth – see :class:`CancelScope` below for the full details.

.. _blocking-cleanup-example:

Expand Down Expand Up @@ -415,7 +415,7 @@ Of course, if you really want to make another blocking call in your
cleanup handler, trio will let you; it's trying to prevent you from
accidentally shooting yourself in the foot. Intentional foot-shooting
is no problem (or at least – it's not trio's problem). To do this,
create a new scope, and set its :attr:`~The cancel scope interface.shield`
create a new scope, and set its :attr:`~CancelScope.shield`
attribute to :data:`True`::

with trio.move_on_after(TIMEOUT):
Expand Down Expand Up @@ -494,67 +494,17 @@ but *will* still close the underlying socket before raising
Cancellation API details
~~~~~~~~~~~~~~~~~~~~~~~~

The primitive operation for creating a new cancellation scope is:
:func:`move_on_after` and all the other cancellation facilities provided
by Trio are ultimately implemented in terms of :class:`CancelScope`
objects.

.. autofunction:: open_cancel_scope
:with: cancel_scope

Cancel scope objects provide the following interface:

.. interface:: The cancel scope interface

.. attribute:: deadline

Read-write, :class:`float`. An absolute time on the current
run's clock at which this scope will automatically become
cancelled. You can adjust the deadline by modifying this
attribute, e.g.::

# I need a little more time!
cancel_scope.deadline += 30

Note that for efficiency, the core run loop only checks for
expired deadlines every once in a while. This means that in
certain cases there may be a short delay between when the clock
says the deadline should have expired, and when checkpoints
start raising :exc:`~trio.Cancelled`. This is a very obscure
corner case that you're unlikely to notice, but we document it
for completeness. (If this *does* cause problems for you, of
course, then `we want to know!
<https://github.com/python-trio/trio/issues>`__)

Defaults to :data:`math.inf`, which means "no deadline", though
this can be overridden by the ``deadline=`` argument to
:func:`~trio.open_cancel_scope`.
.. autoclass:: trio.CancelScope

.. attribute:: shield
.. autoattribute:: deadline

Read-write, :class:`bool`, default :data:`False`. So long as
this is set to :data:`True`, then the code inside this scope
will not receive :exc:`~trio.Cancelled` exceptions from scopes
that are outside this scope. They can still receive
:exc:`~trio.Cancelled` exceptions from (1) this scope, or (2)
scopes inside this scope. You can modify this attribute::
.. autoattribute:: shield

with trio.open_cancel_scope() as cancel_scope:
cancel_scope.shield = True
# This cannot be interrupted by any means short of
# killing the process:
await sleep(10)

cancel_scope.shield = False
# Now this can be cancelled normally:
await sleep(10)

Defaults to :data:`False`, though this can be overridden by the
``shield=`` argument to :func:`~trio.open_cancel_scope`.

.. method:: cancel()

Cancels this scope immediately.

This method is idempotent, i.e. if the scope was already
cancelled then this method silently does nothing.
.. automethod:: cancel()

.. attribute:: cancelled_caught

Expand All @@ -564,24 +514,30 @@ Cancel scope objects provide the following interface:
exception, and (2) this scope is the one that was responsible
for triggering this :exc:`~trio.Cancelled` exception.

If the same :class:`CancelScope` is reused for multiple ``with``
blocks, the :attr:`cancelled_caught` attribute applies to the
most recent ``with`` block. (It is reset to :data:`False` each
time a new ``with`` block is entered.)

.. attribute:: cancel_called

Readonly :class:`bool`. Records whether cancellation has been
requested for this scope, either by an explicit call to
:meth:`cancel` or by the deadline expiring.

This attribute being True does *not* necessarily mean that
the code within the scope has been, or will be, affected by
the cancellation. For example, if :meth:`cancel` was called
just before the scope exits, when it's too late to deliver
a :exc:`~trio.Cancelled` exception, then this attribute will
still be True.
This attribute being True does *not* necessarily mean that the
code within the scope has been, or will be, affected by the
cancellation. For example, if :meth:`cancel` was called after
the last checkpoint in the ``with`` block, when it's too late to
deliver a :exc:`~trio.Cancelled` exception, then this attribute
will still be True.

This attribute is mostly useful for debugging and introspection.
If you want to know whether or not a chunk of code was actually
cancelled, then :attr:`cancelled_caught` is usually more
appropriate.


Trio also provides several convenience functions for the common
situation of just wanting to impose a timeout on some code:

Expand Down
4 changes: 4 additions & 0 deletions newsfragments/607.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add support for "unbound cancel scopes": you can now construct a
:class:`trio.CancelScope` without entering its context, e.g., so you
can pass it to another task which will use it to wrap some work that
you want to be able to cancel from afar.
3 changes: 3 additions & 0 deletions newsfragments/607.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Deprecate ``trio.open_cancel_scope`` in favor of :class:`trio.CancelScope`,
which more clearly reflects that creating a cancel scope is just an ordinary
object construction and does not need to be immediately paired with entering it.
20 changes: 8 additions & 12 deletions notes-to-self/graceful-shutdown-idea.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ def start_shutdown(self):
for cancel_scope in self._cancel_scopes:
cancel_scope.cancel()

@contextmanager
def cancel_on_graceful_shutdown(self):
with trio.open_cancel_scope() as cancel_scope:
self._cancel_scopes.add(cancel_scope)
if self._shutting_down:
cancel_scope.cancel()
try:
yield
finally:
self._cancel_scopes.remove(cancel_scope)
cancel_scope = trio.CancelScope()
self._cancel_scopes.add(cancel_scope)
if self._shutting_down:
cancel_scope.cancel()
return cancel_scope

@property
def shutting_down(self):
Expand All @@ -41,16 +37,16 @@ async def stream_handler(stream):

# To trigger the shutdown:
async def listen_for_shutdown_signals():
with trio.catch_signals({signal.SIGINT, signal.SIGTERM}) as signal_aiter:
async for batch in signal_aiter:
with trio.open_signal_receiver(signal.SIGINT, signal.SIGTERM) as signal_aiter:
async for sig in signal_aiter:
gsm.start_shutdown()
break
# TODO: it'd be nice to have some logic like "if we get another
# signal, or if 30 seconds pass, then do a hard shutdown".
# That's easy enough:
#
# with trio.move_on_after(30):
# async for batch in signal_aiter:
# async for sig in signal_aiter:
# break
# sys.exit()
#
Expand Down
4 changes: 2 additions & 2 deletions trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from ._core import (
TrioInternalError, RunFinishedError, WouldBlock, Cancelled,
BusyResourceError, ClosedResourceError, MultiError, run, open_nursery,
open_cancel_scope, current_effective_deadline, TASK_STATUS_IGNORED,
current_time, BrokenResourceError, EndOfChannel
CancelScope, open_cancel_scope, current_effective_deadline,
TASK_STATUS_IGNORED, current_time, BrokenResourceError, EndOfChannel
)

from ._timeouts import (
Expand Down
2 changes: 1 addition & 1 deletion trio/_core/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Cancelled(BaseException):
Attempting to raise :exc:`Cancelled` yourself will cause a
:exc:`RuntimeError`. It would not be associated with a cancel scope and thus
not be caught by Trio. Use
:meth:`cancel_scope.cancel() <trio.The cancel scope interface.cancel>`
:meth:`cancel_scope.cancel() <trio.CancelScope.cancel>`
instead.
.. note::
Expand Down
Loading

0 comments on commit 960aadc

Please sign in to comment.