Skip to content

Commit 9579a87

Browse files
committed
Accept multiple connections on listener EVENT_READ
When a large number of connections are made at once the event loop struggles to accept them because it only accepts and begins handling one new connection on each pass of the loop in which the EVENT_READ is emitted for the listening socket. This causes incoming connections to time out and sever. This patch uses the backlog setting of the listening socket to allow for accepting a bounded number of new incoming connections rather than a single one. Patch by Kevin Conway <[email protected]>.
1 parent a97ffba commit 9579a87

File tree

4 files changed

+56
-35
lines changed

4 files changed

+56
-35
lines changed

asyncio/base_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ def create_server(self, protocol_factory, host=None, port=None,
10341034
for sock in sockets:
10351035
sock.listen(backlog)
10361036
sock.setblocking(False)
1037-
self._start_serving(protocol_factory, sock, ssl, server)
1037+
self._start_serving(protocol_factory, sock, ssl, server, backlog)
10381038
if self._debug:
10391039
logger.info("%r is serving", server)
10401040
return server

asyncio/selector_events.py

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -162,43 +162,50 @@ def _write_to_self(self):
162162
exc_info=True)
163163

164164
def _start_serving(self, protocol_factory, sock,
165-
sslcontext=None, server=None):
165+
sslcontext=None, server=None, backlog=100):
166166
self.add_reader(sock.fileno(), self._accept_connection,
167-
protocol_factory, sock, sslcontext, server)
167+
protocol_factory, sock, sslcontext, server, backlog)
168168

169169
def _accept_connection(self, protocol_factory, sock,
170-
sslcontext=None, server=None):
171-
try:
172-
conn, addr = sock.accept()
173-
if self._debug:
174-
logger.debug("%r got a new connection from %r: %r",
175-
server, addr, conn)
176-
conn.setblocking(False)
177-
except (BlockingIOError, InterruptedError, ConnectionAbortedError):
178-
pass # False alarm.
179-
except OSError as exc:
180-
# There's nowhere to send the error, so just log it.
181-
if exc.errno in (errno.EMFILE, errno.ENFILE,
182-
errno.ENOBUFS, errno.ENOMEM):
183-
# Some platforms (e.g. Linux keep reporting the FD as
184-
# ready, so we remove the read handler temporarily.
185-
# We'll try again in a while.
186-
self.call_exception_handler({
187-
'message': 'socket.accept() out of system resource',
188-
'exception': exc,
189-
'socket': sock,
190-
})
191-
self.remove_reader(sock.fileno())
192-
self.call_later(constants.ACCEPT_RETRY_DELAY,
193-
self._start_serving,
194-
protocol_factory, sock, sslcontext, server)
170+
sslcontext=None, server=None, backlog=100):
171+
# This method is only called once for each event loop tick where the
172+
# listening socket has triggered an EVENT_READ. There may be multiple
173+
# connections waiting for an .accept() so it is called in a loop.
174+
# See https://bugs.python.org/issue27906 for more details.
175+
for _ in range(backlog):
176+
try:
177+
conn, addr = sock.accept()
178+
if self._debug:
179+
logger.debug("%r got a new connection from %r: %r",
180+
server, addr, conn)
181+
conn.setblocking(False)
182+
except (BlockingIOError, InterruptedError, ConnectionAbortedError):
183+
# Early exit because the socket accept buffer is empty.
184+
return None
185+
except OSError as exc:
186+
# There's nowhere to send the error, so just log it.
187+
if exc.errno in (errno.EMFILE, errno.ENFILE,
188+
errno.ENOBUFS, errno.ENOMEM):
189+
# Some platforms (e.g. Linux keep reporting the FD as
190+
# ready, so we remove the read handler temporarily.
191+
# We'll try again in a while.
192+
self.call_exception_handler({
193+
'message': 'socket.accept() out of system resource',
194+
'exception': exc,
195+
'socket': sock,
196+
})
197+
self.remove_reader(sock.fileno())
198+
self.call_later(constants.ACCEPT_RETRY_DELAY,
199+
self._start_serving,
200+
protocol_factory, sock, sslcontext, server,
201+
backlog)
202+
else:
203+
raise # The event loop will catch, log and ignore it.
195204
else:
196-
raise # The event loop will catch, log and ignore it.
197-
else:
198-
extra = {'peername': addr}
199-
accept = self._accept_connection2(protocol_factory, conn, extra,
200-
sslcontext, server)
201-
self.create_task(accept)
205+
extra = {'peername': addr}
206+
accept = self._accept_connection2(protocol_factory, conn, extra,
207+
sslcontext, server)
208+
self.create_task(accept)
202209

203210
@coroutine
204211
def _accept_connection2(self, protocol_factory, conn, extra,

tests/test_base_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@ def test_accept_connection_exception(self, m_log):
16341634
self.loop.call_later.assert_called_with(constants.ACCEPT_RETRY_DELAY,
16351635
# self.loop._start_serving
16361636
mock.ANY,
1637-
MyProto, sock, None, None)
1637+
MyProto, sock, None, None, mock.ANY)
16381638

16391639
def test_call_coroutine(self):
16401640
@asyncio.coroutine

tests/test_selector_events.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,20 @@ def test_process_events_write_cancelled(self):
687687
selectors.EVENT_WRITE)])
688688
self.loop.remove_writer.assert_called_with(1)
689689

690+
def test_accept_connection_multiple(self):
691+
sock = mock.Mock()
692+
sock.accept.return_value = (mock.Mock(), mock.Mock())
693+
backlog = 100
694+
# Mock the coroutine generation for a connection to prevent
695+
# warnings related to un-awaited coroutines.
696+
mock_obj = mock.patch.object
697+
with mock_obj(self.loop, '_accept_connection2') as accept2_mock:
698+
accept2_mock.return_value = None
699+
with mock_obj(self.loop, 'create_task') as task_mock:
700+
task_mock.return_value = None
701+
self.loop._accept_connection(mock.Mock(), sock, backlog=backlog)
702+
self.assertEqual(sock.accept.call_count, backlog)
703+
690704

691705
class SelectorTransportTests(test_utils.TestCase):
692706

0 commit comments

Comments
 (0)