|
|
import errno
|
|
|
import logging
|
|
|
import os
|
|
|
|
|
|
import trio
|
|
|
|
|
|
# Errors that accept(2) can return, and which indicate that the system is
|
|
|
# overloaded
|
|
|
ACCEPT_CAPACITY_ERRNOS = {
|
|
|
errno.EMFILE,
|
|
|
errno.ENFILE,
|
|
|
errno.ENOMEM,
|
|
|
errno.ENOBUFS,
|
|
|
}
|
|
|
|
|
|
# How long to sleep when we get one of those errors
|
|
|
SLEEP_TIME = 0.100
|
|
|
|
|
|
# The logger we use to complain when this happens
|
|
|
LOGGER = logging.getLogger("trio.serve_listeners")
|
|
|
|
|
|
|
|
|
async def _run_handler(stream, handler):
|
|
|
try:
|
|
|
await handler(stream)
|
|
|
finally:
|
|
|
await trio.aclose_forcefully(stream)
|
|
|
|
|
|
|
|
|
async def _serve_one_listener(listener, handler_nursery, handler):
|
|
|
async with listener:
|
|
|
while True:
|
|
|
try:
|
|
|
stream = await listener.accept()
|
|
|
except OSError as exc:
|
|
|
if exc.errno in ACCEPT_CAPACITY_ERRNOS:
|
|
|
LOGGER.error(
|
|
|
"accept returned %s (%s); retrying in %s seconds",
|
|
|
errno.errorcode[exc.errno],
|
|
|
os.strerror(exc.errno),
|
|
|
SLEEP_TIME,
|
|
|
exc_info=True,
|
|
|
)
|
|
|
await trio.sleep(SLEEP_TIME)
|
|
|
else:
|
|
|
raise
|
|
|
else:
|
|
|
handler_nursery.start_soon(_run_handler, stream, handler)
|
|
|
|
|
|
|
|
|
async def serve_listeners(
|
|
|
handler, listeners, *, handler_nursery=None, task_status=trio.TASK_STATUS_IGNORED
|
|
|
):
|
|
|
r"""Listen for incoming connections on ``listeners``, and for each one
|
|
|
start a task running ``handler(stream)``.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
If ``handler`` raises an exception, then this function doesn't do
|
|
|
anything special to catch it – so by default the exception will
|
|
|
propagate out and crash your server. If you don't want this, then catch
|
|
|
exceptions inside your ``handler``, or use a ``handler_nursery`` object
|
|
|
that responds to exceptions in some other way.
|
|
|
|
|
|
Args:
|
|
|
|
|
|
handler: An async callable, that will be invoked like
|
|
|
``handler_nursery.start_soon(handler, stream)`` for each incoming
|
|
|
connection.
|
|
|
|
|
|
listeners: A list of :class:`~trio.abc.Listener` objects.
|
|
|
:func:`serve_listeners` takes responsibility for closing them.
|
|
|
|
|
|
handler_nursery: The nursery used to start handlers, or any object with
|
|
|
a ``start_soon`` method. If ``None`` (the default), then
|
|
|
:func:`serve_listeners` will create a new nursery internally and use
|
|
|
that.
|
|
|
|
|
|
task_status: This function can be used with ``nursery.start``, which
|
|
|
will return ``listeners``.
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
This function never returns unless cancelled.
|
|
|
|
|
|
Resource handling:
|
|
|
|
|
|
If ``handler`` neglects to close the ``stream``, then it will be closed
|
|
|
using :func:`trio.aclose_forcefully`.
|
|
|
|
|
|
Error handling:
|
|
|
|
|
|
Most errors coming from :meth:`~trio.abc.Listener.accept` are allowed to
|
|
|
propagate out (crashing the server in the process). However, some errors –
|
|
|
those which indicate that the server is temporarily overloaded – are
|
|
|
handled specially. These are :class:`OSError`\s with one of the following
|
|
|
errnos:
|
|
|
|
|
|
* ``EMFILE``: process is out of file descriptors
|
|
|
* ``ENFILE``: system is out of file descriptors
|
|
|
* ``ENOBUFS``, ``ENOMEM``: the kernel hit some sort of memory limitation
|
|
|
when trying to create a socket object
|
|
|
|
|
|
When :func:`serve_listeners` gets one of these errors, then it:
|
|
|
|
|
|
* Logs the error to the standard library logger ``trio.serve_listeners``
|
|
|
(level = ERROR, with exception information included). By default this
|
|
|
causes it to be printed to stderr.
|
|
|
* Waits 100 ms before calling ``accept`` again, in hopes that the
|
|
|
system will recover.
|
|
|
|
|
|
"""
|
|
|
async with trio.open_nursery() as nursery:
|
|
|
if handler_nursery is None:
|
|
|
handler_nursery = nursery
|
|
|
for listener in listeners:
|
|
|
nursery.start_soon(_serve_one_listener, listener, handler_nursery, handler)
|
|
|
# The listeners are already queueing connections when we're called,
|
|
|
# but we wait until the end to call started() just in case we get an
|
|
|
# error or whatever.
|
|
|
task_status.started(listeners)
|