Skip to content

Commit a1b0e7d

Browse files
committed
Issue python#27906: Fix socket accept exhaustion during high TCP traffic.
Patch by Kevin Conway.
1 parent 4357cf6 commit a1b0e7d

File tree

6 files changed

+60
-36
lines changed

6 files changed

+60
-36
lines changed

Lib/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

Lib/asyncio/proactor_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def _write_to_self(self):
494494
self._csock.send(b'\0')
495495

496496
def _start_serving(self, protocol_factory, sock,
497-
sslcontext=None, server=None):
497+
sslcontext=None, server=None, backlog=100):
498498

499499
def loop(f=None):
500500
try:

Lib/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,

Lib/test/test_asyncio/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

Lib/test/test_asyncio/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

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ Library
263263

264264
- Issue #27456: asyncio: Set TCP_NODELAY by default.
265265

266+
- Issue #27906: Fix socket accept exhaustion during high TCP traffic.
267+
Patch by Kevin Conway.
268+
266269
IDLE
267270
----
268271

0 commit comments

Comments
 (0)