initial commit
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Trio - A friendly Python library for async concurrency and I/O"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# General layout:
|
||||
#
|
||||
# trio/_core/... is the self-contained core library. It does various
|
||||
# shenanigans to export a consistent "core API", but parts of the core API are
|
||||
# too low-level to be recommended for regular use.
|
||||
#
|
||||
# trio/*.py define a set of more usable tools on top of this. They import from
|
||||
# trio._core and from each other.
|
||||
#
|
||||
# This file pulls together the friendly public API, by re-exporting the more
|
||||
# innocuous bits of the _core API + the higher-level tools from trio/*.py.
|
||||
#
|
||||
# Uses `from x import y as y` for compatibility with `pyright --verifytypes` (#2625)
|
||||
#
|
||||
# must be imported early to avoid circular import
|
||||
from ._core import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED # isort: split
|
||||
|
||||
# Submodules imported by default
|
||||
from . import abc, from_thread, lowlevel, socket, to_thread
|
||||
from ._channel import (
|
||||
MemoryChannelStatistics as MemoryChannelStatistics,
|
||||
MemoryReceiveChannel as MemoryReceiveChannel,
|
||||
MemorySendChannel as MemorySendChannel,
|
||||
as_safe_channel as as_safe_channel,
|
||||
open_memory_channel as open_memory_channel,
|
||||
)
|
||||
from ._core import (
|
||||
BrokenResourceError as BrokenResourceError,
|
||||
BusyResourceError as BusyResourceError,
|
||||
Cancelled as Cancelled,
|
||||
CancelScope as CancelScope,
|
||||
ClosedResourceError as ClosedResourceError,
|
||||
EndOfChannel as EndOfChannel,
|
||||
Nursery as Nursery,
|
||||
RunFinishedError as RunFinishedError,
|
||||
TaskStatus as TaskStatus,
|
||||
TrioInternalError as TrioInternalError,
|
||||
WouldBlock as WouldBlock,
|
||||
current_effective_deadline as current_effective_deadline,
|
||||
current_time as current_time,
|
||||
open_nursery as open_nursery,
|
||||
run as run,
|
||||
)
|
||||
from ._deprecate import TrioDeprecationWarning as TrioDeprecationWarning
|
||||
from ._dtls import (
|
||||
DTLSChannel as DTLSChannel,
|
||||
DTLSChannelStatistics as DTLSChannelStatistics,
|
||||
DTLSEndpoint as DTLSEndpoint,
|
||||
)
|
||||
from ._file_io import open_file as open_file, wrap_file as wrap_file
|
||||
from ._highlevel_generic import (
|
||||
StapledStream as StapledStream,
|
||||
aclose_forcefully as aclose_forcefully,
|
||||
)
|
||||
from ._highlevel_open_tcp_listeners import (
|
||||
open_tcp_listeners as open_tcp_listeners,
|
||||
serve_tcp as serve_tcp,
|
||||
)
|
||||
from ._highlevel_open_tcp_stream import open_tcp_stream as open_tcp_stream
|
||||
from ._highlevel_open_unix_stream import open_unix_socket as open_unix_socket
|
||||
from ._highlevel_serve_listeners import serve_listeners as serve_listeners
|
||||
from ._highlevel_socket import (
|
||||
SocketListener as SocketListener,
|
||||
SocketStream as SocketStream,
|
||||
)
|
||||
from ._highlevel_ssl_helpers import (
|
||||
open_ssl_over_tcp_listeners as open_ssl_over_tcp_listeners,
|
||||
open_ssl_over_tcp_stream as open_ssl_over_tcp_stream,
|
||||
serve_ssl_over_tcp as serve_ssl_over_tcp,
|
||||
)
|
||||
from ._path import Path as Path, PosixPath as PosixPath, WindowsPath as WindowsPath
|
||||
from ._signals import open_signal_receiver as open_signal_receiver
|
||||
from ._ssl import (
|
||||
NeedHandshakeError as NeedHandshakeError,
|
||||
SSLListener as SSLListener,
|
||||
SSLStream as SSLStream,
|
||||
)
|
||||
from ._subprocess import Process as Process, run_process as run_process
|
||||
from ._sync import (
|
||||
CapacityLimiter as CapacityLimiter,
|
||||
CapacityLimiterStatistics as CapacityLimiterStatistics,
|
||||
Condition as Condition,
|
||||
ConditionStatistics as ConditionStatistics,
|
||||
Event as Event,
|
||||
EventStatistics as EventStatistics,
|
||||
Lock as Lock,
|
||||
LockStatistics as LockStatistics,
|
||||
Semaphore as Semaphore,
|
||||
StrictFIFOLock as StrictFIFOLock,
|
||||
)
|
||||
from ._timeouts import (
|
||||
TooSlowError as TooSlowError,
|
||||
fail_after as fail_after,
|
||||
fail_at as fail_at,
|
||||
move_on_after as move_on_after,
|
||||
move_on_at as move_on_at,
|
||||
sleep as sleep,
|
||||
sleep_forever as sleep_forever,
|
||||
sleep_until as sleep_until,
|
||||
)
|
||||
from ._version import __version__ as __version__
|
||||
|
||||
# Not imported by default, but mentioned here so static analysis tools like
|
||||
# pylint will know that it exists.
|
||||
if TYPE_CHECKING:
|
||||
from . import testing
|
||||
|
||||
from . import _deprecate as _deprecate
|
||||
|
||||
_deprecate.deprecate_attributes(__name__, {})
|
||||
|
||||
# Having the public path in .__module__ attributes is important for:
|
||||
# - exception names in printed tracebacks
|
||||
# - sphinx :show-inheritance:
|
||||
# - deprecation warnings
|
||||
# - pickle
|
||||
# - probably other stuff
|
||||
from ._util import fixup_module_metadata
|
||||
|
||||
fixup_module_metadata(__name__, globals())
|
||||
fixup_module_metadata(lowlevel.__name__, lowlevel.__dict__)
|
||||
fixup_module_metadata(socket.__name__, socket.__dict__)
|
||||
fixup_module_metadata(abc.__name__, abc.__dict__)
|
||||
fixup_module_metadata(from_thread.__name__, from_thread.__dict__)
|
||||
fixup_module_metadata(to_thread.__name__, to_thread.__dict__)
|
||||
del fixup_module_metadata
|
||||
del TYPE_CHECKING
|
||||
@@ -0,0 +1,3 @@
|
||||
from trio._repl import main
|
||||
|
||||
main(locals())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,714 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import trio
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
# both of these introduce circular imports if outside a TYPE_CHECKING guard
|
||||
from ._socket import SocketType
|
||||
from .lowlevel import Task
|
||||
|
||||
|
||||
class Clock(ABC):
|
||||
"""The interface for custom run loop clocks."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def start_clock(self) -> None:
|
||||
"""Do any setup this clock might need.
|
||||
|
||||
Called at the beginning of the run.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def current_time(self) -> float:
|
||||
"""Return the current time, according to this clock.
|
||||
|
||||
This is used to implement functions like :func:`trio.current_time` and
|
||||
:func:`trio.move_on_after`.
|
||||
|
||||
Returns:
|
||||
float: The current time.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def deadline_to_sleep_time(self, deadline: float) -> float:
|
||||
"""Compute the real time until the given deadline.
|
||||
|
||||
This is called before we enter a system-specific wait function like
|
||||
:func:`select.select`, to get the timeout to pass.
|
||||
|
||||
For a clock using wall-time, this should be something like::
|
||||
|
||||
return deadline - self.current_time()
|
||||
|
||||
but of course it may be different if you're implementing some kind of
|
||||
virtual clock.
|
||||
|
||||
Args:
|
||||
deadline (float): The absolute time of the next deadline,
|
||||
according to this clock.
|
||||
|
||||
Returns:
|
||||
float: The number of real seconds to sleep until the given
|
||||
deadline. May be :data:`math.inf`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class Instrument(ABC): # noqa: B024 # conceptually is ABC
|
||||
"""The interface for run loop instrumentation.
|
||||
|
||||
Instruments don't have to inherit from this abstract base class, and all
|
||||
of these methods are optional. This class serves mostly as documentation.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def before_run(self) -> None:
|
||||
"""Called at the beginning of :func:`trio.run`."""
|
||||
return
|
||||
|
||||
def after_run(self) -> None:
|
||||
"""Called just before :func:`trio.run` returns."""
|
||||
return
|
||||
|
||||
def task_spawned(self, task: Task) -> None:
|
||||
"""Called when the given task is created.
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): The new task.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def task_scheduled(self, task: Task) -> None:
|
||||
"""Called when the given task becomes runnable.
|
||||
|
||||
It may still be some time before it actually runs, if there are other
|
||||
runnable tasks ahead of it.
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): The task that became runnable.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def before_task_step(self, task: Task) -> None:
|
||||
"""Called immediately before we resume running the given task.
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): The task that is about to run.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def after_task_step(self, task: Task) -> None:
|
||||
"""Called when we return to the main run loop after a task has yielded.
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): The task that just ran.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def task_exited(self, task: Task) -> None:
|
||||
"""Called when the given task exits.
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): The finished task.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def before_io_wait(self, timeout: float) -> None:
|
||||
"""Called before blocking to wait for I/O readiness.
|
||||
|
||||
Args:
|
||||
timeout (float): The number of seconds we are willing to wait.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
def after_io_wait(self, timeout: float) -> None:
|
||||
"""Called after handling pending I/O.
|
||||
|
||||
Args:
|
||||
timeout (float): The number of seconds we were willing to
|
||||
wait. This much time may or may not have elapsed, depending on
|
||||
whether any I/O was ready.
|
||||
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
class HostnameResolver(ABC):
|
||||
"""If you have a custom hostname resolver, then implementing
|
||||
:class:`HostnameResolver` allows you to register this to be used by Trio.
|
||||
|
||||
See :func:`trio.socket.set_custom_hostname_resolver`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def getaddrinfo(
|
||||
self,
|
||||
host: bytes | None,
|
||||
port: bytes | str | int | None,
|
||||
family: int = 0,
|
||||
type: int = 0,
|
||||
proto: int = 0,
|
||||
flags: int = 0,
|
||||
) -> list[
|
||||
tuple[
|
||||
socket.AddressFamily,
|
||||
socket.SocketKind,
|
||||
int,
|
||||
str,
|
||||
tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes],
|
||||
]
|
||||
]:
|
||||
"""A custom implementation of :func:`~trio.socket.getaddrinfo`.
|
||||
|
||||
Called by :func:`trio.socket.getaddrinfo`.
|
||||
|
||||
If ``host`` is given as a numeric IP address, then
|
||||
:func:`~trio.socket.getaddrinfo` may handle the request itself rather
|
||||
than calling this method.
|
||||
|
||||
Any required IDNA encoding is handled before calling this function;
|
||||
your implementation can assume that it will never see U-labels like
|
||||
``"café.com"``, and only needs to handle A-labels like
|
||||
``b"xn--caf-dma.com"``.""" # spellchecker:disable-line
|
||||
|
||||
@abstractmethod
|
||||
async def getnameinfo(
|
||||
self,
|
||||
sockaddr: tuple[str, int] | tuple[str, int, int, int],
|
||||
flags: int,
|
||||
) -> tuple[str, str]:
|
||||
"""A custom implementation of :func:`~trio.socket.getnameinfo`.
|
||||
|
||||
Called by :func:`trio.socket.getnameinfo`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class SocketFactory(ABC):
|
||||
"""If you write a custom class implementing the Trio socket interface,
|
||||
then you can use a :class:`SocketFactory` to get Trio to use it.
|
||||
|
||||
See :func:`trio.socket.set_custom_socket_factory`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def socket(
|
||||
self,
|
||||
family: socket.AddressFamily | int = socket.AF_INET,
|
||||
type: socket.SocketKind | int = socket.SOCK_STREAM,
|
||||
proto: int = 0,
|
||||
) -> SocketType:
|
||||
"""Create and return a socket object.
|
||||
|
||||
Your socket object must inherit from :class:`trio.socket.SocketType`,
|
||||
which is an empty class whose only purpose is to "mark" which classes
|
||||
should be considered valid Trio sockets.
|
||||
|
||||
Called by :func:`trio.socket.socket`.
|
||||
|
||||
Note that unlike :func:`trio.socket.socket`, this does not take a
|
||||
``fileno=`` argument. If a ``fileno=`` is specified, then
|
||||
:func:`trio.socket.socket` returns a regular Trio socket object
|
||||
instead of calling this method.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class AsyncResource(ABC):
|
||||
"""A standard interface for resources that needs to be cleaned up, and
|
||||
where that cleanup may require blocking operations.
|
||||
|
||||
This class distinguishes between "graceful" closes, which may perform I/O
|
||||
and thus block, and a "forceful" close, which cannot. For example, cleanly
|
||||
shutting down a TLS-encrypted connection requires sending a "goodbye"
|
||||
message; but if a peer has become non-responsive, then sending this
|
||||
message might block forever, so we may want to just drop the connection
|
||||
instead. Therefore the :meth:`aclose` method is unusual in that it
|
||||
should always close the connection (or at least make its best attempt)
|
||||
*even if it fails*; failure indicates a failure to achieve grace, not a
|
||||
failure to close the connection.
|
||||
|
||||
Objects that implement this interface can be used as async context
|
||||
managers, i.e., you can write::
|
||||
|
||||
async with create_resource() as some_async_resource:
|
||||
...
|
||||
|
||||
Entering the context manager is synchronous (not a checkpoint); exiting it
|
||||
calls :meth:`aclose`. The default implementations of
|
||||
``__aenter__`` and ``__aexit__`` should be adequate for all subclasses.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def aclose(self) -> None:
|
||||
"""Close this resource, possibly blocking.
|
||||
|
||||
IMPORTANT: This method may block in order to perform a "graceful"
|
||||
shutdown. But, if this fails, then it still *must* close any
|
||||
underlying resources before returning. An error from this method
|
||||
indicates a failure to achieve grace, *not* a failure to close the
|
||||
connection.
|
||||
|
||||
For example, suppose we call :meth:`aclose` on a TLS-encrypted
|
||||
connection. This requires sending a "goodbye" message; but if the peer
|
||||
has become non-responsive, then our attempt to send this message might
|
||||
block forever, and eventually time out and be cancelled. In this case
|
||||
the :meth:`aclose` method on :class:`~trio.SSLStream` will
|
||||
immediately close the underlying transport stream using
|
||||
:func:`trio.aclose_forcefully` before raising :exc:`~trio.Cancelled`.
|
||||
|
||||
If the resource is already closed, then this method should silently
|
||||
succeed.
|
||||
|
||||
Once this method completes, any other pending or future operations on
|
||||
this resource should generally raise :exc:`~trio.ClosedResourceError`,
|
||||
unless there's a good reason to do otherwise.
|
||||
|
||||
See also: :func:`trio.aclose_forcefully`.
|
||||
|
||||
"""
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
await self.aclose()
|
||||
|
||||
|
||||
class SendStream(AsyncResource):
|
||||
"""A standard interface for sending data on a byte stream.
|
||||
|
||||
The underlying stream may be unidirectional, or bidirectional. If it's
|
||||
bidirectional, then you probably want to also implement
|
||||
:class:`ReceiveStream`, which makes your object a :class:`Stream`.
|
||||
|
||||
:class:`SendStream` objects also implement the :class:`AsyncResource`
|
||||
interface, so they can be closed by calling :meth:`~AsyncResource.aclose`
|
||||
or using an ``async with`` block.
|
||||
|
||||
If you want to send Python objects rather than raw bytes, see
|
||||
:class:`SendChannel`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def send_all(self, data: bytes | bytearray | memoryview) -> None:
|
||||
"""Sends the given data through the stream, blocking if necessary.
|
||||
|
||||
Args:
|
||||
data (bytes, bytearray, or memoryview): The data to send.
|
||||
|
||||
Raises:
|
||||
trio.BusyResourceError: if another task is already executing a
|
||||
:meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
|
||||
:meth:`HalfCloseableStream.send_eof` on this stream.
|
||||
trio.BrokenResourceError: if something has gone wrong, and the stream
|
||||
is broken.
|
||||
trio.ClosedResourceError: if you previously closed this stream
|
||||
object, or if another task closes this stream object while
|
||||
:meth:`send_all` is running.
|
||||
|
||||
Most low-level operations in Trio provide a guarantee: if they raise
|
||||
:exc:`trio.Cancelled`, this means that they had no effect, so the
|
||||
system remains in a known state. This is **not true** for
|
||||
:meth:`send_all`. If this operation raises :exc:`trio.Cancelled` (or
|
||||
any other exception for that matter), then it may have sent some, all,
|
||||
or none of the requested data, and there is no way to know which.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def wait_send_all_might_not_block(self) -> None:
|
||||
"""Block until it's possible that :meth:`send_all` might not block.
|
||||
|
||||
This method may return early: it's possible that after it returns,
|
||||
:meth:`send_all` will still block. (In the worst case, if no better
|
||||
implementation is available, then it might always return immediately
|
||||
without blocking. It's nice to do better than that when possible,
|
||||
though.)
|
||||
|
||||
This method **must not** return *late*: if it's possible for
|
||||
:meth:`send_all` to complete without blocking, then it must
|
||||
return. When implementing it, err on the side of returning early.
|
||||
|
||||
Raises:
|
||||
trio.BusyResourceError: if another task is already executing a
|
||||
:meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
|
||||
:meth:`HalfCloseableStream.send_eof` on this stream.
|
||||
trio.BrokenResourceError: if something has gone wrong, and the stream
|
||||
is broken.
|
||||
trio.ClosedResourceError: if you previously closed this stream
|
||||
object, or if another task closes this stream object while
|
||||
:meth:`wait_send_all_might_not_block` is running.
|
||||
|
||||
Note:
|
||||
|
||||
This method is intended to aid in implementing protocols that want
|
||||
to delay choosing which data to send until the last moment. E.g.,
|
||||
suppose you're working on an implementation of a remote display server
|
||||
like `VNC
|
||||
<https://en.wikipedia.org/wiki/Virtual_Network_Computing>`__, and
|
||||
the network connection is currently backed up so that if you call
|
||||
:meth:`send_all` now then it will sit for 0.5 seconds before actually
|
||||
sending anything. In this case it doesn't make sense to take a
|
||||
screenshot, then wait 0.5 seconds, and then send it, because the
|
||||
screen will keep changing while you wait; it's better to wait 0.5
|
||||
seconds, then take the screenshot, and then send it, because this
|
||||
way the data you deliver will be more
|
||||
up-to-date. Using :meth:`wait_send_all_might_not_block` makes it
|
||||
possible to implement the better strategy.
|
||||
|
||||
If you use this method, you might also want to read up on
|
||||
``TCP_NOTSENT_LOWAT``.
|
||||
|
||||
Further reading:
|
||||
|
||||
* `Prioritization Only Works When There's Pending Data to Prioritize
|
||||
<https://insouciant.org/tech/prioritization-only-works-when-theres-pending-data-to-prioritize/>`__
|
||||
|
||||
* WWDC 2015: Your App and Next Generation Networks: `slides
|
||||
<http://devstreaming.apple.com/videos/wwdc/2015/719ui2k57m/719/719_your_app_and_next_generation_networks.pdf?dl=1>`__,
|
||||
`video and transcript
|
||||
<https://developer.apple.com/videos/play/wwdc2015/719/>`__
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ReceiveStream(AsyncResource):
|
||||
"""A standard interface for receiving data on a byte stream.
|
||||
|
||||
The underlying stream may be unidirectional, or bidirectional. If it's
|
||||
bidirectional, then you probably want to also implement
|
||||
:class:`SendStream`, which makes your object a :class:`Stream`.
|
||||
|
||||
:class:`ReceiveStream` objects also implement the :class:`AsyncResource`
|
||||
interface, so they can be closed by calling :meth:`~AsyncResource.aclose`
|
||||
or using an ``async with`` block.
|
||||
|
||||
If you want to receive Python objects rather than raw bytes, see
|
||||
:class:`ReceiveChannel`.
|
||||
|
||||
`ReceiveStream` objects can be used in ``async for`` loops. Each iteration
|
||||
will produce an arbitrary sized chunk of bytes, like calling
|
||||
`receive_some` with no arguments. Every chunk will contain at least one
|
||||
byte, and the loop automatically exits when reaching end-of-file.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def receive_some(self, max_bytes: int | None = None) -> bytes | bytearray:
|
||||
"""Wait until there is data available on this stream, and then return
|
||||
some of it.
|
||||
|
||||
A return value of ``b""`` (an empty bytestring) indicates that the
|
||||
stream has reached end-of-file. Implementations should be careful that
|
||||
they return ``b""`` if, and only if, the stream has reached
|
||||
end-of-file!
|
||||
|
||||
Args:
|
||||
max_bytes (int): The maximum number of bytes to return. Must be
|
||||
greater than zero. Optional; if omitted, then the stream object
|
||||
is free to pick a reasonable default.
|
||||
|
||||
Returns:
|
||||
bytes or bytearray: The data received.
|
||||
|
||||
Raises:
|
||||
trio.BusyResourceError: if two tasks attempt to call
|
||||
:meth:`receive_some` on the same stream at the same time.
|
||||
trio.BrokenResourceError: if something has gone wrong, and the stream
|
||||
is broken.
|
||||
trio.ClosedResourceError: if you previously closed this stream
|
||||
object, or if another task closes this stream object while
|
||||
:meth:`receive_some` is running.
|
||||
|
||||
"""
|
||||
|
||||
def __aiter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> bytes | bytearray:
|
||||
data = await self.receive_some()
|
||||
if not data:
|
||||
raise StopAsyncIteration
|
||||
return data
|
||||
|
||||
|
||||
class Stream(SendStream, ReceiveStream):
|
||||
"""A standard interface for interacting with bidirectional byte streams.
|
||||
|
||||
A :class:`Stream` is an object that implements both the
|
||||
:class:`SendStream` and :class:`ReceiveStream` interfaces.
|
||||
|
||||
If implementing this interface, you should consider whether you can go one
|
||||
step further and implement :class:`HalfCloseableStream`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class HalfCloseableStream(Stream):
|
||||
"""This interface extends :class:`Stream` to also allow closing the send
|
||||
part of the stream without closing the receive part.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def send_eof(self) -> None:
|
||||
"""Send an end-of-file indication on this stream, if possible.
|
||||
|
||||
The difference between :meth:`send_eof` and
|
||||
:meth:`~AsyncResource.aclose` is that :meth:`send_eof` is a
|
||||
*unidirectional* end-of-file indication. After you call this method,
|
||||
you shouldn't try sending any more data on this stream, and your
|
||||
remote peer should receive an end-of-file indication (eventually,
|
||||
after receiving all the data you sent before that). But, they may
|
||||
continue to send data to you, and you can continue to receive it by
|
||||
calling :meth:`~ReceiveStream.receive_some`. You can think of it as
|
||||
calling :meth:`~AsyncResource.aclose` on just the
|
||||
:class:`SendStream` "half" of the stream object (and in fact that's
|
||||
literally how :class:`trio.StapledStream` implements it).
|
||||
|
||||
Examples:
|
||||
|
||||
* On a socket, this corresponds to ``shutdown(..., SHUT_WR)`` (`man
|
||||
page <https://linux.die.net/man/2/shutdown>`__).
|
||||
|
||||
* The SSH protocol provides the ability to multiplex bidirectional
|
||||
"channels" on top of a single encrypted connection. A Trio
|
||||
implementation of SSH could expose these channels as
|
||||
:class:`HalfCloseableStream` objects, and calling :meth:`send_eof`
|
||||
would send an ``SSH_MSG_CHANNEL_EOF`` request (see `RFC 4254 §5.3
|
||||
<https://tools.ietf.org/html/rfc4254#section-5.3>`__).
|
||||
|
||||
* On an SSL/TLS-encrypted connection, the protocol doesn't provide any
|
||||
way to do a unidirectional shutdown without closing the connection
|
||||
entirely, so :class:`~trio.SSLStream` implements
|
||||
:class:`Stream`, not :class:`HalfCloseableStream`.
|
||||
|
||||
If an EOF has already been sent, then this method should silently
|
||||
succeed.
|
||||
|
||||
Raises:
|
||||
trio.BusyResourceError: if another task is already executing a
|
||||
:meth:`~SendStream.send_all`,
|
||||
:meth:`~SendStream.wait_send_all_might_not_block`, or
|
||||
:meth:`send_eof` on this stream.
|
||||
trio.BrokenResourceError: if something has gone wrong, and the stream
|
||||
is broken.
|
||||
trio.ClosedResourceError: if you previously closed this stream
|
||||
object, or if another task closes this stream object while
|
||||
:meth:`send_eof` is running.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# A regular invariant generic type
|
||||
T = TypeVar("T")
|
||||
|
||||
# The type of object produced by a ReceiveChannel (covariant because
|
||||
# ReceiveChannel[Derived] can be passed to someone expecting
|
||||
# ReceiveChannel[Base])
|
||||
ReceiveType = TypeVar("ReceiveType", covariant=True)
|
||||
|
||||
# The type of object accepted by a SendChannel (contravariant because
|
||||
# SendChannel[Base] can be passed to someone expecting
|
||||
# SendChannel[Derived])
|
||||
SendType = TypeVar("SendType", contravariant=True)
|
||||
|
||||
# The type of object produced by a Listener (covariant plus must be
|
||||
# an AsyncResource)
|
||||
T_resource = TypeVar("T_resource", bound=AsyncResource, covariant=True)
|
||||
|
||||
|
||||
class Listener(AsyncResource, Generic[T_resource]):
|
||||
"""A standard interface for listening for incoming connections.
|
||||
|
||||
:class:`Listener` objects also implement the :class:`AsyncResource`
|
||||
interface, so they can be closed by calling :meth:`~AsyncResource.aclose`
|
||||
or using an ``async with`` block.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def accept(self) -> T_resource:
|
||||
"""Wait until an incoming connection arrives, and then return it.
|
||||
|
||||
Returns:
|
||||
AsyncResource: An object representing the incoming connection. In
|
||||
practice this is generally some kind of :class:`Stream`,
|
||||
but in principle you could also define a :class:`Listener` that
|
||||
returned, say, channel objects.
|
||||
|
||||
Raises:
|
||||
trio.BusyResourceError: if two tasks attempt to call
|
||||
:meth:`accept` on the same listener at the same time.
|
||||
trio.ClosedResourceError: if you previously closed this listener
|
||||
object, or if another task closes this listener object while
|
||||
:meth:`accept` is running.
|
||||
|
||||
Listeners don't generally raise :exc:`~trio.BrokenResourceError`,
|
||||
because for listeners there is no general condition of "the
|
||||
network/remote peer broke the connection" that can be handled in a
|
||||
generic way, like there is for streams. Other errors *can* occur and
|
||||
be raised from :meth:`accept` – for example, if you run out of file
|
||||
descriptors then you might get an :class:`OSError` with its errno set
|
||||
to ``EMFILE``.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class SendChannel(AsyncResource, Generic[SendType]):
|
||||
"""A standard interface for sending Python objects to some receiver.
|
||||
|
||||
`SendChannel` objects also implement the `AsyncResource` interface, so
|
||||
they can be closed by calling `~AsyncResource.aclose` or using an ``async
|
||||
with`` block.
|
||||
|
||||
If you want to send raw bytes rather than Python objects, see
|
||||
`SendStream`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, value: SendType) -> None:
|
||||
"""Attempt to send an object through the channel, blocking if necessary.
|
||||
|
||||
Args:
|
||||
value (object): The object to send.
|
||||
|
||||
Raises:
|
||||
trio.BrokenResourceError: if something has gone wrong, and the
|
||||
channel is broken. For example, you may get this if the receiver
|
||||
has already been closed.
|
||||
trio.ClosedResourceError: if you previously closed this
|
||||
:class:`SendChannel` object, or if another task closes it while
|
||||
:meth:`send` is running.
|
||||
trio.BusyResourceError: some channels allow multiple tasks to call
|
||||
`send` at the same time, but others don't. If you try to call
|
||||
`send` simultaneously from multiple tasks on a channel that
|
||||
doesn't support it, then you can get `~trio.BusyResourceError`.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ReceiveChannel(AsyncResource, Generic[ReceiveType]):
|
||||
"""A standard interface for receiving Python objects from some sender.
|
||||
|
||||
You can iterate over a :class:`ReceiveChannel` using an ``async for``
|
||||
loop::
|
||||
|
||||
async for value in receive_channel:
|
||||
...
|
||||
|
||||
This is equivalent to calling :meth:`receive` repeatedly. The loop exits
|
||||
without error when `receive` raises `~trio.EndOfChannel`.
|
||||
|
||||
`ReceiveChannel` objects also implement the `AsyncResource` interface, so
|
||||
they can be closed by calling `~AsyncResource.aclose` or using an ``async
|
||||
with`` block.
|
||||
|
||||
If you want to receive raw bytes rather than Python objects, see
|
||||
`ReceiveStream`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def receive(self) -> ReceiveType:
|
||||
"""Attempt to receive an incoming object, blocking if necessary.
|
||||
|
||||
Returns:
|
||||
object: Whatever object was received.
|
||||
|
||||
Raises:
|
||||
trio.EndOfChannel: if the sender has been closed cleanly, and no
|
||||
more objects are coming. This is not an error condition.
|
||||
trio.ClosedResourceError: if you previously closed this
|
||||
:class:`ReceiveChannel` object.
|
||||
trio.BrokenResourceError: if something has gone wrong, and the
|
||||
channel is broken.
|
||||
trio.BusyResourceError: some channels allow multiple tasks to call
|
||||
`receive` at the same time, but others don't. If you try to call
|
||||
`receive` simultaneously from multiple tasks on a channel that
|
||||
doesn't support it, then you can get `~trio.BusyResourceError`.
|
||||
|
||||
"""
|
||||
|
||||
def __aiter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> ReceiveType:
|
||||
try:
|
||||
return await self.receive()
|
||||
except trio.EndOfChannel:
|
||||
raise StopAsyncIteration from None
|
||||
|
||||
|
||||
# these are necessary for Sphinx's :show-inheritance: with type args.
|
||||
# (this should be removed if possible)
|
||||
# see: https://github.com/python/cpython/issues/123250
|
||||
SendChannel.__module__ = SendChannel.__module__.replace("_abc", "abc")
|
||||
ReceiveChannel.__module__ = ReceiveChannel.__module__.replace("_abc", "abc")
|
||||
Listener.__module__ = Listener.__module__.replace("_abc", "abc")
|
||||
|
||||
|
||||
class Channel(SendChannel[T], ReceiveChannel[T]):
|
||||
"""A standard interface for interacting with bidirectional channels.
|
||||
|
||||
A `Channel` is an object that implements both the `SendChannel` and
|
||||
`ReceiveChannel` interfaces, so you can both send and receive objects.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
# see above
|
||||
Channel.__module__ = Channel.__module__.replace("_abc", "abc")
|
||||
@@ -0,0 +1,610 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections import OrderedDict, deque
|
||||
from collections.abc import AsyncGenerator, Callable # noqa: TC003 # Needed for Sphinx
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||
from functools import wraps
|
||||
from math import inf
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Generic,
|
||||
)
|
||||
|
||||
import attrs
|
||||
from outcome import Error, Value
|
||||
|
||||
import trio
|
||||
|
||||
from ._abc import ReceiveChannel, ReceiveType, SendChannel, SendType, T
|
||||
from ._core import Abort, BrokenResourceError, RaiseCancelT, Task, enable_ki_protection
|
||||
from ._util import (
|
||||
MultipleExceptionError,
|
||||
NoPublicConstructor,
|
||||
final,
|
||||
raise_single_exception_from_group,
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from typing_extensions import ParamSpec, Self
|
||||
|
||||
P = ParamSpec("P")
|
||||
elif "sphinx.ext.autodoc" in sys.modules:
|
||||
# P needs to exist for Sphinx to parse the type hints successfully.
|
||||
try:
|
||||
from typing_extensions import ParamSpec
|
||||
except ImportError:
|
||||
P = ... # This is valid in Callable, though not correct
|
||||
else:
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
# written as a class so you can say open_memory_channel[int](5)
|
||||
@final
|
||||
class open_memory_channel(tuple["MemorySendChannel[T]", "MemoryReceiveChannel[T]"]):
|
||||
"""Open a channel for passing objects between tasks within a process.
|
||||
|
||||
Memory channels are lightweight, cheap to allocate, and entirely
|
||||
in-memory. They don't involve any operating-system resources, or any kind
|
||||
of serialization. They just pass Python objects directly between tasks
|
||||
(with a possible stop in an internal buffer along the way).
|
||||
|
||||
Channel objects can be closed by calling `~trio.abc.AsyncResource.aclose`
|
||||
or using ``async with``. They are *not* automatically closed when garbage
|
||||
collected. Closing memory channels isn't mandatory, but it is generally a
|
||||
good idea, because it helps avoid situations where tasks get stuck waiting
|
||||
on a channel when there's no-one on the other side. See
|
||||
:ref:`channel-shutdown` for details.
|
||||
|
||||
Memory channel operations are all atomic with respect to
|
||||
cancellation, either `~trio.abc.ReceiveChannel.receive` will
|
||||
successfully return an object, or it will raise :exc:`Cancelled`
|
||||
while leaving the channel unchanged.
|
||||
|
||||
Args:
|
||||
max_buffer_size (int or math.inf): The maximum number of items that can
|
||||
be buffered in the channel before :meth:`~trio.abc.SendChannel.send`
|
||||
blocks. Choosing a sensible value here is important to ensure that
|
||||
backpressure is communicated promptly and avoid unnecessary latency;
|
||||
see :ref:`channel-buffering` for more details. If in doubt, use 0.
|
||||
|
||||
Returns:
|
||||
A pair ``(send_channel, receive_channel)``. If you have
|
||||
trouble remembering which order these go in, remember: data
|
||||
flows from left → right.
|
||||
|
||||
In addition to the standard channel methods, all memory channel objects
|
||||
provide a ``statistics()`` method, which returns an object with the
|
||||
following fields:
|
||||
|
||||
* ``current_buffer_used``: The number of items currently stored in the
|
||||
channel buffer.
|
||||
* ``max_buffer_size``: The maximum number of items allowed in the buffer,
|
||||
as passed to :func:`open_memory_channel`.
|
||||
* ``open_send_channels``: The number of open
|
||||
:class:`MemorySendChannel` endpoints pointing to this channel.
|
||||
Initially 1, but can be increased by
|
||||
:meth:`MemorySendChannel.clone`.
|
||||
* ``open_receive_channels``: Likewise, but for open
|
||||
:class:`MemoryReceiveChannel` endpoints.
|
||||
* ``tasks_waiting_send``: The number of tasks blocked in ``send`` on this
|
||||
channel (summing over all clones).
|
||||
* ``tasks_waiting_receive``: The number of tasks blocked in ``receive`` on
|
||||
this channel (summing over all clones).
|
||||
"""
|
||||
|
||||
def __new__( # type: ignore[misc] # "must return a subtype"
|
||||
cls,
|
||||
max_buffer_size: int | float, # noqa: PYI041
|
||||
) -> tuple[MemorySendChannel[T], MemoryReceiveChannel[T]]:
|
||||
if max_buffer_size != inf and not isinstance(max_buffer_size, int):
|
||||
raise TypeError("max_buffer_size must be an integer or math.inf")
|
||||
if max_buffer_size < 0:
|
||||
raise ValueError("max_buffer_size must be >= 0")
|
||||
state: MemoryChannelState[T] = MemoryChannelState(max_buffer_size)
|
||||
return (
|
||||
MemorySendChannel[T]._create(state),
|
||||
MemoryReceiveChannel[T]._create(state),
|
||||
)
|
||||
|
||||
def __init__(self, max_buffer_size: int | float) -> None: # noqa: PYI041
|
||||
...
|
||||
|
||||
|
||||
@attrs.frozen
|
||||
class MemoryChannelStatistics:
|
||||
current_buffer_used: int
|
||||
max_buffer_size: int | float
|
||||
open_send_channels: int
|
||||
open_receive_channels: int
|
||||
tasks_waiting_send: int
|
||||
tasks_waiting_receive: int
|
||||
|
||||
|
||||
@attrs.define
|
||||
class MemoryChannelState(Generic[T]):
|
||||
max_buffer_size: int | float
|
||||
data: deque[T] = attrs.Factory(deque)
|
||||
# Counts of open endpoints using this state
|
||||
open_send_channels: int = 0
|
||||
open_receive_channels: int = 0
|
||||
# {task: value}
|
||||
send_tasks: OrderedDict[Task, T] = attrs.Factory(OrderedDict)
|
||||
# {task: None}
|
||||
receive_tasks: OrderedDict[Task, None] = attrs.Factory(OrderedDict)
|
||||
|
||||
def statistics(self) -> MemoryChannelStatistics:
|
||||
return MemoryChannelStatistics(
|
||||
current_buffer_used=len(self.data),
|
||||
max_buffer_size=self.max_buffer_size,
|
||||
open_send_channels=self.open_send_channels,
|
||||
open_receive_channels=self.open_receive_channels,
|
||||
tasks_waiting_send=len(self.send_tasks),
|
||||
tasks_waiting_receive=len(self.receive_tasks),
|
||||
)
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False, repr=False, slots=False)
|
||||
class MemorySendChannel(SendChannel[SendType], metaclass=NoPublicConstructor):
|
||||
_state: MemoryChannelState[SendType]
|
||||
_closed: bool = False
|
||||
# This is just the tasks waiting on *this* object. As compared to
|
||||
# self._state.send_tasks, which includes tasks from this object and
|
||||
# all clones.
|
||||
_tasks: set[Task] = attrs.Factory(set)
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self._state.open_send_channels += 1
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<send channel at {id(self):#x}, using buffer at {id(self._state):#x}>"
|
||||
|
||||
def statistics(self) -> MemoryChannelStatistics:
|
||||
"""Returns a `MemoryChannelStatistics` for the memory channel this is
|
||||
associated with."""
|
||||
# XX should we also report statistics specific to this object?
|
||||
return self._state.statistics()
|
||||
|
||||
@enable_ki_protection
|
||||
def send_nowait(self, value: SendType) -> None:
|
||||
"""Like `~trio.abc.SendChannel.send`, but if the channel's buffer is
|
||||
full, raises `WouldBlock` instead of blocking.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise trio.ClosedResourceError
|
||||
if self._state.open_receive_channels == 0:
|
||||
raise trio.BrokenResourceError
|
||||
if self._state.receive_tasks:
|
||||
assert not self._state.data
|
||||
task, _ = self._state.receive_tasks.popitem(last=False)
|
||||
task.custom_sleep_data._tasks.remove(task)
|
||||
trio.lowlevel.reschedule(task, Value(value))
|
||||
elif len(self._state.data) < self._state.max_buffer_size:
|
||||
self._state.data.append(value)
|
||||
else:
|
||||
raise trio.WouldBlock
|
||||
|
||||
@enable_ki_protection
|
||||
async def send(self, value: SendType) -> None:
|
||||
"""See `SendChannel.send <trio.abc.SendChannel.send>`.
|
||||
|
||||
Memory channels allow multiple tasks to call `send` at the same time.
|
||||
|
||||
"""
|
||||
await trio.lowlevel.checkpoint_if_cancelled()
|
||||
try:
|
||||
self.send_nowait(value)
|
||||
except trio.WouldBlock:
|
||||
pass
|
||||
else:
|
||||
await trio.lowlevel.cancel_shielded_checkpoint()
|
||||
return
|
||||
|
||||
task = trio.lowlevel.current_task()
|
||||
self._tasks.add(task)
|
||||
self._state.send_tasks[task] = value
|
||||
task.custom_sleep_data = self
|
||||
|
||||
def abort_fn(_: RaiseCancelT) -> Abort:
|
||||
self._tasks.remove(task)
|
||||
del self._state.send_tasks[task]
|
||||
return trio.lowlevel.Abort.SUCCEEDED
|
||||
|
||||
await trio.lowlevel.wait_task_rescheduled(abort_fn)
|
||||
|
||||
# Return type must be stringified or use a TypeVar
|
||||
@enable_ki_protection
|
||||
def clone(self) -> MemorySendChannel[SendType]:
|
||||
"""Clone this send channel object.
|
||||
|
||||
This returns a new `MemorySendChannel` object, which acts as a
|
||||
duplicate of the original: sending on the new object does exactly the
|
||||
same thing as sending on the old object. (If you're familiar with
|
||||
`os.dup`, then this is a similar idea.)
|
||||
|
||||
However, closing one of the objects does not close the other, and
|
||||
receivers don't get `EndOfChannel` until *all* clones have been
|
||||
closed.
|
||||
|
||||
This is useful for communication patterns that involve multiple
|
||||
producers all sending objects to the same destination. If you give
|
||||
each producer its own clone of the `MemorySendChannel`, and then make
|
||||
sure to close each `MemorySendChannel` when it's finished, receivers
|
||||
will automatically get notified when all producers are finished. See
|
||||
:ref:`channel-mpmc` for examples.
|
||||
|
||||
Raises:
|
||||
trio.ClosedResourceError: if you already closed this
|
||||
`MemorySendChannel` object.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise trio.ClosedResourceError
|
||||
return MemorySendChannel._create(self._state)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.close()
|
||||
|
||||
@enable_ki_protection
|
||||
def close(self) -> None:
|
||||
"""Close this send channel object synchronously.
|
||||
|
||||
All channel objects have an asynchronous `~.AsyncResource.aclose` method.
|
||||
Memory channels can also be closed synchronously. This has the same
|
||||
effect on the channel and other tasks using it, but `close` is not a
|
||||
trio checkpoint. This simplifies cleaning up in cancelled tasks.
|
||||
|
||||
Using ``with send_channel:`` will close the channel object on leaving
|
||||
the with block.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
for task in self._tasks:
|
||||
trio.lowlevel.reschedule(task, Error(trio.ClosedResourceError()))
|
||||
del self._state.send_tasks[task]
|
||||
self._tasks.clear()
|
||||
self._state.open_send_channels -= 1
|
||||
if self._state.open_send_channels == 0:
|
||||
assert not self._state.send_tasks
|
||||
for task in self._state.receive_tasks:
|
||||
task.custom_sleep_data._tasks.remove(task)
|
||||
trio.lowlevel.reschedule(task, Error(trio.EndOfChannel()))
|
||||
self._state.receive_tasks.clear()
|
||||
|
||||
@enable_ki_protection
|
||||
async def aclose(self) -> None:
|
||||
"""Close this send channel object asynchronously.
|
||||
|
||||
See `MemorySendChannel.close`."""
|
||||
self.close()
|
||||
await trio.lowlevel.checkpoint()
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False, repr=False, slots=False)
|
||||
class MemoryReceiveChannel(ReceiveChannel[ReceiveType], metaclass=NoPublicConstructor):
|
||||
_state: MemoryChannelState[ReceiveType]
|
||||
_closed: bool = False
|
||||
_tasks: set[trio._core._run.Task] = attrs.Factory(set)
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self._state.open_receive_channels += 1
|
||||
|
||||
def statistics(self) -> MemoryChannelStatistics:
|
||||
"""Returns a `MemoryChannelStatistics` for the memory channel this is
|
||||
associated with."""
|
||||
return self._state.statistics()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<receive channel at {id(self):#x}, using buffer at {id(self._state):#x}>"
|
||||
)
|
||||
|
||||
@enable_ki_protection
|
||||
def receive_nowait(self) -> ReceiveType:
|
||||
"""Like `~trio.abc.ReceiveChannel.receive`, but if there's nothing
|
||||
ready to receive, raises `WouldBlock` instead of blocking.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise trio.ClosedResourceError
|
||||
if self._state.send_tasks:
|
||||
task, value = self._state.send_tasks.popitem(last=False)
|
||||
task.custom_sleep_data._tasks.remove(task)
|
||||
trio.lowlevel.reschedule(task)
|
||||
self._state.data.append(value)
|
||||
# Fall through
|
||||
if self._state.data:
|
||||
return self._state.data.popleft()
|
||||
if not self._state.open_send_channels:
|
||||
raise trio.EndOfChannel
|
||||
raise trio.WouldBlock
|
||||
|
||||
@enable_ki_protection
|
||||
async def receive(self) -> ReceiveType:
|
||||
"""See `ReceiveChannel.receive <trio.abc.ReceiveChannel.receive>`.
|
||||
|
||||
Memory channels allow multiple tasks to call `receive` at the same
|
||||
time. The first task will get the first item sent, the second task
|
||||
will get the second item sent, and so on.
|
||||
|
||||
"""
|
||||
await trio.lowlevel.checkpoint_if_cancelled()
|
||||
try:
|
||||
value = self.receive_nowait()
|
||||
except trio.WouldBlock:
|
||||
pass
|
||||
else:
|
||||
await trio.lowlevel.cancel_shielded_checkpoint()
|
||||
return value
|
||||
|
||||
task = trio.lowlevel.current_task()
|
||||
self._tasks.add(task)
|
||||
self._state.receive_tasks[task] = None
|
||||
task.custom_sleep_data = self
|
||||
|
||||
def abort_fn(_: RaiseCancelT) -> Abort:
|
||||
self._tasks.remove(task)
|
||||
del self._state.receive_tasks[task]
|
||||
return trio.lowlevel.Abort.SUCCEEDED
|
||||
|
||||
# Not strictly guaranteed to return ReceiveType, but will do so unless
|
||||
# you intentionally reschedule with a bad value.
|
||||
return await trio.lowlevel.wait_task_rescheduled(abort_fn) # type: ignore[no-any-return]
|
||||
|
||||
@enable_ki_protection
|
||||
def clone(self) -> MemoryReceiveChannel[ReceiveType]:
|
||||
"""Clone this receive channel object.
|
||||
|
||||
This returns a new `MemoryReceiveChannel` object, which acts as a
|
||||
duplicate of the original: receiving on the new object does exactly
|
||||
the same thing as receiving on the old object.
|
||||
|
||||
However, closing one of the objects does not close the other, and the
|
||||
underlying channel is not closed until all clones are closed. (If
|
||||
you're familiar with `os.dup`, then this is a similar idea.)
|
||||
|
||||
This is useful for communication patterns that involve multiple
|
||||
consumers all receiving objects from the same underlying channel. See
|
||||
:ref:`channel-mpmc` for examples.
|
||||
|
||||
.. warning:: The clones all share the same underlying channel.
|
||||
Whenever a clone :meth:`receive`\\s a value, it is removed from the
|
||||
channel and the other clones do *not* receive that value. If you
|
||||
want to send multiple copies of the same stream of values to
|
||||
multiple destinations, like :func:`itertools.tee`, then you need to
|
||||
find some other solution; this method does *not* do that.
|
||||
|
||||
Raises:
|
||||
trio.ClosedResourceError: if you already closed this
|
||||
`MemoryReceiveChannel` object.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
raise trio.ClosedResourceError
|
||||
return MemoryReceiveChannel._create(self._state)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.close()
|
||||
|
||||
@enable_ki_protection
|
||||
def close(self) -> None:
|
||||
"""Close this receive channel object synchronously.
|
||||
|
||||
All channel objects have an asynchronous `~.AsyncResource.aclose` method.
|
||||
Memory channels can also be closed synchronously. This has the same
|
||||
effect on the channel and other tasks using it, but `close` is not a
|
||||
trio checkpoint. This simplifies cleaning up in cancelled tasks.
|
||||
|
||||
Using ``with receive_channel:`` will close the channel object on
|
||||
leaving the with block.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
for task in self._tasks:
|
||||
trio.lowlevel.reschedule(task, Error(trio.ClosedResourceError()))
|
||||
del self._state.receive_tasks[task]
|
||||
self._tasks.clear()
|
||||
self._state.open_receive_channels -= 1
|
||||
if self._state.open_receive_channels == 0:
|
||||
assert not self._state.receive_tasks
|
||||
for task in self._state.send_tasks:
|
||||
task.custom_sleep_data._tasks.remove(task)
|
||||
trio.lowlevel.reschedule(task, Error(trio.BrokenResourceError()))
|
||||
self._state.send_tasks.clear()
|
||||
self._state.data.clear()
|
||||
|
||||
@enable_ki_protection
|
||||
async def aclose(self) -> None:
|
||||
"""Close this receive channel object asynchronously.
|
||||
|
||||
See `MemoryReceiveChannel.close`."""
|
||||
self.close()
|
||||
await trio.lowlevel.checkpoint()
|
||||
|
||||
|
||||
class RecvChanWrapper(ReceiveChannel[T]):
|
||||
def __init__(
|
||||
self, recv_chan: MemoryReceiveChannel[T], send_semaphore: trio.Semaphore
|
||||
) -> None:
|
||||
self._recv_chan = recv_chan
|
||||
self._send_semaphore = send_semaphore
|
||||
|
||||
async def receive(self) -> T:
|
||||
self._send_semaphore.release()
|
||||
return await self._recv_chan.receive()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._recv_chan.aclose()
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self._recv_chan.close()
|
||||
|
||||
|
||||
def as_safe_channel(
|
||||
fn: Callable[P, AsyncGenerator[T, None]],
|
||||
) -> Callable[P, AbstractAsyncContextManager[ReceiveChannel[T]]]:
|
||||
"""Decorate an async generator function to make it cancellation-safe.
|
||||
|
||||
The ``yield`` keyword offers a very convenient way to write iterators...
|
||||
which makes it really unfortunate that async generators are so difficult
|
||||
to call correctly. Yielding from the inside of a cancel scope or a nursery
|
||||
to the outside `violates structured concurrency <https://xkcd.com/292/>`_
|
||||
with consequences explained in :pep:`789`. Even then, resource cleanup
|
||||
errors remain common (:pep:`533`) unless you wrap every call in
|
||||
:func:`~contextlib.aclosing`.
|
||||
|
||||
This decorator gives you the best of both worlds: with careful exception
|
||||
handling and a background task we preserve structured concurrency by
|
||||
offering only the safe interface, and you can still write your iterables
|
||||
with the convenience of ``yield``. For example::
|
||||
|
||||
@as_safe_channel
|
||||
async def my_async_iterable(arg, *, kwarg=True):
|
||||
while ...:
|
||||
item = await ...
|
||||
yield item
|
||||
|
||||
async with my_async_iterable(...) as recv_chan:
|
||||
async for item in recv_chan:
|
||||
...
|
||||
|
||||
While the combined async-with-async-for can be inconvenient at first,
|
||||
the context manager is indispensable for both correctness and for prompt
|
||||
cleanup of resources.
|
||||
"""
|
||||
# Perhaps a future PEP will adopt `async with for` syntax, like
|
||||
# https://coconut.readthedocs.io/en/master/DOCS.html#async-with-for
|
||||
|
||||
@asynccontextmanager
|
||||
@wraps(fn)
|
||||
async def context_manager(
|
||||
*args: P.args, **kwargs: P.kwargs
|
||||
) -> AsyncGenerator[trio._channel.RecvChanWrapper[T], None]:
|
||||
send_chan, recv_chan = trio.open_memory_channel[T](0)
|
||||
try:
|
||||
async with trio.open_nursery(strict_exception_groups=True) as nursery:
|
||||
agen = fn(*args, **kwargs)
|
||||
send_semaphore = trio.Semaphore(0)
|
||||
# `nursery.start` to make sure that we will clean up send_chan & agen
|
||||
# If this errors we don't close `recv_chan`, but the caller
|
||||
# never gets access to it, so that's not a problem.
|
||||
await nursery.start(
|
||||
_move_elems_to_channel, agen, send_chan, send_semaphore
|
||||
)
|
||||
# `async with recv_chan` could eat exceptions, so use sync cm
|
||||
with RecvChanWrapper(recv_chan, send_semaphore) as wrapped_recv_chan:
|
||||
yield wrapped_recv_chan
|
||||
# User has exited context manager, cancel to immediately close the
|
||||
# abandoned generator if it's still alive.
|
||||
nursery.cancel_scope.cancel(
|
||||
"exited trio.as_safe_channel context manager"
|
||||
)
|
||||
except BaseExceptionGroup as eg:
|
||||
try:
|
||||
raise_single_exception_from_group(eg)
|
||||
except MultipleExceptionError:
|
||||
# In case user has except* we make it possible for them to handle the
|
||||
# exceptions.
|
||||
if sys.version_info >= (3, 11):
|
||||
eg.add_note(
|
||||
"Encountered exception during cleanup of generator object, as "
|
||||
"well as exception in the contextmanager body - unable to unwrap."
|
||||
)
|
||||
|
||||
raise eg from None
|
||||
|
||||
async def _move_elems_to_channel(
|
||||
agen: AsyncGenerator[T, None],
|
||||
send_chan: trio.MemorySendChannel[T],
|
||||
send_semaphore: trio.Semaphore,
|
||||
task_status: trio.TaskStatus,
|
||||
) -> None:
|
||||
# `async with send_chan` will eat exceptions,
|
||||
# see https://github.com/python-trio/trio/issues/1559
|
||||
with send_chan:
|
||||
# replace try-finally with contextlib.aclosing once python39 is
|
||||
# dropped:
|
||||
try:
|
||||
task_status.started()
|
||||
while True:
|
||||
# wait for receiver to call next on the aiter
|
||||
await send_semaphore.acquire()
|
||||
if not send_chan._state.open_receive_channels:
|
||||
# skip the possibly-expensive computation in the generator,
|
||||
# if we know it will be impossible to send the result.
|
||||
break
|
||||
try:
|
||||
value = await agen.__anext__()
|
||||
except StopAsyncIteration:
|
||||
return
|
||||
# Send the value to the channel
|
||||
try:
|
||||
await send_chan.send(value)
|
||||
except BrokenResourceError:
|
||||
break # closed since we checked above
|
||||
finally:
|
||||
# work around `.aclose()` not suppressing GeneratorExit in an
|
||||
# ExceptionGroup:
|
||||
# TODO: make an issue on CPython about this
|
||||
try:
|
||||
await agen.aclose()
|
||||
except BaseExceptionGroup as exceptions:
|
||||
removed, narrowed_exceptions = exceptions.split(GeneratorExit)
|
||||
|
||||
# TODO: extract a helper to flatten exception groups
|
||||
removed_exceptions: list[BaseException | None] = [removed]
|
||||
genexits_seen = 0
|
||||
for e in removed_exceptions:
|
||||
if isinstance(e, BaseExceptionGroup):
|
||||
removed_exceptions.extend(e.exceptions) # noqa: B909
|
||||
else:
|
||||
genexits_seen += 1
|
||||
|
||||
if genexits_seen > 1:
|
||||
exc = AssertionError("More than one GeneratorExit found.")
|
||||
if narrowed_exceptions is None:
|
||||
narrowed_exceptions = exceptions.derive([exc])
|
||||
else:
|
||||
narrowed_exceptions = narrowed_exceptions.derive(
|
||||
[*narrowed_exceptions.exceptions, exc]
|
||||
)
|
||||
if narrowed_exceptions is not None:
|
||||
raise narrowed_exceptions from None
|
||||
|
||||
return context_manager
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
This namespace represents the core functionality that has to be built-in
|
||||
and deal with private internal data structures. Things in this namespace
|
||||
are publicly available in either trio, trio.lowlevel, or trio.testing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import typing as _t
|
||||
|
||||
from ._entry_queue import TrioToken
|
||||
from ._exceptions import (
|
||||
BrokenResourceError,
|
||||
BusyResourceError,
|
||||
Cancelled,
|
||||
ClosedResourceError,
|
||||
EndOfChannel,
|
||||
RunFinishedError,
|
||||
TrioInternalError,
|
||||
WouldBlock,
|
||||
)
|
||||
from ._ki import currently_ki_protected, disable_ki_protection, enable_ki_protection
|
||||
from ._local import RunVar, RunVarToken
|
||||
from ._mock_clock import MockClock
|
||||
from ._parking_lot import (
|
||||
ParkingLot,
|
||||
ParkingLotStatistics,
|
||||
add_parking_lot_breaker,
|
||||
remove_parking_lot_breaker,
|
||||
)
|
||||
|
||||
# Imports that always exist
|
||||
from ._run import (
|
||||
TASK_STATUS_IGNORED,
|
||||
CancelScope,
|
||||
Nursery,
|
||||
RunStatistics,
|
||||
Task,
|
||||
TaskStatus,
|
||||
add_instrument,
|
||||
checkpoint,
|
||||
checkpoint_if_cancelled,
|
||||
current_clock,
|
||||
current_effective_deadline,
|
||||
current_root_task,
|
||||
current_statistics,
|
||||
current_task,
|
||||
current_time,
|
||||
current_trio_token,
|
||||
in_trio_run,
|
||||
in_trio_task,
|
||||
notify_closing,
|
||||
open_nursery,
|
||||
remove_instrument,
|
||||
reschedule,
|
||||
run,
|
||||
spawn_system_task,
|
||||
start_guest_run,
|
||||
wait_all_tasks_blocked,
|
||||
wait_readable,
|
||||
wait_writable,
|
||||
)
|
||||
from ._thread_cache import start_thread_soon
|
||||
|
||||
# Has to come after _run to resolve a circular import
|
||||
from ._traps import (
|
||||
Abort,
|
||||
RaiseCancelT,
|
||||
cancel_shielded_checkpoint,
|
||||
permanently_detach_coroutine_object,
|
||||
reattach_detached_coroutine_object,
|
||||
temporarily_detach_coroutine_object,
|
||||
wait_task_rescheduled,
|
||||
)
|
||||
from ._unbounded_queue import UnboundedQueue, UnboundedQueueStatistics
|
||||
|
||||
# Windows imports
|
||||
if sys.platform == "win32" or (
|
||||
not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules
|
||||
):
|
||||
from ._run import (
|
||||
current_iocp,
|
||||
monitor_completion_key,
|
||||
readinto_overlapped,
|
||||
register_with_iocp,
|
||||
wait_overlapped,
|
||||
write_overlapped,
|
||||
)
|
||||
# Kqueue imports
|
||||
if (
|
||||
sys.platform != "linux" and sys.platform != "win32" and sys.platform != "android"
|
||||
) or (not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules):
|
||||
from ._run import current_kqueue, monitor_kevent, wait_kevent
|
||||
|
||||
del sys # It would be better to import sys as _sys, but mypy does not understand it
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,243 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, NoReturn, TypeVar
|
||||
|
||||
import attrs
|
||||
|
||||
from .. import _core
|
||||
from .._util import name_asyncgen
|
||||
from . import _run
|
||||
|
||||
# Used to log exceptions in async generator finalizers
|
||||
ASYNCGEN_LOGGER = logging.getLogger("trio.async_generator_errors")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from types import AsyncGeneratorType
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
_WEAK_ASYNC_GEN_SET = weakref.WeakSet[AsyncGeneratorType[object, NoReturn]]
|
||||
_ASYNC_GEN_SET = set[AsyncGeneratorType[object, NoReturn]]
|
||||
else:
|
||||
_WEAK_ASYNC_GEN_SET = weakref.WeakSet
|
||||
_ASYNC_GEN_SET = set
|
||||
|
||||
_R = TypeVar("_R")
|
||||
|
||||
|
||||
@_core.disable_ki_protection
|
||||
def _call_without_ki_protection(
|
||||
f: Callable[_P, _R],
|
||||
/,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> _R:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
|
||||
@attrs.define(eq=False)
|
||||
class AsyncGenerators:
|
||||
# Async generators are added to this set when first iterated. Any
|
||||
# left after the main task exits will be closed before trio.run()
|
||||
# returns. During most of the run, this is a WeakSet so GC works.
|
||||
# During shutdown, when we're finalizing all the remaining
|
||||
# asyncgens after the system nursery has been closed, it's a
|
||||
# regular set so we don't have to deal with GC firing at
|
||||
# unexpected times.
|
||||
alive: _WEAK_ASYNC_GEN_SET | _ASYNC_GEN_SET = attrs.Factory(_WEAK_ASYNC_GEN_SET)
|
||||
# The ids of foreign async generators are added to this set when first
|
||||
# iterated. Usually it is not safe to refer to ids like this, but because
|
||||
# we're using a finalizer we can ensure ids in this set do not outlive
|
||||
# their async generator.
|
||||
foreign: set[int] = attrs.Factory(set)
|
||||
|
||||
# This collects async generators that get garbage collected during
|
||||
# the one-tick window between the system nursery closing and the
|
||||
# init task starting end-of-run asyncgen finalization.
|
||||
trailing_needs_finalize: _ASYNC_GEN_SET = attrs.Factory(_ASYNC_GEN_SET)
|
||||
|
||||
prev_hooks: sys._asyncgen_hooks = attrs.field(init=False)
|
||||
|
||||
def install_hooks(self, runner: _run.Runner) -> None:
|
||||
def firstiter(agen: AsyncGeneratorType[object, NoReturn]) -> None:
|
||||
if hasattr(_run.GLOBAL_RUN_CONTEXT, "task"):
|
||||
self.alive.add(agen)
|
||||
else:
|
||||
# An async generator first iterated outside of a Trio
|
||||
# task doesn't belong to Trio. Probably we're in guest
|
||||
# mode and the async generator belongs to our host.
|
||||
# A strong set of ids is one of the only good places to
|
||||
# remember this fact, at least until
|
||||
# https://github.com/python/cpython/issues/85093 is implemented.
|
||||
self.foreign.add(id(agen))
|
||||
if self.prev_hooks.firstiter is not None:
|
||||
self.prev_hooks.firstiter(agen)
|
||||
|
||||
def finalize_in_trio_context(
|
||||
agen: AsyncGeneratorType[object, NoReturn],
|
||||
agen_name: str,
|
||||
) -> None:
|
||||
try:
|
||||
runner.spawn_system_task(
|
||||
self._finalize_one,
|
||||
agen,
|
||||
agen_name,
|
||||
name=f"close asyncgen {agen_name} (abandoned)",
|
||||
)
|
||||
except RuntimeError:
|
||||
# There is a one-tick window where the system nursery
|
||||
# is closed but the init task hasn't yet made
|
||||
# self.asyncgens a strong set to disable GC. We seem to
|
||||
# have hit it.
|
||||
self.trailing_needs_finalize.add(agen)
|
||||
|
||||
@_core.enable_ki_protection
|
||||
def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
|
||||
try:
|
||||
self.foreign.remove(id(agen))
|
||||
except KeyError:
|
||||
is_ours = True
|
||||
else:
|
||||
is_ours = False
|
||||
|
||||
agen_name = name_asyncgen(agen)
|
||||
if is_ours:
|
||||
runner.entry_queue.run_sync_soon(
|
||||
finalize_in_trio_context,
|
||||
agen,
|
||||
agen_name,
|
||||
)
|
||||
|
||||
# Do this last, because it might raise an exception
|
||||
# depending on the user's warnings filter. (That
|
||||
# exception will be printed to the terminal and
|
||||
# ignored, since we're running in GC context.)
|
||||
warnings.warn(
|
||||
f"Async generator {agen_name!r} was garbage collected before it "
|
||||
"had been exhausted. Surround its use in 'async with "
|
||||
"aclosing(...):' to ensure that it gets cleaned up as soon as "
|
||||
"you're done using it.",
|
||||
ResourceWarning,
|
||||
stacklevel=2,
|
||||
source=agen,
|
||||
)
|
||||
else:
|
||||
# Not ours -> forward to the host loop's async generator finalizer
|
||||
finalizer = self.prev_hooks.finalizer
|
||||
if finalizer is not None:
|
||||
_call_without_ki_protection(finalizer, agen)
|
||||
else:
|
||||
# Host has no finalizer. Reimplement the default
|
||||
# Python behavior with no hooks installed: throw in
|
||||
# GeneratorExit, step once, raise RuntimeError if
|
||||
# it doesn't exit.
|
||||
closer = agen.aclose()
|
||||
try:
|
||||
# If the next thing is a yield, this will raise RuntimeError
|
||||
# which we allow to propagate
|
||||
_call_without_ki_protection(closer.send, None)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
# If the next thing is an await, we get here. Give a nicer
|
||||
# error than the default "async generator ignored GeneratorExit"
|
||||
raise RuntimeError(
|
||||
f"Non-Trio async generator {agen_name!r} awaited something "
|
||||
"during finalization; install a finalization hook to "
|
||||
"support this, or wrap it in 'async with aclosing(...):'",
|
||||
)
|
||||
|
||||
self.prev_hooks = sys.get_asyncgen_hooks()
|
||||
sys.set_asyncgen_hooks(firstiter=firstiter, finalizer=finalizer) # type: ignore[arg-type] # Finalizer doesn't use AsyncGeneratorType
|
||||
|
||||
async def finalize_remaining(self, runner: _run.Runner) -> None:
|
||||
# This is called from init after shutting down the system nursery.
|
||||
# The only tasks running at this point are init and
|
||||
# the run_sync_soon task, and since the system nursery is closed,
|
||||
# there's no way for user code to spawn more.
|
||||
assert _core.current_task() is runner.init_task
|
||||
assert len(runner.tasks) == 2
|
||||
|
||||
# To make async generator finalization easier to reason
|
||||
# about, we'll shut down asyncgen garbage collection by turning
|
||||
# the alive WeakSet into a regular set.
|
||||
self.alive = set(self.alive)
|
||||
|
||||
# Process all pending run_sync_soon callbacks, in case one of
|
||||
# them was an asyncgen finalizer that snuck in under the wire.
|
||||
runner.entry_queue.run_sync_soon(runner.reschedule, runner.init_task)
|
||||
await _core.wait_task_rescheduled(
|
||||
lambda _: _core.Abort.FAILED, # pragma: no cover
|
||||
)
|
||||
self.alive.update(self.trailing_needs_finalize)
|
||||
self.trailing_needs_finalize.clear()
|
||||
|
||||
# None of the still-living tasks use async generators, so
|
||||
# every async generator must be suspended at a yield point --
|
||||
# there's no one to be doing the iteration. That's good,
|
||||
# because aclose() only works on an asyncgen that's suspended
|
||||
# at a yield point. (If it's suspended at an event loop trap,
|
||||
# because someone is in the middle of iterating it, then you
|
||||
# get a RuntimeError on 3.8+, and a nasty surprise on earlier
|
||||
# versions due to https://bugs.python.org/issue32526.)
|
||||
#
|
||||
# However, once we start aclose() of one async generator, it
|
||||
# might start fetching the next value from another, thus
|
||||
# preventing us from closing that other (at least until
|
||||
# aclose() of the first one is complete). This constraint
|
||||
# effectively requires us to finalize the remaining asyncgens
|
||||
# in arbitrary order, rather than doing all of them at the
|
||||
# same time. On 3.8+ we could defer any generator with
|
||||
# ag_running=True to a later batch, but that only catches
|
||||
# the case where our aclose() starts after the user's
|
||||
# asend()/etc. If our aclose() starts first, then the
|
||||
# user's asend()/etc will raise RuntimeError, since they're
|
||||
# probably not checking ag_running.
|
||||
#
|
||||
# It might be possible to allow some parallelized cleanup if
|
||||
# we can determine that a certain set of asyncgens have no
|
||||
# interdependencies, using gc.get_referents() and such.
|
||||
# But just doing one at a time will typically work well enough
|
||||
# (since each aclose() executes in a cancelled scope) and
|
||||
# is much easier to reason about.
|
||||
|
||||
# It's possible that that cleanup code will itself create
|
||||
# more async generators, so we iterate repeatedly until
|
||||
# all are gone.
|
||||
while self.alive:
|
||||
batch = self.alive
|
||||
self.alive = _ASYNC_GEN_SET()
|
||||
for agen in batch:
|
||||
await self._finalize_one(agen, name_asyncgen(agen))
|
||||
|
||||
def close(self) -> None:
|
||||
sys.set_asyncgen_hooks(*self.prev_hooks)
|
||||
|
||||
async def _finalize_one(
|
||||
self,
|
||||
agen: AsyncGeneratorType[object, NoReturn],
|
||||
name: object,
|
||||
) -> None:
|
||||
try:
|
||||
# This shield ensures that finalize_asyncgen never exits
|
||||
# with an exception, not even a Cancelled. The inside
|
||||
# is cancelled so there's no deadlock risk.
|
||||
with _core.CancelScope(shield=True) as cancel_scope:
|
||||
cancel_scope.cancel(
|
||||
reason="disallow async work when closing async generators during trio shutdown"
|
||||
)
|
||||
await agen.aclose()
|
||||
except BaseException:
|
||||
ASYNCGEN_LOGGER.exception(
|
||||
"Exception ignored during finalization of async generator %r -- "
|
||||
"surround your use of the generator in 'async with aclosing(...):' "
|
||||
"to raise exceptions like this in the context where they're generated",
|
||||
name,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import TracebackType
|
||||
|
||||
|
||||
# this is used for collapsing single-exception ExceptionGroups when using
|
||||
# `strict_exception_groups=False`. Once that is retired this function can
|
||||
# be removed as well.
|
||||
def concat_tb(
|
||||
head: TracebackType | None,
|
||||
tail: TracebackType | None,
|
||||
) -> TracebackType | None:
|
||||
# We have to use an iterative algorithm here, because in the worst case
|
||||
# this might be a RecursionError stack that is by definition too deep to
|
||||
# process by recursion!
|
||||
head_tbs = []
|
||||
pointer = head
|
||||
while pointer is not None:
|
||||
head_tbs.append(pointer)
|
||||
pointer = pointer.tb_next
|
||||
current_head = tail
|
||||
for head_tb in reversed(head_tbs):
|
||||
current_head = TracebackType(
|
||||
current_head, head_tb.tb_frame, head_tb.tb_lasti, head_tb.tb_lineno
|
||||
)
|
||||
return current_head
|
||||
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, NoReturn
|
||||
|
||||
import attrs
|
||||
|
||||
from .. import _core
|
||||
from .._util import NoPublicConstructor, final
|
||||
from ._wakeup_socketpair import WakeupSocketpair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
PosArgsT = TypeVarTuple("PosArgsT")
|
||||
|
||||
Function = Callable[..., object] # type: ignore[explicit-any]
|
||||
Job = tuple[Function, tuple[object, ...]]
|
||||
|
||||
|
||||
@attrs.define
|
||||
class EntryQueue:
|
||||
# This used to use a queue.Queue. but that was broken, because Queues are
|
||||
# implemented in Python, and not reentrant -- so it was thread-safe, but
|
||||
# not signal-safe. deque is implemented in C, so each operation is atomic
|
||||
# WRT threads (and this is guaranteed in the docs), AND each operation is
|
||||
# atomic WRT signal delivery (signal handlers can run on either side, but
|
||||
# not *during* a deque operation). dict makes similar guarantees - and
|
||||
# it's even ordered!
|
||||
queue: deque[Job] = attrs.Factory(deque)
|
||||
idempotent_queue: dict[Job, None] = attrs.Factory(dict)
|
||||
|
||||
wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair)
|
||||
done: bool = False
|
||||
# Must be a reentrant lock, because it's acquired from signal handlers.
|
||||
# RLock is signal-safe as of cpython 3.2. NB that this does mean that the
|
||||
# lock is effectively *disabled* when we enter from signal context. The
|
||||
# way we use the lock this is OK though, because when
|
||||
# run_sync_soon is called from a signal it's atomic WRT the
|
||||
# main thread -- it just might happen at some inconvenient place. But if
|
||||
# you look at the one place where the main thread holds the lock, it's
|
||||
# just to make 1 assignment, so that's atomic WRT a signal anyway.
|
||||
lock: threading.RLock = attrs.Factory(threading.RLock)
|
||||
|
||||
async def task(self) -> None:
|
||||
assert _core.currently_ki_protected()
|
||||
# RLock has two implementations: a signal-safe version in _thread, and
|
||||
# and signal-UNsafe version in threading. We need the signal safe
|
||||
# version. Python 3.2 and later should always use this anyway, but,
|
||||
# since the symptoms if this goes wrong are just "weird rare
|
||||
# deadlocks", then let's make a little check.
|
||||
# See:
|
||||
# https://bugs.python.org/issue13697#msg237140
|
||||
assert self.lock.__class__.__module__ == "_thread"
|
||||
|
||||
def run_cb(job: Job) -> None:
|
||||
# We run this with KI protection enabled; it's the callback's
|
||||
# job to disable it if it wants it disabled. Exceptions are
|
||||
# treated like system task exceptions (i.e., converted into
|
||||
# TrioInternalError and cause everything to shut down).
|
||||
sync_fn, args = job
|
||||
try:
|
||||
sync_fn(*args)
|
||||
except BaseException as exc:
|
||||
|
||||
async def kill_everything( # noqa: RUF029 # await not used
|
||||
exc: BaseException,
|
||||
) -> NoReturn:
|
||||
raise exc
|
||||
|
||||
try:
|
||||
_core.spawn_system_task(kill_everything, exc)
|
||||
except RuntimeError:
|
||||
# We're quite late in the shutdown process and the
|
||||
# system nursery is already closed.
|
||||
# TODO(2020-06): this is a gross hack and should
|
||||
# be fixed soon when we address #1607.
|
||||
parent_nursery = _core.current_task().parent_nursery
|
||||
if parent_nursery is None:
|
||||
raise AssertionError(
|
||||
"Internal error: `parent_nursery` should never be `None`",
|
||||
) from exc # pragma: no cover
|
||||
parent_nursery.start_soon(kill_everything, exc)
|
||||
|
||||
# This has to be carefully written to be safe in the face of new items
|
||||
# being queued while we iterate, and to do a bounded amount of work on
|
||||
# each pass:
|
||||
def run_all_bounded() -> None:
|
||||
for _ in range(len(self.queue)):
|
||||
run_cb(self.queue.popleft())
|
||||
for job in list(self.idempotent_queue):
|
||||
del self.idempotent_queue[job]
|
||||
run_cb(job)
|
||||
|
||||
try:
|
||||
while True:
|
||||
run_all_bounded()
|
||||
if not self.queue and not self.idempotent_queue:
|
||||
await self.wakeup.wait_woken()
|
||||
else:
|
||||
await _core.checkpoint()
|
||||
except _core.Cancelled:
|
||||
# Keep the work done with this lock held as minimal as possible,
|
||||
# because it doesn't protect us against concurrent signal delivery
|
||||
# (see the comment above). Notice that this code would still be
|
||||
# correct if written like:
|
||||
# self.done = True
|
||||
# with self.lock:
|
||||
# pass
|
||||
# because all we want is to force run_sync_soon
|
||||
# to either be completely before or completely after the write to
|
||||
# done. That's why we don't need the lock to protect
|
||||
# against signal handlers.
|
||||
with self.lock:
|
||||
self.done = True
|
||||
# No more jobs will be submitted, so just clear out any residual
|
||||
# ones:
|
||||
run_all_bounded()
|
||||
assert not self.queue
|
||||
assert not self.idempotent_queue
|
||||
|
||||
def close(self) -> None:
|
||||
self.wakeup.close()
|
||||
|
||||
def size(self) -> int:
|
||||
return len(self.queue) + len(self.idempotent_queue)
|
||||
|
||||
def run_sync_soon(
|
||||
self,
|
||||
sync_fn: Callable[[Unpack[PosArgsT]], object],
|
||||
*args: Unpack[PosArgsT],
|
||||
idempotent: bool = False,
|
||||
) -> None:
|
||||
with self.lock:
|
||||
if self.done:
|
||||
raise _core.RunFinishedError("run() has exited")
|
||||
# We have to hold the lock all the way through here, because
|
||||
# otherwise the main thread might exit *while* we're doing these
|
||||
# calls, and then our queue item might not be processed, or the
|
||||
# wakeup call might trigger an OSError b/c the IO manager has
|
||||
# already been shut down.
|
||||
if idempotent:
|
||||
self.idempotent_queue[sync_fn, args] = None
|
||||
else:
|
||||
self.queue.append((sync_fn, args))
|
||||
self.wakeup.wakeup_thread_and_signal_safe()
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False)
|
||||
class TrioToken(metaclass=NoPublicConstructor):
|
||||
"""An opaque object representing a single call to :func:`trio.run`.
|
||||
|
||||
It has no public constructor; instead, see :func:`current_trio_token`.
|
||||
|
||||
This object has two uses:
|
||||
|
||||
1. It lets you re-enter the Trio run loop from external threads or signal
|
||||
handlers. This is the low-level primitive that :func:`trio.to_thread`
|
||||
and `trio.from_thread` use to communicate with worker threads, that
|
||||
`trio.open_signal_receiver` uses to receive notifications about
|
||||
signals, and so forth.
|
||||
|
||||
2. Each call to :func:`trio.run` has exactly one associated
|
||||
:class:`TrioToken` object, so you can use it to identify a particular
|
||||
call.
|
||||
|
||||
"""
|
||||
|
||||
_reentry_queue: EntryQueue
|
||||
|
||||
def run_sync_soon(
|
||||
self,
|
||||
sync_fn: Callable[[Unpack[PosArgsT]], object],
|
||||
*args: Unpack[PosArgsT],
|
||||
idempotent: bool = False,
|
||||
) -> None:
|
||||
"""Schedule a call to ``sync_fn(*args)`` to occur in the context of a
|
||||
Trio task.
|
||||
|
||||
This is safe to call from the main thread, from other threads, and
|
||||
from signal handlers. This is the fundamental primitive used to
|
||||
re-enter the Trio run loop from outside of it.
|
||||
|
||||
The call will happen "soon", but there's no guarantee about exactly
|
||||
when, and no mechanism provided for finding out when it's happened.
|
||||
If you need this, you'll have to build your own.
|
||||
|
||||
The call is effectively run as part of a system task (see
|
||||
:func:`~trio.lowlevel.spawn_system_task`). In particular this means
|
||||
that:
|
||||
|
||||
* :exc:`KeyboardInterrupt` protection is *enabled* by default; if
|
||||
you want ``sync_fn`` to be interruptible by control-C, then you
|
||||
need to use :func:`~trio.lowlevel.disable_ki_protection`
|
||||
explicitly.
|
||||
|
||||
* If ``sync_fn`` raises an exception, then it's converted into a
|
||||
:exc:`~trio.TrioInternalError` and *all* tasks are cancelled. You
|
||||
should be careful that ``sync_fn`` doesn't crash.
|
||||
|
||||
All calls with ``idempotent=False`` are processed in strict
|
||||
first-in first-out order.
|
||||
|
||||
If ``idempotent=True``, then ``sync_fn`` and ``args`` must be
|
||||
hashable, and Trio will make a best-effort attempt to discard any
|
||||
call submission which is equal to an already-pending call. Trio
|
||||
will process these in first-in first-out order.
|
||||
|
||||
Any ordering guarantees apply separately to ``idempotent=False``
|
||||
and ``idempotent=True`` calls; there's no rule for how calls in the
|
||||
different categories are ordered with respect to each other.
|
||||
|
||||
:raises trio.RunFinishedError:
|
||||
if the associated call to :func:`trio.run`
|
||||
has already exited. (Any call that *doesn't* raise this error
|
||||
is guaranteed to be fully processed before :func:`trio.run`
|
||||
exits.)
|
||||
|
||||
"""
|
||||
self._reentry_queue.run_sync_soon(sync_fn, *args, idempotent=idempotent)
|
||||
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias
|
||||
|
||||
import attrs
|
||||
|
||||
from trio._util import NoPublicConstructor, final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
CancelReasonLiteral: TypeAlias = Literal[
|
||||
"KeyboardInterrupt",
|
||||
"deadline",
|
||||
"explicit",
|
||||
"nursery",
|
||||
"shutdown",
|
||||
"unknown",
|
||||
]
|
||||
|
||||
|
||||
class TrioInternalError(Exception):
|
||||
"""Raised by :func:`run` if we encounter a bug in Trio, or (possibly) a
|
||||
misuse of one of the low-level :mod:`trio.lowlevel` APIs.
|
||||
|
||||
This should never happen! If you get this error, please file a bug.
|
||||
|
||||
Unfortunately, if you get this error it also means that all bets are off –
|
||||
Trio doesn't know what is going on and its normal invariants may be void.
|
||||
(For example, we might have "lost track" of a task. Or lost track of all
|
||||
tasks.) Again, though, this shouldn't happen.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class RunFinishedError(RuntimeError):
|
||||
"""Raised by `trio.from_thread.run` and similar functions if the
|
||||
corresponding call to :func:`trio.run` has already finished.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class WouldBlock(Exception):
|
||||
"""Raised by ``X_nowait`` functions if ``X`` would block."""
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False, kw_only=True)
|
||||
class Cancelled(BaseException, metaclass=NoPublicConstructor):
|
||||
"""Raised by blocking calls if the surrounding scope has been cancelled.
|
||||
|
||||
You should let this exception propagate, to be caught by the relevant
|
||||
cancel scope. To remind you of this, it inherits from :exc:`BaseException`
|
||||
instead of :exc:`Exception`, just like :exc:`KeyboardInterrupt` and
|
||||
:exc:`SystemExit` do. This means that if you write something like::
|
||||
|
||||
try:
|
||||
...
|
||||
except Exception:
|
||||
...
|
||||
|
||||
then this *won't* catch a :exc:`Cancelled` exception.
|
||||
|
||||
You cannot raise :exc:`Cancelled` yourself. Attempting to do so
|
||||
will produce a :exc:`TypeError`. Use :meth:`cancel_scope.cancel()
|
||||
<trio.CancelScope.cancel>` instead.
|
||||
|
||||
.. note::
|
||||
|
||||
In the US it's also common to see this word spelled "canceled", with
|
||||
only one "l". This is a `recent
|
||||
<https://books.google.com/ngrams/graph?content=canceled%2Ccancelled&year_start=1800&year_end=2000&corpus=5&smoothing=3&direct_url=t1%3B%2Ccanceled%3B%2Cc0%3B.t1%3B%2Ccancelled%3B%2Cc0>`__
|
||||
and `US-specific
|
||||
<https://books.google.com/ngrams/graph?content=canceled%2Ccancelled&year_start=1800&year_end=2000&corpus=18&smoothing=3&share=&direct_url=t1%3B%2Ccanceled%3B%2Cc0%3B.t1%3B%2Ccancelled%3B%2Cc0>`__
|
||||
innovation, and even in the US both forms are still commonly used. So
|
||||
for consistency with the rest of the world and with "cancellation"
|
||||
(which always has two "l"s), Trio uses the two "l" spelling
|
||||
everywhere.
|
||||
|
||||
"""
|
||||
|
||||
source: CancelReasonLiteral = "unknown"
|
||||
# repr(Task), so as to avoid gc troubles from holding a reference
|
||||
source_task: str | None = None
|
||||
reason: str | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"cancelled due to {self.source}"
|
||||
+ ("" if self.reason is None else f" with reason {self.reason!r}")
|
||||
+ ("" if self.source_task is None else f" from task {self.source_task}")
|
||||
)
|
||||
|
||||
def __reduce__(self) -> tuple[Callable[[], Cancelled], tuple[()]]:
|
||||
# The `__reduce__` tuple does not support directly passing kwargs, and the
|
||||
# kwargs are required so we can't use the third item for adding to __dict__,
|
||||
# so we use partial.
|
||||
return (
|
||||
partial(
|
||||
Cancelled._create,
|
||||
source=self.source,
|
||||
source_task=self.source_task,
|
||||
reason=self.reason,
|
||||
),
|
||||
(),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# for type checking on internal code
|
||||
@classmethod
|
||||
def _create(
|
||||
cls,
|
||||
*,
|
||||
source: CancelReasonLiteral = "unknown",
|
||||
source_task: str | None = None,
|
||||
reason: str | None = None,
|
||||
) -> Self: ...
|
||||
|
||||
|
||||
class BusyResourceError(Exception):
|
||||
"""Raised when a task attempts to use a resource that some other task is
|
||||
already using, and this would lead to bugs and nonsense.
|
||||
|
||||
For example, if two tasks try to send data through the same socket at the
|
||||
same time, Trio will raise :class:`BusyResourceError` instead of letting
|
||||
the data get scrambled.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ClosedResourceError(Exception):
|
||||
"""Raised when attempting to use a resource after it has been closed.
|
||||
|
||||
Note that "closed" here means that *your* code closed the resource,
|
||||
generally by calling a method with a name like ``close`` or ``aclose``, or
|
||||
by exiting a context manager. If a problem arises elsewhere – for example,
|
||||
because of a network failure, or because a remote peer closed their end of
|
||||
a connection – then that should be indicated by a different exception
|
||||
class, like :exc:`BrokenResourceError` or an :exc:`OSError` subclass.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class BrokenResourceError(Exception):
|
||||
"""Raised when an attempt to use a resource fails due to external
|
||||
circumstances.
|
||||
|
||||
For example, you might get this if you try to send data on a stream where
|
||||
the remote side has already closed the connection.
|
||||
|
||||
You *don't* get this error if *you* closed the resource – in that case you
|
||||
get :class:`ClosedResourceError`.
|
||||
|
||||
This exception's ``__cause__`` attribute will often contain more
|
||||
information about the underlying error.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class EndOfChannel(Exception):
|
||||
"""Raised when trying to receive from a :class:`trio.abc.ReceiveChannel`
|
||||
that has no more data to receive.
|
||||
|
||||
This is analogous to an "end-of-file" condition, but for channels.
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,50 @@
|
||||
# ***********************************************************
|
||||
# ******* WARNING: AUTOGENERATED! ALL EDITS WILL BE LOST ******
|
||||
# *************************************************************
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._ki import enable_ki_protection
|
||||
from ._run import GLOBAL_RUN_CONTEXT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._instrumentation import Instrument
|
||||
|
||||
__all__ = ["add_instrument", "remove_instrument"]
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def add_instrument(instrument: Instrument) -> None:
|
||||
"""Start instrumenting the current run loop with the given instrument.
|
||||
|
||||
Args:
|
||||
instrument (trio.abc.Instrument): The instrument to activate.
|
||||
|
||||
If ``instrument`` is already active, does nothing.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.instruments.add_instrument(instrument)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def remove_instrument(instrument: Instrument) -> None:
|
||||
"""Stop instrumenting the current run loop with the given instrument.
|
||||
|
||||
Args:
|
||||
instrument (trio.abc.Instrument): The instrument to de-activate.
|
||||
|
||||
Raises:
|
||||
KeyError: if the instrument is not currently active. This could
|
||||
occur either because you never added it, or because you added it
|
||||
and then it raised an unhandled exception and was automatically
|
||||
deactivated.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.instruments.remove_instrument(instrument)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
@@ -0,0 +1,98 @@
|
||||
# ***********************************************************
|
||||
# ******* WARNING: AUTOGENERATED! ALL EDITS WILL BE LOST ******
|
||||
# *************************************************************
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._ki import enable_ki_protection
|
||||
from ._run import GLOBAL_RUN_CONTEXT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._file_io import _HasFileNo
|
||||
|
||||
assert not TYPE_CHECKING or sys.platform == "linux"
|
||||
|
||||
|
||||
__all__ = ["notify_closing", "wait_readable", "wait_writable"]
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_readable(fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is readable.
|
||||
|
||||
On Unix systems, ``fd`` must either be an integer file descriptor,
|
||||
or else an object with a ``.fileno()`` method which returns an
|
||||
integer file descriptor. Any kind of file descriptor can be passed,
|
||||
though the exact semantics will depend on your kernel. For example,
|
||||
this probably won't do anything useful for on-disk files.
|
||||
|
||||
On Windows systems, ``fd`` must either be an integer ``SOCKET``
|
||||
handle, or else an object with a ``.fileno()`` method which returns
|
||||
an integer ``SOCKET`` handle. File descriptors aren't supported,
|
||||
and neither are handles that refer to anything besides a
|
||||
``SOCKET``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become readable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_writable(fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is writable.
|
||||
|
||||
See `wait_readable` for the definition of ``fd``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become writable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def notify_closing(fd: int | _HasFileNo) -> None:
|
||||
"""Notify waiters of the given object that it will be closed.
|
||||
|
||||
Call this before closing a file descriptor (on Unix) or socket (on
|
||||
Windows). This will cause any `wait_readable` or `wait_writable`
|
||||
calls on the given object to immediately wake up and raise
|
||||
`~trio.ClosedResourceError`.
|
||||
|
||||
This doesn't actually close the object – you still have to do that
|
||||
yourself afterwards. Also, you want to be careful to make sure no
|
||||
new tasks start waiting on the object in between when you call this
|
||||
and when it's actually closed. So to close something properly, you
|
||||
usually want to do these steps in order:
|
||||
|
||||
1. Explicitly mark the object as closed, so that any new attempts
|
||||
to use it will abort before they start.
|
||||
2. Call `notify_closing` to wake up any already-existing users.
|
||||
3. Actually close the object.
|
||||
|
||||
It's also possible to do them in a different order if that's more
|
||||
convenient, *but only if* you make sure not to have any checkpoints in
|
||||
between the steps. This way they all happen in a single atomic
|
||||
step, so other tasks won't be able to tell what order they happened
|
||||
in anyway.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
@@ -0,0 +1,153 @@
|
||||
# ***********************************************************
|
||||
# ******* WARNING: AUTOGENERATED! ALL EDITS WILL BE LOST ******
|
||||
# *************************************************************
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._ki import enable_ki_protection
|
||||
from ._run import GLOBAL_RUN_CONTEXT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import select
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from .. import _core
|
||||
from .._file_io import _HasFileNo
|
||||
from ._traps import Abort, RaiseCancelT
|
||||
|
||||
assert not TYPE_CHECKING or sys.platform == "darwin"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"current_kqueue",
|
||||
"monitor_kevent",
|
||||
"notify_closing",
|
||||
"wait_kevent",
|
||||
"wait_readable",
|
||||
"wait_writable",
|
||||
]
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_kqueue() -> select.kqueue:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.current_kqueue()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def monitor_kevent(
|
||||
ident: int, filter: int
|
||||
) -> AbstractContextManager[_core.UnboundedQueue[select.kevent]]:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.monitor_kevent(ident, filter)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_kevent(
|
||||
ident: int, filter: int, abort_func: Callable[[RaiseCancelT], Abort]
|
||||
) -> Abort:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_kevent(
|
||||
ident, filter, abort_func
|
||||
)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_readable(fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is readable.
|
||||
|
||||
On Unix systems, ``fd`` must either be an integer file descriptor,
|
||||
or else an object with a ``.fileno()`` method which returns an
|
||||
integer file descriptor. Any kind of file descriptor can be passed,
|
||||
though the exact semantics will depend on your kernel. For example,
|
||||
this probably won't do anything useful for on-disk files.
|
||||
|
||||
On Windows systems, ``fd`` must either be an integer ``SOCKET``
|
||||
handle, or else an object with a ``.fileno()`` method which returns
|
||||
an integer ``SOCKET`` handle. File descriptors aren't supported,
|
||||
and neither are handles that refer to anything besides a
|
||||
``SOCKET``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become readable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_writable(fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is writable.
|
||||
|
||||
See `wait_readable` for the definition of ``fd``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become writable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def notify_closing(fd: int | _HasFileNo) -> None:
|
||||
"""Notify waiters of the given object that it will be closed.
|
||||
|
||||
Call this before closing a file descriptor (on Unix) or socket (on
|
||||
Windows). This will cause any `wait_readable` or `wait_writable`
|
||||
calls on the given object to immediately wake up and raise
|
||||
`~trio.ClosedResourceError`.
|
||||
|
||||
This doesn't actually close the object – you still have to do that
|
||||
yourself afterwards. Also, you want to be careful to make sure no
|
||||
new tasks start waiting on the object in between when you call this
|
||||
and when it's actually closed. So to close something properly, you
|
||||
usually want to do these steps in order:
|
||||
|
||||
1. Explicitly mark the object as closed, so that any new attempts
|
||||
to use it will abort before they start.
|
||||
2. Call `notify_closing` to wake up any already-existing users.
|
||||
3. Actually close the object.
|
||||
|
||||
It's also possible to do them in a different order if that's more
|
||||
convenient, *but only if* you make sure not to have any checkpoints in
|
||||
between the steps. This way they all happen in a single atomic
|
||||
step, so other tasks won't be able to tell what order they happened
|
||||
in anyway.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(fd)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
@@ -0,0 +1,204 @@
|
||||
# ***********************************************************
|
||||
# ******* WARNING: AUTOGENERATED! ALL EDITS WILL BE LOST ******
|
||||
# *************************************************************
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._ki import enable_ki_protection
|
||||
from ._run import GLOBAL_RUN_CONTEXT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from .._file_io import _HasFileNo
|
||||
from ._unbounded_queue import UnboundedQueue
|
||||
from ._windows_cffi import CData, Handle
|
||||
|
||||
assert not TYPE_CHECKING or sys.platform == "win32"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"current_iocp",
|
||||
"monitor_completion_key",
|
||||
"notify_closing",
|
||||
"readinto_overlapped",
|
||||
"register_with_iocp",
|
||||
"wait_overlapped",
|
||||
"wait_readable",
|
||||
"wait_writable",
|
||||
"write_overlapped",
|
||||
]
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_readable(sock: _HasFileNo | int) -> None:
|
||||
"""Block until the kernel reports that the given object is readable.
|
||||
|
||||
On Unix systems, ``sock`` must either be an integer file descriptor,
|
||||
or else an object with a ``.fileno()`` method which returns an
|
||||
integer file descriptor. Any kind of file descriptor can be passed,
|
||||
though the exact semantics will depend on your kernel. For example,
|
||||
this probably won't do anything useful for on-disk files.
|
||||
|
||||
On Windows systems, ``sock`` must either be an integer ``SOCKET``
|
||||
handle, or else an object with a ``.fileno()`` method which returns
|
||||
an integer ``SOCKET`` handle. File descriptors aren't supported,
|
||||
and neither are handles that refer to anything besides a
|
||||
``SOCKET``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become readable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(sock)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_writable(sock: _HasFileNo | int) -> None:
|
||||
"""Block until the kernel reports that the given object is writable.
|
||||
|
||||
See `wait_readable` for the definition of ``sock``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become writable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(sock)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def notify_closing(handle: Handle | int | _HasFileNo) -> None:
|
||||
"""Notify waiters of the given object that it will be closed.
|
||||
|
||||
Call this before closing a file descriptor (on Unix) or socket (on
|
||||
Windows). This will cause any `wait_readable` or `wait_writable`
|
||||
calls on the given object to immediately wake up and raise
|
||||
`~trio.ClosedResourceError`.
|
||||
|
||||
This doesn't actually close the object – you still have to do that
|
||||
yourself afterwards. Also, you want to be careful to make sure no
|
||||
new tasks start waiting on the object in between when you call this
|
||||
and when it's actually closed. So to close something properly, you
|
||||
usually want to do these steps in order:
|
||||
|
||||
1. Explicitly mark the object as closed, so that any new attempts
|
||||
to use it will abort before they start.
|
||||
2. Call `notify_closing` to wake up any already-existing users.
|
||||
3. Actually close the object.
|
||||
|
||||
It's also possible to do them in a different order if that's more
|
||||
convenient, *but only if* you make sure not to have any checkpoints in
|
||||
between the steps. This way they all happen in a single atomic
|
||||
step, so other tasks won't be able to tell what order they happened
|
||||
in anyway.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(handle)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def register_with_iocp(handle: int | CData) -> None:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.register_with_iocp(handle)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_overlapped(handle_: int | CData, lpOverlapped: CData | int) -> object:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_overlapped(
|
||||
handle_, lpOverlapped
|
||||
)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def write_overlapped(
|
||||
handle: int | CData, data: Buffer, file_offset: int = 0
|
||||
) -> int:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.write_overlapped(
|
||||
handle, data, file_offset
|
||||
)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def readinto_overlapped(
|
||||
handle: int | CData, buffer: Buffer, file_offset: int = 0
|
||||
) -> int:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.io_manager.readinto_overlapped(
|
||||
handle, buffer, file_offset
|
||||
)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_iocp() -> int:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.current_iocp()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def monitor_completion_key() -> (
|
||||
AbstractContextManager[tuple[int, UnboundedQueue[object]]]
|
||||
):
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__ and `#52
|
||||
<https://github.com/python-trio/trio/issues/52>`__.
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.io_manager.monitor_completion_key()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
@@ -0,0 +1,269 @@
|
||||
# ***********************************************************
|
||||
# ******* WARNING: AUTOGENERATED! ALL EDITS WILL BE LOST ******
|
||||
# *************************************************************
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._ki import enable_ki_protection
|
||||
from ._run import _NO_SEND, GLOBAL_RUN_CONTEXT, RunStatistics, Task
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import contextvars
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
import outcome
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from .._abc import Clock
|
||||
from ._entry_queue import TrioToken
|
||||
from ._run import PosArgT
|
||||
|
||||
|
||||
__all__ = [
|
||||
"current_clock",
|
||||
"current_root_task",
|
||||
"current_statistics",
|
||||
"current_time",
|
||||
"current_trio_token",
|
||||
"reschedule",
|
||||
"spawn_system_task",
|
||||
"wait_all_tasks_blocked",
|
||||
]
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_statistics() -> RunStatistics:
|
||||
"""Returns ``RunStatistics``, which contains run-loop-level debugging information.
|
||||
|
||||
Currently, the following fields are defined:
|
||||
|
||||
* ``tasks_living`` (int): The number of tasks that have been spawned
|
||||
and not yet exited.
|
||||
* ``tasks_runnable`` (int): The number of tasks that are currently
|
||||
queued on the run queue (as opposed to blocked waiting for something
|
||||
to happen).
|
||||
* ``seconds_to_next_deadline`` (float): The time until the next
|
||||
pending cancel scope deadline. May be negative if the deadline has
|
||||
expired but we haven't yet processed cancellations. May be
|
||||
:data:`~math.inf` if there are no pending deadlines.
|
||||
* ``run_sync_soon_queue_size`` (int): The number of
|
||||
unprocessed callbacks queued via
|
||||
:meth:`trio.lowlevel.TrioToken.run_sync_soon`.
|
||||
* ``io_statistics`` (object): Some statistics from Trio's I/O
|
||||
backend. This always has an attribute ``backend`` which is a string
|
||||
naming which operating-system-specific I/O backend is in use; the
|
||||
other attributes vary between backends.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.current_statistics()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_time() -> float:
|
||||
"""Returns the current time according to Trio's internal clock.
|
||||
|
||||
Returns:
|
||||
float: The current time.
|
||||
|
||||
Raises:
|
||||
RuntimeError: if not inside a call to :func:`trio.run`.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.current_time()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_clock() -> Clock:
|
||||
"""Returns the current :class:`~trio.abc.Clock`."""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.current_clock()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_root_task() -> Task | None:
|
||||
"""Returns the current root :class:`Task`.
|
||||
|
||||
This is the task that is the ultimate parent of all other tasks.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.current_root_task()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def reschedule(task: Task, next_send: outcome.Outcome[object] = _NO_SEND) -> None:
|
||||
"""Reschedule the given task with the given
|
||||
:class:`outcome.Outcome`.
|
||||
|
||||
See :func:`wait_task_rescheduled` for the gory details.
|
||||
|
||||
There must be exactly one call to :func:`reschedule` for every call to
|
||||
:func:`wait_task_rescheduled`. (And when counting, keep in mind that
|
||||
returning :data:`Abort.SUCCEEDED` from an abort callback is equivalent
|
||||
to calling :func:`reschedule` once.)
|
||||
|
||||
Args:
|
||||
task (trio.lowlevel.Task): the task to be rescheduled. Must be blocked
|
||||
in a call to :func:`wait_task_rescheduled`.
|
||||
next_send (outcome.Outcome): the value (or error) to return (or
|
||||
raise) from :func:`wait_task_rescheduled`.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.reschedule(task, next_send)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def spawn_system_task(
|
||||
async_fn: Callable[[Unpack[PosArgT]], Awaitable[object]],
|
||||
*args: Unpack[PosArgT],
|
||||
name: object = None,
|
||||
context: contextvars.Context | None = None,
|
||||
) -> Task:
|
||||
"""Spawn a "system" task.
|
||||
|
||||
System tasks have a few differences from regular tasks:
|
||||
|
||||
* They don't need an explicit nursery; instead they go into the
|
||||
internal "system nursery".
|
||||
|
||||
* If a system task raises an exception, then it's converted into a
|
||||
:exc:`~trio.TrioInternalError` and *all* tasks are cancelled. If you
|
||||
write a system task, you should be careful to make sure it doesn't
|
||||
crash.
|
||||
|
||||
* System tasks are automatically cancelled when the main task exits.
|
||||
|
||||
* By default, system tasks have :exc:`KeyboardInterrupt` protection
|
||||
*enabled*. If you want your task to be interruptible by control-C,
|
||||
then you need to use :func:`disable_ki_protection` explicitly (and
|
||||
come up with some plan for what to do with a
|
||||
:exc:`KeyboardInterrupt`, given that system tasks aren't allowed to
|
||||
raise exceptions).
|
||||
|
||||
* System tasks do not inherit context variables from their creator.
|
||||
|
||||
Towards the end of a call to :meth:`trio.run`, after the main
|
||||
task and all system tasks have exited, the system nursery
|
||||
becomes closed. At this point, new calls to
|
||||
:func:`spawn_system_task` will raise ``RuntimeError("Nursery
|
||||
is closed to new arrivals")`` instead of creating a system
|
||||
task. It's possible to encounter this state either in
|
||||
a ``finally`` block in an async generator, or in a callback
|
||||
passed to :meth:`TrioToken.run_sync_soon` at the right moment.
|
||||
|
||||
Args:
|
||||
async_fn: An async callable.
|
||||
args: Positional arguments for ``async_fn``. If you want to pass
|
||||
keyword arguments, use :func:`functools.partial`.
|
||||
name: The name for this task. Only used for debugging/introspection
|
||||
(e.g. ``repr(task_obj)``). If this isn't a string,
|
||||
:func:`spawn_system_task` will try to make it one. A common use
|
||||
case is if you're wrapping a function before spawning a new
|
||||
task, you might pass the original function as the ``name=`` to
|
||||
make debugging easier.
|
||||
context: An optional ``contextvars.Context`` object with context variables
|
||||
to use for this task. You would normally get a copy of the current
|
||||
context with ``context = contextvars.copy_context()`` and then you would
|
||||
pass that ``context`` object here.
|
||||
|
||||
Returns:
|
||||
Task: the newly spawned task
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.spawn_system_task(
|
||||
async_fn, *args, name=name, context=context
|
||||
)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
def current_trio_token() -> TrioToken:
|
||||
"""Retrieve the :class:`TrioToken` for the current call to
|
||||
:func:`trio.run`.
|
||||
|
||||
"""
|
||||
try:
|
||||
return GLOBAL_RUN_CONTEXT.runner.current_trio_token()
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
|
||||
|
||||
@enable_ki_protection
|
||||
async def wait_all_tasks_blocked(cushion: float = 0.0) -> None:
|
||||
"""Block until there are no runnable tasks.
|
||||
|
||||
This is useful in testing code when you want to give other tasks a
|
||||
chance to "settle down". The calling task is blocked, and doesn't wake
|
||||
up until all other tasks are also blocked for at least ``cushion``
|
||||
seconds. (Setting a non-zero ``cushion`` is intended to handle cases
|
||||
like two tasks talking to each other over a local socket, where we
|
||||
want to ignore the potential brief moment between a send and receive
|
||||
when all tasks are blocked.)
|
||||
|
||||
Note that ``cushion`` is measured in *real* time, not the Trio clock
|
||||
time.
|
||||
|
||||
If there are multiple tasks blocked in :func:`wait_all_tasks_blocked`,
|
||||
then the one with the shortest ``cushion`` is the one woken (and
|
||||
this task becoming unblocked resets the timers for the remaining
|
||||
tasks). If there are multiple tasks that have exactly the same
|
||||
``cushion``, then all are woken.
|
||||
|
||||
You should also consider :class:`trio.testing.Sequencer`, which
|
||||
provides a more explicit way to control execution ordering within a
|
||||
test, and will often produce more readable tests.
|
||||
|
||||
Example:
|
||||
Here's an example of one way to test that Trio's locks are fair: we
|
||||
take the lock in the parent, start a child, wait for the child to be
|
||||
blocked waiting for the lock (!), and then check that we can't
|
||||
release and immediately re-acquire the lock::
|
||||
|
||||
async def lock_taker(lock):
|
||||
await lock.acquire()
|
||||
lock.release()
|
||||
|
||||
async def test_lock_fairness():
|
||||
lock = trio.Lock()
|
||||
await lock.acquire()
|
||||
async with trio.open_nursery() as nursery:
|
||||
nursery.start_soon(lock_taker, lock)
|
||||
# child hasn't run yet, we have the lock
|
||||
assert lock.locked()
|
||||
assert lock._owner is trio.lowlevel.current_task()
|
||||
await trio.testing.wait_all_tasks_blocked()
|
||||
# now the child has run and is blocked on lock.acquire(), we
|
||||
# still have the lock
|
||||
assert lock.locked()
|
||||
assert lock._owner is trio.lowlevel.current_task()
|
||||
lock.release()
|
||||
try:
|
||||
# The child has a prior claim, so we can't have it
|
||||
lock.acquire_nowait()
|
||||
except trio.WouldBlock:
|
||||
assert lock._owner is not trio.lowlevel.current_task()
|
||||
print("PASS")
|
||||
else:
|
||||
print("FAIL")
|
||||
|
||||
"""
|
||||
try:
|
||||
return await GLOBAL_RUN_CONTEXT.runner.wait_all_tasks_blocked(cushion)
|
||||
except AttributeError:
|
||||
raise RuntimeError("must be called from async context") from None
|
||||
@@ -0,0 +1,10 @@
|
||||
# auto-generated file
|
||||
import _cffi_backend
|
||||
|
||||
ffi = _cffi_backend.FFI('trio._core._generated_windows_ffi',
|
||||
_version = 0x2601,
|
||||
_types = b'\x00\x00\x39\x0D\x00\x00\x1A\x01\x00\x00\x0A\x01\x00\x00\x72\x03\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x02\x03\x00\x00\x6D\x03\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x04\x01\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x72\x03\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x08\x11\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x6E\x03\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x0A\x01\x00\x00\x07\x01\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x07\x01\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x1A\x01\x00\x00\x08\x11\x00\x00\x02\x0F\x00\x00\x02\x0D\x00\x00\x08\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x0A\x01\x00\x00\x03\x03\x00\x00\x07\x01\x00\x00\x0A\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x03\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x73\x03\x00\x00\x0A\x01\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x03\x11\x00\x00\x03\x11\x00\x00\x1A\x01\x00\x00\x0A\x01\x00\x00\x02\x0F\x00\x00\x68\x03\x00\x00\x02\x09\x00\x00\x68\x05\x00\x00\x00\x01\x00\x00\x6C\x03\x00\x00\x03\x09\x00\x00\x04\x09\x00\x00\x05\x09\x00\x00\x17\x01\x00\x00\x01\x09\x00\x00\x00\x09\x00\x00\x00\x01\x00\x00\x10\x01',
|
||||
_globals = (b'\x00\x00\x2F\x23CancelIoEx',0,b'\x00\x00\x2C\x23CloseHandle',0,b'\x00\x00\x52\x23CreateEventA',0,b'\x00\x00\x58\x23CreateFileW',0,b'\x00\x00\x61\x23CreateIoCompletionPort',0,b'\x00\x00\x12\x23DeviceIoControl',0,b'\x00\x00\x33\x23GetQueuedCompletionStatusEx',0,b'\x00\x00\x3F\x23PostQueuedCompletionStatus',0,b'\x00\x00\x1C\x23ReadFile',0,b'\x00\x00\x0B\x23ResetEvent',0,b'\x00\x00\x45\x23RtlNtStatusToDosError',0,b'\x00\x00\x3B\x23SetConsoleCtrlHandler',0,b'\x00\x00\x0B\x23SetEvent',0,b'\x00\x00\x0E\x23SetFileCompletionNotificationModes',0,b'\x00\x00\x2A\x23WSAGetLastError',0,b'\x00\x00\x00\x23WSAIoctl',0,b'\x00\x00\x48\x23WaitForMultipleObjects',0,b'\x00\x00\x4E\x23WaitForSingleObject',0,b'\x00\x00\x23\x23WriteFile',0),
|
||||
_struct_unions = ((b'\x00\x00\x00\x71\x00\x00\x00\x03$1',b'\x00\x00\x70\x11DUMMYSTRUCTNAME',b'\x00\x00\x03\x11Pointer'),(b'\x00\x00\x00\x70\x00\x00\x00\x02$2',b'\x00\x00\x02\x11Offset',b'\x00\x00\x02\x11OffsetHigh'),(b'\x00\x00\x00\x68\x00\x00\x00\x02_AFD_POLL_HANDLE_INFO',b'\x00\x00\x03\x11Handle',b'\x00\x00\x02\x11Events',b'\x00\x00\x46\x11Status'),(b'\x00\x00\x00\x6C\x00\x00\x00\x02_AFD_POLL_INFO',b'\x00\x00\x6F\x11Timeout',b'\x00\x00\x02\x11NumberOfHandles',b'\x00\x00\x02\x11Exclusive',b'\x00\x00\x69\x11Handles'),(b'\x00\x00\x00\x6D\x00\x00\x00\x02_OVERLAPPED',b'\x00\x00\x01\x11Internal',b'\x00\x00\x01\x11InternalHigh',b'\x00\x00\x71\x11DUMMYUNIONNAME',b'\x00\x00\x03\x11hEvent'),(b'\x00\x00\x00\x6E\x00\x00\x00\x02_OVERLAPPED_ENTRY',b'\x00\x00\x01\x11lpCompletionKey',b'\x00\x00\x08\x11lpOverlapped',b'\x00\x00\x01\x11Internal',b'\x00\x00\x02\x11dwNumberOfBytesTransferred')),
|
||||
_typenames = (b'\x00\x00\x00\x68AFD_POLL_HANDLE_INFO',b'\x00\x00\x00\x6CAFD_POLL_INFO',b'\x00\x00\x00\x39BOOL',b'\x00\x00\x00\x10BOOLEAN',b'\x00\x00\x00\x10BYTE',b'\x00\x00\x00\x02DWORD',b'\x00\x00\x00\x03HANDLE',b'\x00\x00\x00\x6FLARGE_INTEGER',b'\x00\x00\x00\x03LPCSTR',b'\x00\x00\x00\x25LPCVOID',b'\x00\x00\x00\x59LPCWSTR',b'\x00\x00\x00\x07LPDWORD',b'\x00\x00\x00\x08LPOVERLAPPED',b'\x00\x00\x00\x35LPOVERLAPPED_ENTRY',b'\x00\x00\x00\x03LPSECURITY_ATTRIBUTES',b'\x00\x00\x00\x03LPVOID',b'\x00\x00\x00\x08LPWSAOVERLAPPED',b'\x00\x00\x00\x46NTSTATUS',b'\x00\x00\x00\x6DOVERLAPPED',b'\x00\x00\x00\x6EOVERLAPPED_ENTRY',b'\x00\x00\x00\x67PAFD_POLL_HANDLE_INFO',b'\x00\x00\x00\x6BPAFD_POLL_INFO',b'\x00\x00\x00\x07PULONG',b'\x00\x00\x00\x03PVOID',b'\x00\x00\x00\x01SOCKET',b'\x00\x00\x00\x10UCHAR',b'\x00\x00\x00\x01UINT_PTR',b'\x00\x00\x00\x02ULONG',b'\x00\x00\x00\x01ULONG_PTR',b'\x00\x00\x00\x6DWSAOVERLAPPED',b'\x00\x00\x00\x02u_long'),
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import types
|
||||
from collections import UserDict
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from .._abc import Instrument
|
||||
|
||||
# Used to log exceptions in instruments
|
||||
INSTRUMENT_LOGGER = logging.getLogger("trio.abc.Instrument")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# Decorator to mark methods public. This does nothing by itself, but
|
||||
# trio/_tools/gen_exports.py looks for it.
|
||||
def _public(fn: T) -> T:
|
||||
return fn
|
||||
|
||||
|
||||
class Instruments(UserDict[str, dict[Instrument, None]]):
|
||||
"""A collection of `trio.abc.Instrument` organized by hook.
|
||||
|
||||
Instrumentation calls are rather expensive, and we don't want a
|
||||
rarely-used instrument (like before_run()) to slow down hot
|
||||
operations (like before_task_step()). Thus, we cache the set of
|
||||
instruments to be called for each hook, and skip the instrumentation
|
||||
call if there's nothing currently installed for that hook.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, incoming: Sequence[Instrument]) -> None:
|
||||
super().__init__({"_all": {}})
|
||||
for instrument in incoming:
|
||||
self.add_instrument(instrument)
|
||||
|
||||
@_public
|
||||
def add_instrument(self, instrument: Instrument) -> None:
|
||||
"""Start instrumenting the current run loop with the given instrument.
|
||||
|
||||
Args:
|
||||
instrument (trio.abc.Instrument): The instrument to activate.
|
||||
|
||||
If ``instrument`` is already active, does nothing.
|
||||
|
||||
"""
|
||||
if instrument in self.data["_all"]:
|
||||
return
|
||||
self.data["_all"][instrument] = None
|
||||
try:
|
||||
for name in dir(instrument):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
prototype = getattr(Instrument, name)
|
||||
except AttributeError:
|
||||
continue
|
||||
impl = getattr(instrument, name)
|
||||
if isinstance(impl, types.MethodType) and impl.__func__ is prototype:
|
||||
# Inherited unchanged from _abc.Instrument
|
||||
continue
|
||||
self.data.setdefault(name, {})[instrument] = None
|
||||
except:
|
||||
self.remove_instrument(instrument)
|
||||
raise
|
||||
|
||||
@_public
|
||||
def remove_instrument(self, instrument: Instrument) -> None:
|
||||
"""Stop instrumenting the current run loop with the given instrument.
|
||||
|
||||
Args:
|
||||
instrument (trio.abc.Instrument): The instrument to de-activate.
|
||||
|
||||
Raises:
|
||||
KeyError: if the instrument is not currently active. This could
|
||||
occur either because you never added it, or because you added it
|
||||
and then it raised an unhandled exception and was automatically
|
||||
deactivated.
|
||||
|
||||
"""
|
||||
# If instrument isn't present, the KeyError propagates out
|
||||
self.data["_all"].pop(instrument)
|
||||
for hookname, instruments in list(self.data.items()):
|
||||
if instrument in instruments:
|
||||
del instruments[instrument]
|
||||
if not instruments:
|
||||
del self.data[hookname]
|
||||
|
||||
def call(
|
||||
self,
|
||||
hookname: str,
|
||||
*args: object,
|
||||
) -> None:
|
||||
"""Call hookname(*args) on each applicable instrument.
|
||||
|
||||
You must first check whether there are any instruments installed for
|
||||
that hook, e.g.::
|
||||
|
||||
if "before_task_step" in instruments:
|
||||
instruments.call("before_task_step", task)
|
||||
"""
|
||||
for instrument in list(self.data[hookname]):
|
||||
try:
|
||||
getattr(instrument, hookname)(*args)
|
||||
except BaseException:
|
||||
self.remove_instrument(instrument)
|
||||
INSTRUMENT_LOGGER.exception(
|
||||
"Exception raised when calling %r on instrument %r. "
|
||||
"Instrument has been disabled.",
|
||||
hookname,
|
||||
instrument,
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import outcome
|
||||
|
||||
from .. import _core
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._io_epoll import EpollWaiters
|
||||
from ._io_windows import AFDWaiters
|
||||
|
||||
|
||||
# Utility function shared between _io_epoll and _io_windows
|
||||
def wake_all(waiters: EpollWaiters | AFDWaiters, exc: BaseException) -> None:
|
||||
try:
|
||||
current_task = _core.current_task()
|
||||
except RuntimeError:
|
||||
current_task = None
|
||||
raise_at_end = False
|
||||
for attr_name in ["read_task", "write_task"]:
|
||||
task = getattr(waiters, attr_name)
|
||||
if task is not None:
|
||||
if task is current_task:
|
||||
raise_at_end = True
|
||||
else:
|
||||
_core.reschedule(task, outcome.Error(copy.copy(exc)))
|
||||
setattr(waiters, attr_name, None)
|
||||
if raise_at_end:
|
||||
raise exc
|
||||
@@ -0,0 +1,385 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import select
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias
|
||||
|
||||
import attrs
|
||||
|
||||
from .. import _core
|
||||
from ._io_common import wake_all
|
||||
from ._run import Task, _public
|
||||
from ._wakeup_socketpair import WakeupSocketpair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Abort, RaiseCancelT
|
||||
from .._file_io import _HasFileNo
|
||||
|
||||
|
||||
@attrs.define(eq=False)
|
||||
class EpollWaiters:
|
||||
read_task: Task | None = None
|
||||
write_task: Task | None = None
|
||||
current_flags: int = 0
|
||||
|
||||
|
||||
assert not TYPE_CHECKING or sys.platform == "linux"
|
||||
|
||||
|
||||
EventResult: TypeAlias = "list[tuple[int, int]]"
|
||||
|
||||
|
||||
@attrs.frozen(eq=False)
|
||||
class _EpollStatistics:
|
||||
tasks_waiting_read: int
|
||||
tasks_waiting_write: int
|
||||
backend: Literal["epoll"] = attrs.field(init=False, default="epoll")
|
||||
|
||||
|
||||
# Some facts about epoll
|
||||
# ----------------------
|
||||
#
|
||||
# Internally, an epoll object is sort of like a WeakKeyDictionary where the
|
||||
# keys are tuples of (fd number, file object). When you call epoll_ctl, you
|
||||
# pass in an fd; that gets converted to an (fd number, file object) tuple by
|
||||
# looking up the fd in the process's fd table at the time of the call. When an
|
||||
# event happens on the file object, epoll_wait drops the file object part, and
|
||||
# just returns the fd number in its event. So from the outside it looks like
|
||||
# it's keeping a table of fds, but really it's a bit more complicated. This
|
||||
# has some subtle consequences.
|
||||
#
|
||||
# In general, file objects inside the kernel are reference counted. Each entry
|
||||
# in a process's fd table holds a strong reference to the corresponding file
|
||||
# object, and most operations that use file objects take a temporary strong
|
||||
# reference while they're working. So when you call close() on an fd, that
|
||||
# might or might not cause the file object to be deallocated -- it depends on
|
||||
# whether there are any other references to that file object. Some common ways
|
||||
# this can happen:
|
||||
#
|
||||
# - after calling dup(), you have two fds in the same process referring to the
|
||||
# same file object. Even if you close one fd (= remove that entry from the
|
||||
# fd table), the file object will be kept alive by the other fd.
|
||||
# - when calling fork(), the child inherits a copy of the parent's fd table,
|
||||
# so all the file objects get another reference. (But if the fork() is
|
||||
# followed by exec(), then all of the child's fds that have the CLOEXEC flag
|
||||
# set will be closed at that point.)
|
||||
# - most syscalls that work on fds take a strong reference to the underlying
|
||||
# file object while they're using it. So there's one thread blocked in
|
||||
# read(fd), and then another thread calls close() on the last fd referring
|
||||
# to that object, the underlying file won't actually be closed until
|
||||
# after read() returns.
|
||||
#
|
||||
# However, epoll does *not* take a reference to any of the file objects in its
|
||||
# interest set (that's what makes it similar to a WeakKeyDictionary). File
|
||||
# objects inside an epoll interest set will be deallocated if all *other*
|
||||
# references to them are closed. And when that happens, the epoll object will
|
||||
# automatically deregister that file object and stop reporting events on it.
|
||||
# So that's quite handy.
|
||||
#
|
||||
# But, what happens if we do this?
|
||||
#
|
||||
# fd1 = open(...)
|
||||
# epoll_ctl(EPOLL_CTL_ADD, fd1, ...)
|
||||
# fd2 = dup(fd1)
|
||||
# close(fd1)
|
||||
#
|
||||
# In this case, the dup() keeps the underlying file object alive, so it
|
||||
# remains registered in the epoll object's interest set, as the tuple (fd1,
|
||||
# file object). But, fd1 no longer refers to this file object! You might think
|
||||
# there was some magic to handle this, but unfortunately no; the consequences
|
||||
# are totally predictable from what I said above:
|
||||
#
|
||||
# If any events occur on the file object, then epoll will report them as
|
||||
# happening on fd1, even though that doesn't make sense.
|
||||
#
|
||||
# Perhaps we would like to deregister fd1 to stop getting nonsensical events.
|
||||
# But how? When we call epoll_ctl, we have to pass an fd number, which will
|
||||
# get expanded to an (fd number, file object) tuple. We can't pass fd1,
|
||||
# because when epoll_ctl tries to look it up, it won't find our file object.
|
||||
# And we can't pass fd2, because that will get expanded to (fd2, file object),
|
||||
# which is a different lookup key. In fact, it's *impossible* to de-register
|
||||
# this fd!
|
||||
#
|
||||
# We could even have fd1 get assigned to another file object, and then we can
|
||||
# have multiple keys registered simultaneously using the same fd number, like:
|
||||
# (fd1, file object 1), (fd1, file object 2). And if events happen on either
|
||||
# file object, then epoll will happily report that something happened to
|
||||
# "fd1".
|
||||
#
|
||||
# Now here's what makes this especially nasty: suppose the old file object
|
||||
# becomes, say, readable. That means that every time we call epoll_wait, it
|
||||
# will return immediately to tell us that "fd1" is readable. Normally, we
|
||||
# would handle this by de-registering fd1, waking up the corresponding call to
|
||||
# wait_readable, then the user will call read() or recv() or something, and
|
||||
# we're fine. But if this happens on a stale fd where we can't remove the
|
||||
# registration, then we might get stuck in a state where epoll_wait *always*
|
||||
# returns immediately, so our event loop becomes unable to sleep, and now our
|
||||
# program is burning 100% of the CPU doing nothing, with no way out.
|
||||
#
|
||||
#
|
||||
# What does this mean for Trio?
|
||||
# -----------------------------
|
||||
#
|
||||
# Since we don't control the user's code, we have no way to guarantee that we
|
||||
# don't get stuck with stale fd's in our epoll interest set. For example, a
|
||||
# user could call wait_readable(fd) in one task, and then while that's
|
||||
# running, they might close(fd) from another task. In this situation, they're
|
||||
# *supposed* to call notify_closing(fd) to let us know what's happening, so we
|
||||
# can interrupt the wait_readable() call and avoid getting into this mess. And
|
||||
# that's the only thing that can possibly work correctly in all cases. But
|
||||
# sometimes user code has bugs. So if this does happen, we'd like to degrade
|
||||
# gracefully, and survive without corrupting Trio's internal state or
|
||||
# otherwise causing the whole program to explode messily.
|
||||
#
|
||||
# Our solution: we always use EPOLLONESHOT. This way, we might get *one*
|
||||
# spurious event on a stale fd, but then epoll will automatically silence it
|
||||
# until we explicitly say that we want more events... and if we have a stale
|
||||
# fd, then we actually can't re-enable it! So we can't get stuck in an
|
||||
# infinite busy-loop. If there's a stale fd hanging around, then it might
|
||||
# cause a spurious `BusyResourceError`, or cause one wait_* call to return
|
||||
# before it should have... but in general, the wait_* functions are allowed to
|
||||
# have some spurious wakeups; the user code will just attempt the operation,
|
||||
# get EWOULDBLOCK, and call wait_* again. And the program as a whole will
|
||||
# survive, any exceptions will propagate, etc.
|
||||
#
|
||||
# As a bonus, EPOLLONESHOT also saves us having to explicitly deregister fds
|
||||
# on the normal wakeup path, so it's a bit more efficient in general.
|
||||
#
|
||||
# However, EPOLLONESHOT has a few trade-offs to consider:
|
||||
#
|
||||
# First, you can't combine EPOLLONESHOT with EPOLLEXCLUSIVE. This is a bit sad
|
||||
# in one somewhat rare case: if you have a multi-process server where a group
|
||||
# of processes all share the same listening socket, then EPOLLEXCLUSIVE can be
|
||||
# used to avoid "thundering herd" problems when a new connection comes in. But
|
||||
# this isn't too bad. It's not clear if EPOLLEXCLUSIVE even works for us
|
||||
# anyway:
|
||||
#
|
||||
# https://stackoverflow.com/questions/41582560/how-does-epolls-epollexclusive-mode-interact-with-level-triggering
|
||||
#
|
||||
# And it's not clear that EPOLLEXCLUSIVE is a great approach either:
|
||||
#
|
||||
# https://blog.cloudflare.com/the-sad-state-of-linux-socket-balancing/
|
||||
#
|
||||
# And if we do need to support this, we could always add support through some
|
||||
# more-specialized API in the future. So this isn't a blocker to using
|
||||
# EPOLLONESHOT.
|
||||
#
|
||||
# Second, EPOLLONESHOT does not actually *deregister* the fd after delivering
|
||||
# an event (EPOLL_CTL_DEL). Instead, it keeps the fd registered, but
|
||||
# effectively does an EPOLL_CTL_MOD to set the fd's interest flags to
|
||||
# all-zeros. So we could still end up with an fd hanging around in the
|
||||
# interest set for a long time, even if we're not using it.
|
||||
#
|
||||
# Fortunately, this isn't a problem, because it's only a weak reference – if
|
||||
# we have a stale fd that's been silenced by EPOLLONESHOT, then it wastes a
|
||||
# tiny bit of kernel memory remembering this fd that can never be revived, but
|
||||
# when the underlying file object is eventually closed, that memory will be
|
||||
# reclaimed. So that's OK.
|
||||
#
|
||||
# The other issue is that when someone calls wait_*, using EPOLLONESHOT means
|
||||
# that if we have ever waited for this fd before, we have to use EPOLL_CTL_MOD
|
||||
# to re-enable it; but if it's a new fd, we have to use EPOLL_CTL_ADD. How do
|
||||
# we know which one to use? There's no reasonable way to track which fds are
|
||||
# currently registered -- remember, we're assuming the user might have gone
|
||||
# and rearranged their fds without telling us!
|
||||
#
|
||||
# Fortunately, this also has a simple solution: if we wait on a socket or
|
||||
# other fd once, then we'll probably wait on it lots of times. And the epoll
|
||||
# object itself knows which fds it already has registered. So when an fd comes
|
||||
# in, we optimistically assume that it's been waited on before, and try doing
|
||||
# EPOLL_CTL_MOD. And if that fails with an ENOENT error, then we try again
|
||||
# with EPOLL_CTL_ADD.
|
||||
#
|
||||
# So that's why this code is the way it is. And now you know more than you
|
||||
# wanted to about how epoll works.
|
||||
|
||||
|
||||
@attrs.define(eq=False)
|
||||
class EpollIOManager:
|
||||
# Using lambda here because otherwise crash on import with gevent monkey patching
|
||||
# See https://github.com/python-trio/trio/issues/2848
|
||||
_epoll: select.epoll = attrs.Factory(lambda: select.epoll())
|
||||
# {fd: EpollWaiters}
|
||||
_registered: defaultdict[int, EpollWaiters] = attrs.Factory(
|
||||
lambda: defaultdict(EpollWaiters),
|
||||
)
|
||||
_force_wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair)
|
||||
_force_wakeup_fd: int | None = None
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self._epoll.register(self._force_wakeup.wakeup_sock, select.EPOLLIN)
|
||||
self._force_wakeup_fd = self._force_wakeup.wakeup_sock.fileno()
|
||||
|
||||
def statistics(self) -> _EpollStatistics:
|
||||
tasks_waiting_read = 0
|
||||
tasks_waiting_write = 0
|
||||
for waiter in self._registered.values():
|
||||
if waiter.read_task is not None:
|
||||
tasks_waiting_read += 1
|
||||
if waiter.write_task is not None:
|
||||
tasks_waiting_write += 1
|
||||
return _EpollStatistics(
|
||||
tasks_waiting_read=tasks_waiting_read,
|
||||
tasks_waiting_write=tasks_waiting_write,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self._epoll.close()
|
||||
self._force_wakeup.close()
|
||||
|
||||
def force_wakeup(self) -> None:
|
||||
self._force_wakeup.wakeup_thread_and_signal_safe()
|
||||
|
||||
# Return value must be False-y IFF the timeout expired, NOT if any I/O
|
||||
# happened or force_wakeup was called. Otherwise it can be anything; gets
|
||||
# passed straight through to process_events.
|
||||
def get_events(self, timeout: float) -> EventResult:
|
||||
# max_events must be > 0 or epoll gets cranky
|
||||
# accessing self._registered from a thread looks dangerous, but it's
|
||||
# OK because it doesn't matter if our value is a little bit off.
|
||||
max_events = max(1, len(self._registered))
|
||||
return self._epoll.poll(timeout, max_events)
|
||||
|
||||
def process_events(self, events: EventResult) -> None:
|
||||
for fd, flags in events:
|
||||
if fd == self._force_wakeup_fd:
|
||||
self._force_wakeup.drain()
|
||||
continue
|
||||
waiters = self._registered[fd]
|
||||
# EPOLLONESHOT always clears the flags when an event is delivered
|
||||
waiters.current_flags = 0
|
||||
# Clever hack stolen from selectors.EpollSelector: an event
|
||||
# with EPOLLHUP or EPOLLERR flags wakes both readers and
|
||||
# writers.
|
||||
if flags & ~select.EPOLLIN and waiters.write_task is not None:
|
||||
_core.reschedule(waiters.write_task)
|
||||
waiters.write_task = None
|
||||
if flags & ~select.EPOLLOUT and waiters.read_task is not None:
|
||||
_core.reschedule(waiters.read_task)
|
||||
waiters.read_task = None
|
||||
self._update_registrations(fd)
|
||||
|
||||
def _update_registrations(self, fd: int) -> None:
|
||||
waiters = self._registered[fd]
|
||||
wanted_flags = 0
|
||||
if waiters.read_task is not None:
|
||||
wanted_flags |= select.EPOLLIN
|
||||
if waiters.write_task is not None:
|
||||
wanted_flags |= select.EPOLLOUT
|
||||
if wanted_flags != waiters.current_flags:
|
||||
try:
|
||||
try:
|
||||
# First try EPOLL_CTL_MOD
|
||||
self._epoll.modify(fd, wanted_flags | select.EPOLLONESHOT)
|
||||
except OSError:
|
||||
# If that fails, it might be a new fd; try EPOLL_CTL_ADD
|
||||
self._epoll.register(fd, wanted_flags | select.EPOLLONESHOT)
|
||||
waiters.current_flags = wanted_flags
|
||||
except OSError as exc:
|
||||
# If everything fails, probably it's a bad fd, e.g. because
|
||||
# the fd was closed behind our back. In this case we don't
|
||||
# want to try to unregister the fd, because that will probably
|
||||
# fail too. Just clear our state and wake everyone up.
|
||||
del self._registered[fd]
|
||||
# This could raise (in case we're calling this inside one of
|
||||
# the to-be-woken tasks), so we have to do it last.
|
||||
wake_all(waiters, exc)
|
||||
return
|
||||
if not wanted_flags:
|
||||
del self._registered[fd]
|
||||
|
||||
async def _epoll_wait(self, fd: int | _HasFileNo, attr_name: str) -> None:
|
||||
if not isinstance(fd, int):
|
||||
fd = fd.fileno()
|
||||
waiters = self._registered[fd]
|
||||
if getattr(waiters, attr_name) is not None:
|
||||
raise _core.BusyResourceError(
|
||||
"another task is already reading / writing this fd",
|
||||
)
|
||||
setattr(waiters, attr_name, _core.current_task())
|
||||
self._update_registrations(fd)
|
||||
|
||||
def abort(_: RaiseCancelT) -> Abort:
|
||||
setattr(waiters, attr_name, None)
|
||||
self._update_registrations(fd)
|
||||
return _core.Abort.SUCCEEDED
|
||||
|
||||
await _core.wait_task_rescheduled(abort)
|
||||
|
||||
@_public
|
||||
async def wait_readable(self, fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is readable.
|
||||
|
||||
On Unix systems, ``fd`` must either be an integer file descriptor,
|
||||
or else an object with a ``.fileno()`` method which returns an
|
||||
integer file descriptor. Any kind of file descriptor can be passed,
|
||||
though the exact semantics will depend on your kernel. For example,
|
||||
this probably won't do anything useful for on-disk files.
|
||||
|
||||
On Windows systems, ``fd`` must either be an integer ``SOCKET``
|
||||
handle, or else an object with a ``.fileno()`` method which returns
|
||||
an integer ``SOCKET`` handle. File descriptors aren't supported,
|
||||
and neither are handles that refer to anything besides a
|
||||
``SOCKET``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become readable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
await self._epoll_wait(fd, "read_task")
|
||||
|
||||
@_public
|
||||
async def wait_writable(self, fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is writable.
|
||||
|
||||
See `wait_readable` for the definition of ``fd``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become writable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
await self._epoll_wait(fd, "write_task")
|
||||
|
||||
@_public
|
||||
def notify_closing(self, fd: int | _HasFileNo) -> None:
|
||||
"""Notify waiters of the given object that it will be closed.
|
||||
|
||||
Call this before closing a file descriptor (on Unix) or socket (on
|
||||
Windows). This will cause any `wait_readable` or `wait_writable`
|
||||
calls on the given object to immediately wake up and raise
|
||||
`~trio.ClosedResourceError`.
|
||||
|
||||
This doesn't actually close the object – you still have to do that
|
||||
yourself afterwards. Also, you want to be careful to make sure no
|
||||
new tasks start waiting on the object in between when you call this
|
||||
and when it's actually closed. So to close something properly, you
|
||||
usually want to do these steps in order:
|
||||
|
||||
1. Explicitly mark the object as closed, so that any new attempts
|
||||
to use it will abort before they start.
|
||||
2. Call `notify_closing` to wake up any already-existing users.
|
||||
3. Actually close the object.
|
||||
|
||||
It's also possible to do them in a different order if that's more
|
||||
convenient, *but only if* you make sure not to have any checkpoints in
|
||||
between the steps. This way they all happen in a single atomic
|
||||
step, so other tasks won't be able to tell what order they happened
|
||||
in anyway.
|
||||
"""
|
||||
if not isinstance(fd, int):
|
||||
fd = fd.fileno()
|
||||
wake_all(
|
||||
self._registered[fd],
|
||||
_core.ClosedResourceError("another task closed this fd"),
|
||||
)
|
||||
del self._registered[fd]
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
self._epoll.unregister(fd)
|
||||
@@ -0,0 +1,292 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import select
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias
|
||||
|
||||
import attrs
|
||||
import outcome
|
||||
|
||||
from .. import _core
|
||||
from ._run import _public
|
||||
from ._wakeup_socketpair import WakeupSocketpair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterator
|
||||
|
||||
from .._core import Abort, RaiseCancelT, Task, UnboundedQueue
|
||||
from .._file_io import _HasFileNo
|
||||
|
||||
assert not TYPE_CHECKING or (sys.platform != "linux" and sys.platform != "win32")
|
||||
|
||||
EventResult: TypeAlias = "list[select.kevent]"
|
||||
|
||||
|
||||
@attrs.frozen(eq=False)
|
||||
class _KqueueStatistics:
|
||||
tasks_waiting: int
|
||||
monitors: int
|
||||
backend: Literal["kqueue"] = attrs.field(init=False, default="kqueue")
|
||||
|
||||
|
||||
@attrs.define(eq=False)
|
||||
class KqueueIOManager:
|
||||
_kqueue: select.kqueue = attrs.Factory(select.kqueue)
|
||||
# {(ident, filter): Task or UnboundedQueue}
|
||||
_registered: dict[tuple[int, int], Task | UnboundedQueue[select.kevent]] = (
|
||||
attrs.Factory(dict)
|
||||
)
|
||||
_force_wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair)
|
||||
_force_wakeup_fd: int | None = None
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
force_wakeup_event = select.kevent(
|
||||
self._force_wakeup.wakeup_sock,
|
||||
select.KQ_FILTER_READ,
|
||||
select.KQ_EV_ADD,
|
||||
)
|
||||
self._kqueue.control([force_wakeup_event], 0)
|
||||
self._force_wakeup_fd = self._force_wakeup.wakeup_sock.fileno()
|
||||
|
||||
def statistics(self) -> _KqueueStatistics:
|
||||
tasks_waiting = 0
|
||||
monitors = 0
|
||||
for receiver in self._registered.values():
|
||||
if type(receiver) is _core.Task:
|
||||
tasks_waiting += 1
|
||||
else:
|
||||
monitors += 1
|
||||
return _KqueueStatistics(tasks_waiting=tasks_waiting, monitors=monitors)
|
||||
|
||||
def close(self) -> None:
|
||||
self._kqueue.close()
|
||||
self._force_wakeup.close()
|
||||
|
||||
def force_wakeup(self) -> None:
|
||||
self._force_wakeup.wakeup_thread_and_signal_safe()
|
||||
|
||||
def get_events(self, timeout: float) -> EventResult:
|
||||
# max_events must be > 0 or kqueue gets cranky
|
||||
# and we generally want this to be strictly larger than the actual
|
||||
# number of events we get, so that we can tell that we've gotten
|
||||
# all the events in just 1 call.
|
||||
max_events = len(self._registered) + 1
|
||||
events = []
|
||||
while True:
|
||||
batch = self._kqueue.control([], max_events, timeout)
|
||||
events += batch
|
||||
if len(batch) < max_events:
|
||||
break
|
||||
else: # TODO: test this line
|
||||
timeout = 0
|
||||
# and loop back to the start
|
||||
return events
|
||||
|
||||
def process_events(self, events: EventResult) -> None:
|
||||
for event in events:
|
||||
key = (event.ident, event.filter)
|
||||
if event.ident == self._force_wakeup_fd:
|
||||
self._force_wakeup.drain()
|
||||
continue
|
||||
receiver = self._registered[key]
|
||||
if event.flags & select.KQ_EV_ONESHOT: # TODO: test this branch
|
||||
del self._registered[key]
|
||||
if isinstance(receiver, _core.Task):
|
||||
_core.reschedule(receiver, outcome.Value(event))
|
||||
else:
|
||||
receiver.put_nowait(event) # TODO: test this line
|
||||
|
||||
# kevent registration is complicated -- e.g. aio submission can
|
||||
# implicitly perform a EV_ADD, and EVFILT_PROC with NOTE_TRACK will
|
||||
# automatically register filters for child processes. So our lowlevel
|
||||
# API is *very* low-level: we expose the kqueue itself for adding
|
||||
# events or sticking into AIO submission structs, and split waiting
|
||||
# off into separate methods. It's your responsibility to make sure
|
||||
# that handle_io never receives an event without a corresponding
|
||||
# registration! This may be challenging if you want to be careful
|
||||
# about e.g. KeyboardInterrupt. Possibly this API could be improved to
|
||||
# be more ergonomic...
|
||||
|
||||
@_public
|
||||
def current_kqueue(self) -> select.kqueue:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
return self._kqueue
|
||||
|
||||
@contextmanager
|
||||
@_public
|
||||
def monitor_kevent(
|
||||
self,
|
||||
ident: int,
|
||||
filter: int,
|
||||
) -> Iterator[_core.UnboundedQueue[select.kevent]]:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
key = (ident, filter)
|
||||
if key in self._registered:
|
||||
raise _core.BusyResourceError(
|
||||
"attempt to register multiple listeners for same ident/filter pair",
|
||||
)
|
||||
q = _core.UnboundedQueue[select.kevent]()
|
||||
self._registered[key] = q
|
||||
try:
|
||||
yield q
|
||||
finally:
|
||||
del self._registered[key]
|
||||
|
||||
@_public
|
||||
async def wait_kevent(
|
||||
self,
|
||||
ident: int,
|
||||
filter: int,
|
||||
abort_func: Callable[[RaiseCancelT], Abort],
|
||||
) -> Abort:
|
||||
"""TODO: these are implemented, but are currently more of a sketch than
|
||||
anything real. See `#26
|
||||
<https://github.com/python-trio/trio/issues/26>`__.
|
||||
"""
|
||||
key = (ident, filter)
|
||||
if key in self._registered:
|
||||
raise _core.BusyResourceError(
|
||||
"attempt to register multiple listeners for same ident/filter pair",
|
||||
)
|
||||
self._registered[key] = _core.current_task()
|
||||
|
||||
def abort(raise_cancel: RaiseCancelT) -> Abort:
|
||||
r = abort_func(raise_cancel)
|
||||
if r is _core.Abort.SUCCEEDED: # TODO: test this branch
|
||||
del self._registered[key]
|
||||
return r
|
||||
|
||||
# wait_task_rescheduled does not have its return type typed
|
||||
return await _core.wait_task_rescheduled(abort) # type: ignore[no-any-return]
|
||||
|
||||
async def _wait_common(
|
||||
self,
|
||||
fd: int | _HasFileNo,
|
||||
filter: int,
|
||||
) -> None:
|
||||
if not isinstance(fd, int):
|
||||
fd = fd.fileno()
|
||||
flags = select.KQ_EV_ADD | select.KQ_EV_ONESHOT
|
||||
event = select.kevent(fd, filter, flags)
|
||||
self._kqueue.control([event], 0)
|
||||
|
||||
def abort(_: RaiseCancelT) -> Abort:
|
||||
event = select.kevent(fd, filter, select.KQ_EV_DELETE)
|
||||
try:
|
||||
self._kqueue.control([event], 0)
|
||||
except OSError as exc:
|
||||
# kqueue tracks individual fds (*not* the underlying file
|
||||
# object, see _io_epoll.py for a long discussion of why this
|
||||
# distinction matters), and automatically deregisters an event
|
||||
# if the fd is closed. So if kqueue.control says that it
|
||||
# doesn't know about this event, then probably it's because
|
||||
# the fd was closed behind our backs. (Too bad we can't ask it
|
||||
# to wake us up when this happens, versus discovering it after
|
||||
# the fact... oh well, you can't have everything.)
|
||||
#
|
||||
# FreeBSD reports this using EBADF. macOS uses ENOENT.
|
||||
if exc.errno in (errno.EBADF, errno.ENOENT): # pragma: no branch
|
||||
pass
|
||||
else: # pragma: no cover
|
||||
# As far as we know, this branch can't happen.
|
||||
raise
|
||||
return _core.Abort.SUCCEEDED
|
||||
|
||||
await self.wait_kevent(fd, filter, abort)
|
||||
|
||||
@_public
|
||||
async def wait_readable(self, fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is readable.
|
||||
|
||||
On Unix systems, ``fd`` must either be an integer file descriptor,
|
||||
or else an object with a ``.fileno()`` method which returns an
|
||||
integer file descriptor. Any kind of file descriptor can be passed,
|
||||
though the exact semantics will depend on your kernel. For example,
|
||||
this probably won't do anything useful for on-disk files.
|
||||
|
||||
On Windows systems, ``fd`` must either be an integer ``SOCKET``
|
||||
handle, or else an object with a ``.fileno()`` method which returns
|
||||
an integer ``SOCKET`` handle. File descriptors aren't supported,
|
||||
and neither are handles that refer to anything besides a
|
||||
``SOCKET``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become readable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
await self._wait_common(fd, select.KQ_FILTER_READ)
|
||||
|
||||
@_public
|
||||
async def wait_writable(self, fd: int | _HasFileNo) -> None:
|
||||
"""Block until the kernel reports that the given object is writable.
|
||||
|
||||
See `wait_readable` for the definition of ``fd``.
|
||||
|
||||
:raises trio.BusyResourceError:
|
||||
if another task is already waiting for the given socket to
|
||||
become writable.
|
||||
:raises trio.ClosedResourceError:
|
||||
if another task calls :func:`notify_closing` while this
|
||||
function is still working.
|
||||
"""
|
||||
await self._wait_common(fd, select.KQ_FILTER_WRITE)
|
||||
|
||||
@_public
|
||||
def notify_closing(self, fd: int | _HasFileNo) -> None:
|
||||
"""Notify waiters of the given object that it will be closed.
|
||||
|
||||
Call this before closing a file descriptor (on Unix) or socket (on
|
||||
Windows). This will cause any `wait_readable` or `wait_writable`
|
||||
calls on the given object to immediately wake up and raise
|
||||
`~trio.ClosedResourceError`.
|
||||
|
||||
This doesn't actually close the object – you still have to do that
|
||||
yourself afterwards. Also, you want to be careful to make sure no
|
||||
new tasks start waiting on the object in between when you call this
|
||||
and when it's actually closed. So to close something properly, you
|
||||
usually want to do these steps in order:
|
||||
|
||||
1. Explicitly mark the object as closed, so that any new attempts
|
||||
to use it will abort before they start.
|
||||
2. Call `notify_closing` to wake up any already-existing users.
|
||||
3. Actually close the object.
|
||||
|
||||
It's also possible to do them in a different order if that's more
|
||||
convenient, *but only if* you make sure not to have any checkpoints in
|
||||
between the steps. This way they all happen in a single atomic
|
||||
step, so other tasks won't be able to tell what order they happened
|
||||
in anyway.
|
||||
"""
|
||||
if not isinstance(fd, int):
|
||||
fd = fd.fileno()
|
||||
|
||||
for filter_ in [select.KQ_FILTER_READ, select.KQ_FILTER_WRITE]:
|
||||
key = (fd, filter_)
|
||||
receiver = self._registered.get(key)
|
||||
|
||||
if receiver is None:
|
||||
continue
|
||||
|
||||
if type(receiver) is _core.Task:
|
||||
event = select.kevent(fd, filter_, select.KQ_EV_DELETE)
|
||||
self._kqueue.control([event], 0)
|
||||
exc = _core.ClosedResourceError("another task closed this fd")
|
||||
_core.reschedule(receiver, outcome.Error(exc))
|
||||
del self._registered[key]
|
||||
else:
|
||||
# XX this is an interesting example of a case where being able
|
||||
# to close a queue would be useful...
|
||||
raise NotImplementedError(
|
||||
"can't close an fd that monitor_kevent is using",
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Generic, Protocol, TypeGuard, TypeVar
|
||||
|
||||
import attrs
|
||||
|
||||
from .._util import is_main_thread
|
||||
from ._run_context import GLOBAL_RUN_CONTEXT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
|
||||
from typing_extensions import Self
|
||||
# In ordinary single-threaded Python code, when you hit control-C, it raises
|
||||
# an exception and automatically does all the regular unwinding stuff.
|
||||
#
|
||||
# In Trio code, we would like hitting control-C to raise an exception and
|
||||
# automatically do all the regular unwinding stuff. In particular, we would
|
||||
# like to maintain our invariant that all tasks always run to completion (one
|
||||
# way or another), by unwinding all of them.
|
||||
#
|
||||
# But it's basically impossible to write the core task running code in such a
|
||||
# way that it can maintain this invariant in the face of KeyboardInterrupt
|
||||
# exceptions arising at arbitrary bytecode positions. Similarly, if a
|
||||
# KeyboardInterrupt happened at the wrong moment inside pretty much any of our
|
||||
# inter-task synchronization or I/O primitives, then the system state could
|
||||
# get corrupted and prevent our being able to clean up properly.
|
||||
#
|
||||
# So, we need a way to defer KeyboardInterrupt processing from these critical
|
||||
# sections.
|
||||
#
|
||||
# Things that don't work:
|
||||
#
|
||||
# - Listen for SIGINT and process it in a system task: works fine for
|
||||
# well-behaved programs that regularly pass through the event loop, but if
|
||||
# user-code goes into an infinite loop then it can't be interrupted. Which
|
||||
# is unfortunate, since dealing with infinite loops is what
|
||||
# KeyboardInterrupt is for!
|
||||
#
|
||||
# - Use pthread_sigmask to disable signal delivery during critical section:
|
||||
# (a) windows has no pthread_sigmask, (b) python threads start with all
|
||||
# signals unblocked, so if there are any threads around they'll receive the
|
||||
# signal and then tell the main thread to run the handler, even if the main
|
||||
# thread has that signal blocked.
|
||||
#
|
||||
# - Install a signal handler which checks a global variable to decide whether
|
||||
# to raise the exception immediately (if we're in a non-critical section),
|
||||
# or to schedule it on the event loop (if we're in a critical section). The
|
||||
# problem here is that it's impossible to transition safely out of user code:
|
||||
#
|
||||
# with keyboard_interrupt_enabled:
|
||||
# msg = coro.send(value)
|
||||
#
|
||||
# If this raises a KeyboardInterrupt, it might be because the coroutine got
|
||||
# interrupted and has unwound... or it might be the KeyboardInterrupt
|
||||
# arrived just *after* 'send' returned, so the coroutine is still running,
|
||||
# but we just lost the message it sent. (And worse, in our actual task
|
||||
# runner, the send is hidden inside a utility function etc.)
|
||||
#
|
||||
# Solution:
|
||||
#
|
||||
# Mark *stack frames* as being interrupt-safe or interrupt-unsafe, and from
|
||||
# the signal handler check which kind of frame we're currently in when
|
||||
# deciding whether to raise or schedule the exception.
|
||||
#
|
||||
# There are still some cases where this can fail, like if someone hits
|
||||
# control-C while the process is in the event loop, and then it immediately
|
||||
# enters an infinite loop in user code. In this case the user has to hit
|
||||
# control-C a second time. And of course if the user code is written so that
|
||||
# it doesn't actually exit after a task crashes and everything gets cancelled,
|
||||
# then there's not much to be done. (Hitting control-C repeatedly might help,
|
||||
# but in general the solution is to kill the process some other way, just like
|
||||
# for any Python program that's written to catch and ignore
|
||||
# KeyboardInterrupt.)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class _IdRef(weakref.ref[_T]):
|
||||
__slots__ = ("_hash",)
|
||||
_hash: int
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
ob: _T,
|
||||
callback: Callable[[Self], object] | None = None,
|
||||
/,
|
||||
) -> Self:
|
||||
self: Self = weakref.ref.__new__(cls, ob, callback)
|
||||
self._hash = object.__hash__(ob)
|
||||
return self
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self is other:
|
||||
return True
|
||||
|
||||
if not isinstance(other, _IdRef):
|
||||
return NotImplemented
|
||||
|
||||
my_obj = None
|
||||
try:
|
||||
my_obj = self()
|
||||
return my_obj is not None and my_obj is other()
|
||||
finally:
|
||||
del my_obj
|
||||
|
||||
# we're overriding a builtin so we do need this
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self == other
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self._hash
|
||||
|
||||
|
||||
_KT = TypeVar("_KT")
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
# see also: https://github.com/python/cpython/issues/88306
|
||||
class WeakKeyIdentityDictionary(Generic[_KT, _VT]):
|
||||
def __init__(self) -> None:
|
||||
self._data: dict[_IdRef[_KT], _VT] = {}
|
||||
|
||||
def remove(
|
||||
k: _IdRef[_KT],
|
||||
selfref: weakref.ref[
|
||||
WeakKeyIdentityDictionary[_KT, _VT]
|
||||
] = weakref.ref( # noqa: B008 # function-call-in-default-argument
|
||||
self,
|
||||
),
|
||||
) -> None:
|
||||
self = selfref()
|
||||
if self is not None:
|
||||
try: # noqa: SIM105 # suppressible-exception
|
||||
del self._data[k]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self._remove = remove
|
||||
|
||||
def __getitem__(self, k: _KT) -> _VT:
|
||||
return self._data[_IdRef(k)]
|
||||
|
||||
def __setitem__(self, k: _KT, v: _VT) -> None:
|
||||
self._data[_IdRef(k, self._remove)] = v
|
||||
|
||||
|
||||
_CODE_KI_PROTECTION_STATUS_WMAP: WeakKeyIdentityDictionary[
|
||||
types.CodeType,
|
||||
bool,
|
||||
] = WeakKeyIdentityDictionary()
|
||||
|
||||
|
||||
# This is to support the async_generator package necessary for aclosing on <3.10
|
||||
# functions decorated @async_generator are given this magic property that's a
|
||||
# reference to the object itself
|
||||
# see python-trio/async_generator/async_generator/_impl.py
|
||||
def legacy_isasyncgenfunction(
|
||||
obj: object,
|
||||
) -> TypeGuard[Callable[..., types.AsyncGeneratorType[object, object]]]:
|
||||
return getattr(obj, "_async_gen_function", None) == id(obj)
|
||||
|
||||
|
||||
# NB: according to the signal.signal docs, 'frame' can be None on entry to
|
||||
# this function:
|
||||
def ki_protection_enabled(frame: types.FrameType | None) -> bool:
|
||||
try:
|
||||
task = GLOBAL_RUN_CONTEXT.task
|
||||
except AttributeError:
|
||||
task_ki_protected = False
|
||||
task_frame = None
|
||||
else:
|
||||
task_ki_protected = task._ki_protected
|
||||
task_frame = task.coro.cr_frame
|
||||
|
||||
while frame is not None:
|
||||
try:
|
||||
v = _CODE_KI_PROTECTION_STATUS_WMAP[frame.f_code]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
return bool(v)
|
||||
if frame.f_code.co_name == "__del__":
|
||||
return True
|
||||
if frame is task_frame:
|
||||
return task_ki_protected
|
||||
frame = frame.f_back
|
||||
return True
|
||||
|
||||
|
||||
def currently_ki_protected() -> bool:
|
||||
r"""Check whether the calling code has :exc:`KeyboardInterrupt` protection
|
||||
enabled.
|
||||
|
||||
It's surprisingly easy to think that one's :exc:`KeyboardInterrupt`
|
||||
protection is enabled when it isn't, or vice-versa. This function tells
|
||||
you what Trio thinks of the matter, which makes it useful for ``assert``\s
|
||||
and unit tests.
|
||||
|
||||
Returns:
|
||||
bool: True if protection is enabled, and False otherwise.
|
||||
|
||||
"""
|
||||
return ki_protection_enabled(sys._getframe())
|
||||
|
||||
|
||||
class _SupportsCode(Protocol):
|
||||
__code__: types.CodeType
|
||||
|
||||
|
||||
_T_supports_code = TypeVar("_T_supports_code", bound=_SupportsCode)
|
||||
|
||||
|
||||
def enable_ki_protection(f: _T_supports_code, /) -> _T_supports_code:
|
||||
"""Decorator to enable KI protection."""
|
||||
orig = f
|
||||
|
||||
if legacy_isasyncgenfunction(f):
|
||||
f = f.__wrapped__ # type: ignore
|
||||
|
||||
_CODE_KI_PROTECTION_STATUS_WMAP[f.__code__] = True
|
||||
return orig
|
||||
|
||||
|
||||
def disable_ki_protection(f: _T_supports_code, /) -> _T_supports_code:
|
||||
"""Decorator to disable KI protection."""
|
||||
orig = f
|
||||
|
||||
if legacy_isasyncgenfunction(f):
|
||||
f = f.__wrapped__ # type: ignore
|
||||
|
||||
_CODE_KI_PROTECTION_STATUS_WMAP[f.__code__] = False
|
||||
return orig
|
||||
|
||||
|
||||
@attrs.define(slots=False)
|
||||
class KIManager:
|
||||
handler: Callable[[int, types.FrameType | None], None] | None = None
|
||||
|
||||
def install(
|
||||
self,
|
||||
deliver_cb: Callable[[], object],
|
||||
restrict_keyboard_interrupt_to_checkpoints: bool,
|
||||
) -> None:
|
||||
assert self.handler is None
|
||||
if (
|
||||
not is_main_thread()
|
||||
or signal.getsignal(signal.SIGINT) != signal.default_int_handler
|
||||
):
|
||||
return
|
||||
|
||||
def handler(signum: int, frame: types.FrameType | None) -> None:
|
||||
assert signum == signal.SIGINT
|
||||
protection_enabled = ki_protection_enabled(frame)
|
||||
if protection_enabled or restrict_keyboard_interrupt_to_checkpoints:
|
||||
deliver_cb()
|
||||
else:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
self.handler = handler
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
def close(self) -> None:
|
||||
if self.handler is not None:
|
||||
if signal.getsignal(signal.SIGINT) is self.handler:
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
self.handler = None
|
||||
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Generic, TypeVar, cast
|
||||
|
||||
# Runvar implementations
|
||||
import attrs
|
||||
|
||||
from .._util import NoPublicConstructor, final
|
||||
from . import _run
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@final
|
||||
class _NoValue: ...
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False)
|
||||
class RunVarToken(Generic[T], metaclass=NoPublicConstructor):
|
||||
_var: RunVar[T]
|
||||
previous_value: T | type[_NoValue] = _NoValue
|
||||
redeemed: bool = attrs.field(default=False, init=False)
|
||||
|
||||
@classmethod
|
||||
def _empty(cls, var: RunVar[T]) -> RunVarToken[T]:
|
||||
return cls._create(var)
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False, repr=False)
|
||||
class RunVar(Generic[T]):
|
||||
"""The run-local variant of a context variable.
|
||||
|
||||
:class:`RunVar` objects are similar to context variable objects,
|
||||
except that they are shared across a single call to :func:`trio.run`
|
||||
rather than a single task.
|
||||
|
||||
"""
|
||||
|
||||
_name: str = attrs.field(alias="name")
|
||||
_default: T | type[_NoValue] = attrs.field(default=_NoValue, alias="default")
|
||||
|
||||
def get(self, default: T | type[_NoValue] = _NoValue) -> T:
|
||||
"""Gets the value of this :class:`RunVar` for the current run call."""
|
||||
try:
|
||||
return cast("T", _run.GLOBAL_RUN_CONTEXT.runner._locals[self])
|
||||
except AttributeError:
|
||||
raise RuntimeError("Cannot be used outside of a run context") from None
|
||||
except KeyError:
|
||||
# contextvars consistency
|
||||
# `type: ignore` awaiting https://github.com/python/mypy/issues/15553 to be fixed & released
|
||||
if default is not _NoValue:
|
||||
return default # type: ignore[return-value]
|
||||
|
||||
if self._default is not _NoValue:
|
||||
return self._default # type: ignore[return-value]
|
||||
|
||||
raise LookupError(self) from None
|
||||
|
||||
def set(self, value: T) -> RunVarToken[T]:
|
||||
"""Sets the value of this :class:`RunVar` for this current run
|
||||
call.
|
||||
|
||||
"""
|
||||
try:
|
||||
old_value = self.get()
|
||||
except LookupError:
|
||||
token = RunVarToken._empty(self)
|
||||
else:
|
||||
token = RunVarToken[T]._create(self, old_value)
|
||||
|
||||
# This can't fail, because if we weren't in Trio context then the
|
||||
# get() above would have failed.
|
||||
_run.GLOBAL_RUN_CONTEXT.runner._locals[self] = value
|
||||
return token
|
||||
|
||||
def reset(self, token: RunVarToken[T]) -> None:
|
||||
"""Resets the value of this :class:`RunVar` to what it was
|
||||
previously specified by the token.
|
||||
|
||||
"""
|
||||
if token is None:
|
||||
raise TypeError("token must not be none")
|
||||
|
||||
if token.redeemed:
|
||||
raise ValueError("token has already been used")
|
||||
|
||||
if token._var is not self:
|
||||
raise ValueError("token is not for us")
|
||||
|
||||
previous = token.previous_value
|
||||
try:
|
||||
if previous is _NoValue:
|
||||
_run.GLOBAL_RUN_CONTEXT.runner._locals.pop(self)
|
||||
else:
|
||||
_run.GLOBAL_RUN_CONTEXT.runner._locals[self] = previous
|
||||
except AttributeError:
|
||||
raise RuntimeError("Cannot be used outside of a run context") from None
|
||||
|
||||
token.redeemed = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RunVar name={self._name!r}>"
|
||||
@@ -0,0 +1,165 @@
|
||||
import time
|
||||
from math import inf
|
||||
|
||||
from .. import _core
|
||||
from .._abc import Clock
|
||||
from .._util import final
|
||||
from ._run import GLOBAL_RUN_CONTEXT
|
||||
|
||||
################################################################
|
||||
# The glorious MockClock
|
||||
################################################################
|
||||
|
||||
|
||||
# Prior art:
|
||||
# https://twistedmatrix.com/documents/current/api/twisted.internet.task.Clock.html
|
||||
# https://github.com/ztellman/manifold/issues/57
|
||||
@final
|
||||
class MockClock(Clock):
|
||||
"""A user-controllable clock suitable for writing tests.
|
||||
|
||||
Args:
|
||||
rate (float): the initial :attr:`rate`.
|
||||
autojump_threshold (float): the initial :attr:`autojump_threshold`.
|
||||
|
||||
.. attribute:: rate
|
||||
|
||||
How many seconds of clock time pass per second of real time. Default is
|
||||
0.0, i.e. the clock only advances through manuals calls to :meth:`jump`
|
||||
or when the :attr:`autojump_threshold` is triggered. You can assign to
|
||||
this attribute to change it.
|
||||
|
||||
.. attribute:: autojump_threshold
|
||||
|
||||
The clock keeps an eye on the run loop, and if at any point it detects
|
||||
that all tasks have been blocked for this many real seconds (i.e.,
|
||||
according to the actual clock, not this clock), then the clock
|
||||
automatically jumps ahead to the run loop's next scheduled
|
||||
timeout. Default is :data:`math.inf`, i.e., to never autojump. You can
|
||||
assign to this attribute to change it.
|
||||
|
||||
Basically the idea is that if you have code or tests that use sleeps
|
||||
and timeouts, you can use this to make it run much faster, totally
|
||||
automatically. (At least, as long as those sleeps/timeouts are
|
||||
happening inside Trio; if your test involves talking to external
|
||||
service and waiting for it to timeout then obviously we can't help you
|
||||
there.)
|
||||
|
||||
You should set this to the smallest value that lets you reliably avoid
|
||||
"false alarms" where some I/O is in flight (e.g. between two halves of
|
||||
a socketpair) but the threshold gets triggered and time gets advanced
|
||||
anyway. This will depend on the details of your tests and test
|
||||
environment. If you aren't doing any I/O (like in our sleeping example
|
||||
above) then just set it to zero, and the clock will jump whenever all
|
||||
tasks are blocked.
|
||||
|
||||
.. note:: If you use ``autojump_threshold`` and
|
||||
`wait_all_tasks_blocked` at the same time, then you might wonder how
|
||||
they interact, since they both cause things to happen after the run
|
||||
loop goes idle for some time. The answer is:
|
||||
`wait_all_tasks_blocked` takes priority. If there's a task blocked
|
||||
in `wait_all_tasks_blocked`, then the autojump feature treats that
|
||||
as active task and does *not* jump the clock.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, rate: float = 0.0, autojump_threshold: float = inf) -> None:
|
||||
# when the real clock said 'real_base', the virtual time was
|
||||
# 'virtual_base', and since then it's advanced at 'rate' virtual
|
||||
# seconds per real second.
|
||||
self._real_base = 0.0
|
||||
self._virtual_base = 0.0
|
||||
self._rate = 0.0
|
||||
|
||||
# kept as an attribute so that our tests can monkeypatch it
|
||||
self._real_clock = time.perf_counter
|
||||
|
||||
# use the property update logic to set initial values
|
||||
self.rate = rate
|
||||
self.autojump_threshold = autojump_threshold
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<MockClock, time={self.current_time():.7f}, rate={self._rate} @ {id(self):#x}>"
|
||||
|
||||
@property
|
||||
def rate(self) -> float:
|
||||
return self._rate
|
||||
|
||||
@rate.setter
|
||||
def rate(self, new_rate: float) -> None:
|
||||
if new_rate < 0:
|
||||
raise ValueError("rate must be >= 0")
|
||||
else:
|
||||
real = self._real_clock()
|
||||
virtual = self._real_to_virtual(real)
|
||||
self._virtual_base = virtual
|
||||
self._real_base = real
|
||||
self._rate = float(new_rate)
|
||||
|
||||
@property
|
||||
def autojump_threshold(self) -> float:
|
||||
return self._autojump_threshold
|
||||
|
||||
@autojump_threshold.setter
|
||||
def autojump_threshold(self, new_autojump_threshold: float) -> None:
|
||||
self._autojump_threshold = float(new_autojump_threshold)
|
||||
self._try_resync_autojump_threshold()
|
||||
|
||||
# runner.clock_autojump_threshold is an internal API that isn't easily
|
||||
# usable by custom third-party Clock objects. If you need access to this
|
||||
# functionality, let us know, and we'll figure out how to make a public
|
||||
# API. Discussion:
|
||||
#
|
||||
# https://github.com/python-trio/trio/issues/1587
|
||||
def _try_resync_autojump_threshold(self) -> None:
|
||||
try:
|
||||
runner = GLOBAL_RUN_CONTEXT.runner
|
||||
if runner.is_guest:
|
||||
runner.force_guest_tick_asap()
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if runner.clock is self:
|
||||
runner.clock_autojump_threshold = self._autojump_threshold
|
||||
|
||||
# Invoked by the run loop when runner.clock_autojump_threshold is
|
||||
# exceeded.
|
||||
def _autojump(self) -> None:
|
||||
statistics = _core.current_statistics()
|
||||
jump = statistics.seconds_to_next_deadline
|
||||
if 0 < jump < inf:
|
||||
self.jump(jump)
|
||||
|
||||
def _real_to_virtual(self, real: float) -> float:
|
||||
real_offset = real - self._real_base
|
||||
virtual_offset = self._rate * real_offset
|
||||
return self._virtual_base + virtual_offset
|
||||
|
||||
def start_clock(self) -> None:
|
||||
self._try_resync_autojump_threshold()
|
||||
|
||||
def current_time(self) -> float:
|
||||
return self._real_to_virtual(self._real_clock())
|
||||
|
||||
def deadline_to_sleep_time(self, deadline: float) -> float:
|
||||
virtual_timeout = deadline - self.current_time()
|
||||
if virtual_timeout <= 0:
|
||||
return 0
|
||||
elif self._rate > 0:
|
||||
return virtual_timeout / self._rate
|
||||
else:
|
||||
return 999999999
|
||||
|
||||
def jump(self, seconds: float) -> None:
|
||||
"""Manually advance the clock by the given number of seconds.
|
||||
|
||||
Args:
|
||||
seconds (float): the number of seconds to jump the clock forward.
|
||||
|
||||
Raises:
|
||||
ValueError: if you try to pass a negative value for ``seconds``.
|
||||
|
||||
"""
|
||||
if seconds < 0:
|
||||
raise ValueError("time can't go backwards")
|
||||
self._virtual_base += seconds
|
||||
@@ -0,0 +1,317 @@
|
||||
# ParkingLot provides an abstraction for a fair waitqueue with cancellation
|
||||
# and requeuing support. Inspiration:
|
||||
#
|
||||
# https://webkit.org/blog/6161/locking-in-webkit/
|
||||
# https://amanieu.github.io/parking_lot/
|
||||
#
|
||||
# which were in turn heavily influenced by
|
||||
#
|
||||
# http://gee.cs.oswego.edu/dl/papers/aqs.pdf
|
||||
#
|
||||
# Compared to these, our use of cooperative scheduling allows some
|
||||
# simplifications (no need for internal locking). On the other hand, the need
|
||||
# to support Trio's strong cancellation semantics adds some complications
|
||||
# (tasks need to know where they're queued so they can cancel). Also, in the
|
||||
# above work, the ParkingLot is a global structure that holds a collection of
|
||||
# waitqueues keyed by lock address, and which are opportunistically allocated
|
||||
# and destroyed as contention arises; this allows the worst-case memory usage
|
||||
# for all waitqueues to be O(#tasks). Here we allocate a separate wait queue
|
||||
# for each synchronization object, so we're O(#objects + #tasks). This isn't
|
||||
# *so* bad since compared to our synchronization objects are heavier than
|
||||
# theirs and our tasks are lighter, so for us #objects is smaller and #tasks
|
||||
# is larger.
|
||||
#
|
||||
# This is in the core because for two reasons. First, it's used by
|
||||
# UnboundedQueue, and UnboundedQueue is used for a number of things in the
|
||||
# core. And second, it's responsible for providing fairness to all of our
|
||||
# high-level synchronization primitives (locks, queues, etc.). For now with
|
||||
# our FIFO scheduler this is relatively trivial (it's just a FIFO waitqueue),
|
||||
# but in the future we ever start support task priorities or fair scheduling
|
||||
#
|
||||
# https://github.com/python-trio/trio/issues/32
|
||||
#
|
||||
# then all we'll have to do is update this. (Well, full-fledged task
|
||||
# priorities might also require priority inheritance, which would require more
|
||||
# work.)
|
||||
#
|
||||
# For discussion of data structures to use here, see:
|
||||
#
|
||||
# https://github.com/dabeaz/curio/issues/136
|
||||
#
|
||||
# (and also the articles above). Currently we use a SortedDict ordered by a
|
||||
# global monotonic counter that ensures FIFO ordering. The main advantage of
|
||||
# this is that it's easy to implement :-). An intrusive doubly-linked list
|
||||
# would also be a natural approach, so long as we only handle FIFO ordering.
|
||||
#
|
||||
# XX: should we switch to the shared global ParkingLot approach?
|
||||
#
|
||||
# XX: we should probably add support for "parking tokens" to allow for
|
||||
# task-fair RWlock (basically: when parking a task needs to be able to mark
|
||||
# itself as a reader or a writer, and then a task-fair wakeup policy is, wake
|
||||
# the next task, and if it's a reader than keep waking tasks so long as they
|
||||
# are readers). Without this I think you can implement write-biased or
|
||||
# read-biased RWlocks (by using two parking lots and drawing from whichever is
|
||||
# preferred), but not task-fair -- and task-fair plays much more nicely with
|
||||
# WFQ. (Consider what happens in the two-lot implementation if you're
|
||||
# write-biased but all the pending writers are blocked at the scheduler level
|
||||
# by the WFQ logic...)
|
||||
# ...alternatively, "phase-fair" RWlocks are pretty interesting:
|
||||
# http://www.cs.unc.edu/~anderson/papers/ecrts09b.pdf
|
||||
# Useful summary:
|
||||
# https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReadWriteLock.html
|
||||
#
|
||||
# XX: if we do add WFQ, then we might have to drop the current feature where
|
||||
# unpark returns the tasks that were unparked. Rationale: suppose that at the
|
||||
# time we call unpark, the next task is deprioritized... and then, before it
|
||||
# becomes runnable, a new task parks which *is* runnable. Ideally we should
|
||||
# immediately wake the new task, and leave the old task on the queue for
|
||||
# later. But this means we can't commit to which task we are unparking when
|
||||
# unpark is called.
|
||||
#
|
||||
# See: https://github.com/python-trio/trio/issues/53
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import math
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import attrs
|
||||
import outcome
|
||||
|
||||
from .. import _core
|
||||
from .._util import final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from ._run import Task
|
||||
|
||||
|
||||
GLOBAL_PARKING_LOT_BREAKER: dict[Task, list[ParkingLot]] = {}
|
||||
|
||||
|
||||
def add_parking_lot_breaker(task: Task, lot: ParkingLot) -> None:
|
||||
"""Register a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`.
|
||||
|
||||
raises:
|
||||
trio.BrokenResourceError: if the task has already exited.
|
||||
"""
|
||||
if inspect.getcoroutinestate(task.coro) == inspect.CORO_CLOSED:
|
||||
raise _core._exceptions.BrokenResourceError(
|
||||
"Attempted to add already exited task as lot breaker.",
|
||||
)
|
||||
if task not in GLOBAL_PARKING_LOT_BREAKER:
|
||||
GLOBAL_PARKING_LOT_BREAKER[task] = [lot]
|
||||
else:
|
||||
GLOBAL_PARKING_LOT_BREAKER[task].append(lot)
|
||||
|
||||
|
||||
def remove_parking_lot_breaker(task: Task, lot: ParkingLot) -> None:
|
||||
"""Deregister a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`"""
|
||||
try:
|
||||
GLOBAL_PARKING_LOT_BREAKER[task].remove(lot)
|
||||
except (KeyError, ValueError):
|
||||
raise RuntimeError(
|
||||
"Attempted to remove task as breaker for a lot it is not registered for",
|
||||
) from None
|
||||
if not GLOBAL_PARKING_LOT_BREAKER[task]:
|
||||
del GLOBAL_PARKING_LOT_BREAKER[task]
|
||||
|
||||
|
||||
@attrs.frozen
|
||||
class ParkingLotStatistics:
|
||||
"""An object containing debugging information for a ParkingLot.
|
||||
|
||||
Currently, the following fields are defined:
|
||||
|
||||
* ``tasks_waiting`` (int): The number of tasks blocked on this lot's
|
||||
:meth:`trio.lowlevel.ParkingLot.park` method.
|
||||
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@final
|
||||
@attrs.define(eq=False)
|
||||
class ParkingLot:
|
||||
"""A fair wait queue with cancellation and requeuing.
|
||||
|
||||
This class encapsulates the tricky parts of implementing a wait
|
||||
queue. It's useful for implementing higher-level synchronization
|
||||
primitives like queues and locks.
|
||||
|
||||
In addition to the methods below, you can use ``len(parking_lot)`` to get
|
||||
the number of parked tasks, and ``if parking_lot: ...`` to check whether
|
||||
there are any parked tasks.
|
||||
|
||||
"""
|
||||
|
||||
# {task: None}, we just want a deque where we can quickly delete random
|
||||
# items
|
||||
_parked: OrderedDict[Task, None] = attrs.field(factory=OrderedDict, init=False)
|
||||
broken_by: list[Task] = attrs.field(factory=list, init=False)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Returns the number of parked tasks."""
|
||||
return len(self._parked)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""True if there are parked tasks, False otherwise."""
|
||||
return bool(self._parked)
|
||||
|
||||
# XX this currently returns None
|
||||
# if we ever add the ability to repark while one's resuming place in
|
||||
# line (for false wakeups), then we could have it return a ticket that
|
||||
# abstracts the "place in line" concept.
|
||||
@_core.enable_ki_protection
|
||||
async def park(self) -> None:
|
||||
"""Park the current task until woken by a call to :meth:`unpark` or
|
||||
:meth:`unpark_all`.
|
||||
|
||||
Raises:
|
||||
BrokenResourceError: if attempting to park in a broken lot, or the lot
|
||||
breaks before we get to unpark.
|
||||
|
||||
"""
|
||||
if self.broken_by:
|
||||
raise _core.BrokenResourceError(
|
||||
f"Attempted to park in parking lot broken by {self.broken_by}",
|
||||
)
|
||||
task = _core.current_task()
|
||||
self._parked[task] = None
|
||||
task.custom_sleep_data = self
|
||||
|
||||
def abort_fn(_: _core.RaiseCancelT) -> _core.Abort:
|
||||
del task.custom_sleep_data._parked[task]
|
||||
return _core.Abort.SUCCEEDED
|
||||
|
||||
await _core.wait_task_rescheduled(abort_fn)
|
||||
|
||||
def _pop_several(self, count: int | float) -> Iterator[Task]: # noqa: PYI041
|
||||
if isinstance(count, float):
|
||||
if math.isinf(count):
|
||||
count = len(self._parked)
|
||||
else:
|
||||
raise ValueError("Cannot pop a non-integer number of tasks.")
|
||||
else:
|
||||
count = min(count, len(self._parked))
|
||||
for _ in range(count):
|
||||
task, _ = self._parked.popitem(last=False)
|
||||
yield task
|
||||
|
||||
@_core.enable_ki_protection
|
||||
def unpark(self, *, count: int | float = 1) -> list[Task]: # noqa: PYI041
|
||||
"""Unpark one or more tasks.
|
||||
|
||||
This wakes up ``count`` tasks that are blocked in :meth:`park`. If
|
||||
there are fewer than ``count`` tasks parked, then wakes as many tasks
|
||||
are available and then returns successfully.
|
||||
|
||||
Args:
|
||||
count (int | math.inf): the number of tasks to unpark.
|
||||
|
||||
"""
|
||||
tasks = list(self._pop_several(count))
|
||||
for task in tasks:
|
||||
_core.reschedule(task)
|
||||
return tasks
|
||||
|
||||
def unpark_all(self) -> list[Task]:
|
||||
"""Unpark all parked tasks."""
|
||||
return self.unpark(count=len(self))
|
||||
|
||||
@_core.enable_ki_protection
|
||||
def repark(
|
||||
self,
|
||||
new_lot: ParkingLot,
|
||||
*,
|
||||
count: int | float = 1, # noqa: PYI041
|
||||
) -> None:
|
||||
"""Move parked tasks from one :class:`ParkingLot` object to another.
|
||||
|
||||
This dequeues ``count`` tasks from one lot, and requeues them on
|
||||
another, preserving order. For example::
|
||||
|
||||
async def parker(lot):
|
||||
print("sleeping")
|
||||
await lot.park()
|
||||
print("woken")
|
||||
|
||||
async def main():
|
||||
lot1 = trio.lowlevel.ParkingLot()
|
||||
lot2 = trio.lowlevel.ParkingLot()
|
||||
async with trio.open_nursery() as nursery:
|
||||
nursery.start_soon(parker, lot1)
|
||||
await trio.testing.wait_all_tasks_blocked()
|
||||
assert len(lot1) == 1
|
||||
assert len(lot2) == 0
|
||||
lot1.repark(lot2)
|
||||
assert len(lot1) == 0
|
||||
assert len(lot2) == 1
|
||||
# This wakes up the task that was originally parked in lot1
|
||||
lot2.unpark()
|
||||
|
||||
If there are fewer than ``count`` tasks parked, then reparks as many
|
||||
tasks as are available and then returns successfully.
|
||||
|
||||
Args:
|
||||
new_lot (ParkingLot): the parking lot to move tasks to.
|
||||
count (int|math.inf): the number of tasks to move.
|
||||
|
||||
"""
|
||||
if not isinstance(new_lot, ParkingLot):
|
||||
raise TypeError("new_lot must be a ParkingLot")
|
||||
for task in self._pop_several(count):
|
||||
new_lot._parked[task] = None
|
||||
task.custom_sleep_data = new_lot
|
||||
|
||||
def repark_all(self, new_lot: ParkingLot) -> None:
|
||||
"""Move all parked tasks from one :class:`ParkingLot` object to
|
||||
another.
|
||||
|
||||
See :meth:`repark` for details.
|
||||
|
||||
"""
|
||||
return self.repark(new_lot, count=len(self))
|
||||
|
||||
def break_lot(self, task: Task | None = None) -> None:
|
||||
"""Break this lot, with ``task`` noted as the task that broke it.
|
||||
|
||||
This causes all parked tasks to raise an error, and any
|
||||
future tasks attempting to park to error. Unpark & repark become no-ops as the
|
||||
parking lot is empty.
|
||||
|
||||
The error raised contains a reference to the task sent as a parameter. The task
|
||||
is also saved in the parking lot in the ``broken_by`` attribute.
|
||||
"""
|
||||
if task is None:
|
||||
task = _core.current_task()
|
||||
|
||||
# if lot is already broken, just mark this as another breaker and return
|
||||
if self.broken_by:
|
||||
self.broken_by.append(task)
|
||||
return
|
||||
|
||||
self.broken_by.append(task)
|
||||
|
||||
for parked_task in self._parked:
|
||||
_core.reschedule(
|
||||
parked_task,
|
||||
outcome.Error(
|
||||
_core.BrokenResourceError(f"Parking lot broken by {task}"),
|
||||
),
|
||||
)
|
||||
self._parked.clear()
|
||||
|
||||
def statistics(self) -> ParkingLotStatistics:
|
||||
"""Return an object containing debugging information.
|
||||
|
||||
Currently the following fields are defined:
|
||||
|
||||
* ``tasks_waiting``: The number of tasks blocked on this lot's
|
||||
:meth:`park` method.
|
||||
|
||||
"""
|
||||
return ParkingLotStatistics(tasks_waiting=len(self._parked))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._run import Runner, Task
|
||||
|
||||
|
||||
class RunContext(threading.local):
|
||||
runner: Runner
|
||||
task: Task
|
||||
|
||||
|
||||
GLOBAL_RUN_CONTEXT: Final = RunContext()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user