Repository navigation
bpo-34638: Store a weak reference to stream reader to break strong references loop #9201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
5c87303
3ac4338
465d9b3
5447b8a
521ec5d
a7b4ad1
806b5c2
119bfab
ced1bd2
1670250
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,16 @@ | |
| 'open_connection', 'start_server') | ||
|
|
||
| import socket | ||
| import sys | ||
| import weakref | ||
|
|
||
| if hasattr(socket, 'AF_UNIX'): | ||
| __all__ += ('open_unix_connection', 'start_unix_server') | ||
|
|
||
| from . import coroutines | ||
| from . import events | ||
| from . import exceptions | ||
| from . import format_helpers | ||
| from . import protocols | ||
| from .log import logger | ||
| from .tasks import sleep | ||
|
|
@@ -186,46 +189,107 @@ class StreamReaderProtocol(FlowControlMixin, protocols.Protocol): | |
| call inappropriate methods of the protocol.) | ||
| """ | ||
|
|
||
| _source_traceback = None | ||
|
|
||
| def __init__(self, stream_reader, client_connected_cb=None, loop=None): | ||
| super().__init__(loop=loop) | ||
| self._stream_reader = stream_reader | ||
| if stream_reader is not None: | ||
| self._stream_reader_wr = weakref.ref(stream_reader, | ||
| self._on_reader_gc) | ||
| self._source_traceback = stream_reader._source_traceback | ||
| else: | ||
| self._stream_reader_wr = None | ||
| if client_connected_cb is not None: | ||
| # server socket | ||
| # we need to keep a strong reference to the reader | ||
| # until connection is made | ||
| self._strong_reader = stream_reader | ||
| self._reject_connection = False | ||
| self._stream_writer = None | ||
| self._transport = None | ||
| self._client_connected_cb = client_connected_cb | ||
| self._over_ssl = False | ||
| self._closed = self._loop.create_future() | ||
|
|
||
| def _on_reader_gc(self, wr): | ||
| transport = self._transport | ||
| if transport is not None: | ||
| # connection_made was called | ||
| context = { | ||
| 'message': ("Close transport. " | ||
| "A stream was destroyed without " | ||
| "stream.close() call") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rephrase as: |
||
| } | ||
| if self._source_traceback: | ||
| context['source_traceback'] = self._source_traceback | ||
| self._loop.call_exception_handler(context) | ||
| transport.abort() | ||
| else: | ||
| self._reject_connection = True | ||
| self._stream_reader_wr = None | ||
|
|
||
| def _untrack_reader(self): | ||
| self._stream_reader_wr = None | ||
|
|
||
| @property | ||
| def _stream_reader(self): | ||
| if self._stream_reader_wr is None: | ||
| return None | ||
| return self._stream_reader_wr() | ||
|
|
||
| def connection_made(self, transport): | ||
| self._stream_reader.set_transport(transport) | ||
| if self._reject_connection: | ||
| context = { | ||
| 'message': ("Close transport. " | ||
| "a stream was destroyed " | ||
| "before connection establishment") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| if self._source_traceback: | ||
| context['source_traceback'] = self._source_traceback | ||
| self._loop.call_exception_handler(context) | ||
| transport.abort() | ||
| return | ||
| self._transport = transport | ||
| reader = self._stream_reader | ||
| if reader is not None: | ||
| reader.set_transport(transport) | ||
| self._over_ssl = transport.get_extra_info('sslcontext') is not None | ||
| if self._client_connected_cb is not None: | ||
| self._stream_writer = StreamWriter(transport, self, | ||
| self._stream_reader, | ||
| reader, | ||
| self._loop) | ||
| res = self._client_connected_cb(self._stream_reader, | ||
| res = self._client_connected_cb(reader, | ||
| self._stream_writer) | ||
| if coroutines.iscoroutine(res): | ||
| self._loop.create_task(res) | ||
| self._strong_reader = None | ||
|
|
||
| def connection_lost(self, exc): | ||
| if self._stream_reader is not None: | ||
| reader = self._stream_reader | ||
| if reader is not None: | ||
| if exc is None: | ||
| self._stream_reader.feed_eof() | ||
| reader.feed_eof() | ||
| else: | ||
| self._stream_reader.set_exception(exc) | ||
| reader.set_exception(exc) | ||
| if not self._closed.done(): | ||
| if exc is None: | ||
| self._closed.set_result(None) | ||
| else: | ||
| self._closed.set_exception(exc) | ||
| super().connection_lost(exc) | ||
| self._stream_reader = None | ||
| self._stream_reader_wr = None | ||
| self._stream_writer = None | ||
| self._transport = None | ||
|
|
||
| def data_received(self, data): | ||
| self._stream_reader.feed_data(data) | ||
| reader = self._stream_reader | ||
| if reader is not None: | ||
| reader.feed_data(data) | ||
|
|
||
| def eof_received(self): | ||
| self._stream_reader.feed_eof() | ||
| reader = self._stream_reader | ||
| if reader is not None: | ||
| reader.feed_eof() | ||
| if self._over_ssl: | ||
| # Prevent a warning in SSLProtocol.eof_received: | ||
| # "returning true from eof_received() | ||
|
|
@@ -282,6 +346,9 @@ def can_write_eof(self): | |
| return self._transport.can_write_eof() | ||
|
|
||
| def close(self): | ||
| # a reader could be garbage collected / destroyed | ||
| # after connection closing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| self._protocol._untrack_reader() | ||
| return self._transport.close() | ||
|
|
||
| def is_closing(self): | ||
|
|
@@ -318,6 +385,8 @@ async def drain(self): | |
|
|
||
| class StreamReader: | ||
|
|
||
| _source_traceback = None | ||
|
|
||
| def __init__(self, limit=_DEFAULT_LIMIT, loop=None): | ||
| # The line length limit is a security feature; | ||
| # it also doubles as half the buffer limit. | ||
|
|
@@ -336,6 +405,9 @@ def __init__(self, limit=_DEFAULT_LIMIT, loop=None): | |
| self._exception = None | ||
| self._transport = None | ||
| self._paused = False | ||
| if self._loop.get_debug(): | ||
| self._source_traceback = format_helpers.extract_stack( | ||
| sys._getframe(1)) | ||
|
|
||
| def __repr__(self): | ||
| info = ['StreamReader'] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,11 @@ def __repr__(self): | |
| info.append(f'stderr={self.stderr!r}') | ||
| return '<{}>'.format(' '.join(info)) | ||
|
|
||
| def _untrack_reader(self): | ||
| # a dummy placeholder subprocess protocol doesn't need it but | ||
| # the method is required for regular stream implementation. | ||
| pass | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| def connection_made(self, transport): | ||
| self._transport = transport | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Store a weak reference to stream reader to break strong references loop | ||
| between reader and protocol. It allows to detect and close the socket if | ||
| the stream is deleted (garbage collected) without ``close()`` call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.