Skip to content

asyncio: _accept_connection keeps looping after a resource error, causing duplicate _start_serving reschedules #151615

Description

@deadlovelll

Bug report

Bug description:

In BaseSelectorEventLoop._accept_connection when sock.accept() fails when system is out of file descriptors (EMFILE/ENFILE/ENOBUFS/ENOMEM) code removes the reader and schedules a retry, but does not make return, which generates a lot of unnecessary calls.

# Lib/asyncio/selector_events.py:168
def _accept_connection(
        self, protocol_factory, sock,
        sslcontext=None, server=None, backlog=100,
        ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT,
        ssl_shutdown_timeout=constants.SSL_SHUTDOWN_TIMEOUT, context=None):
    for _ in range(backlog + 1):
        try:
            conn, addr = sock.accept()
            if self._debug:
                logger.debug("%r got a new connection from %r: %r",
                                server, addr, conn)
            conn.setblocking(False)
        except ConnectionAbortedError:
            continue
        except (BlockingIOError, InterruptedError):
            return
        except OSError as exc:
            if exc.errno in (errno.EMFILE, errno.ENFILE,
                                errno.ENOBUFS, errno.ENOMEM):
                self.call_exception_handler({
                    'message': 'socket.accept() out of system resource',
                    'exception': exc,
                    'socket': trsock.TransportSocket(sock),
                })
                self._remove_reader(sock.fileno())
                self.call_later(constants.ACCEPT_RETRY_DELAY,
                                self._start_serving,
                                protocol_factory, sock, sslcontext, server,
                                backlog, ssl_handshake_timeout,
                                ssl_shutdown_timeout, context)
            else:
                raise

The branch except (BlockingIOError, InterruptedError) already do early return, so this branch should too

Reproducer:

import errno, asyncio
from unittest import mock

loop = asyncio.new_event_loop()
sock = mock.Mock()
sock.accept.side_effect = OSError(errno.EMFILE, "too many open files")
loop.call_exception_handler = mock.Mock()
loop._remove_reader = mock.Mock()
loop.call_later = mock.Mock()

loop._accept_connection(mock.Mock(), sock, backlog=100)

print("accept calls   :", sock.accept.call_count)
print("retry schedules:", loop.call_later.call_count)

Expected:

accept calls   : 1
retry schedules: 1

Actual:

accept calls   : 101
retry schedules: 101

Proposed fix is to add the return statement :

except OSError as exc:
    if exc.errno in (errno.EMFILE, errno.ENFILE,
                        errno.ENOBUFS, errno.ENOMEM):
        self.call_exception_handler({
            'message': 'socket.accept() out of system resource',
            'exception': exc,
            'socket': trsock.TransportSocket(sock),
        })
        self._remove_reader(sock.fileno())
        self.call_later(constants.ACCEPT_RETRY_DELAY,
                        self._start_serving,
                        protocol_factory, sock, sslcontext, server,
                        backlog, ssl_handshake_timeout,
                        ssl_shutdown_timeout, context)
        return
    else:
        raise 

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions