-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-41279: Add StreamReaderBufferedProtocol #21446
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
base: main
Are you sure you want to change the base?
Changes from all commits
9a65cfe
6db642c
71d6a90
cdc14d8
9e81f1a
3dc4d18
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 |
---|---|---|
|
@@ -112,7 +112,7 @@ def factory(): | |
return await loop.create_unix_server(factory, path, **kwds) | ||
|
||
|
||
class FlowControlMixin(protocols.Protocol): | ||
class FlowControlMixin(protocols.BaseProtocol): | ||
"""Reusable flow control logic for StreamWriter.drain(). | ||
|
||
This implements the protocol methods pause_writing(), | ||
|
@@ -180,7 +180,7 @@ def _get_close_waiter(self, stream): | |
raise NotImplementedError | ||
|
||
|
||
class StreamReaderProtocol(FlowControlMixin, protocols.Protocol): | ||
class BaseStreamReaderProtocol(FlowControlMixin): | ||
"""Helper class to adapt between Protocol and StreamReader. | ||
|
||
(This is a helper class instead of making StreamReader itself a | ||
|
@@ -267,11 +267,6 @@ def connection_lost(self, exc): | |
self._stream_writer = None | ||
self._transport = None | ||
|
||
def data_received(self, data): | ||
reader = self._stream_reader | ||
if reader is not None: | ||
reader.feed_data(data) | ||
|
||
def eof_received(self): | ||
reader = self._stream_reader | ||
if reader is not None: | ||
|
@@ -298,6 +293,30 @@ def __del__(self): | |
closed.exception() | ||
|
||
|
||
class StreamReaderProtocol(BaseStreamReaderProtocol, protocols.Protocol): | ||
def data_received(self, data): | ||
reader = self._stream_reader | ||
if reader is not None: | ||
reader.feed_data(data) | ||
|
||
|
||
class StreamReaderBufferedProtocol(BaseStreamReaderProtocol, protocols.BufferedProtocol): | ||
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. Sorry, I don't follow. 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. It isn't used right now, as there is no way to include this class without changing or adding to the API. 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. Please elaborate. I like this PR, it makes a value if the buffered protocol is used. But if this protocol is not used -- why we need the PR at all? I can imagine a little different approach: don't extract What do you think? 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. The problem with this approach is that the For example on a server with 10k clients, python will hold ~640mb of memory. This is why we should probably make it a custom choice of when to use the new But this means that we either change / add API or choose a set number defaulted to the protocol pool. Btw, doing the buffered protocol pool approach is not really trivial as I don't currently know when a 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. We already have I can imagine a variadic input buffer size starting from some default value and growing up to a limit but I doubt if we need it really. A simple implementation can be good enough. A pool of buffers is an interesting idea but, again, the feature can be added later. Thoughts? 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 don't really understand what you mean by using the At the beginning what I did was that if
I believe what you mean is already implemented in
I think this is the best solution, though we need to think if we want that feature to be in this PR or on a different one. By the way, if you know of a nice way of knowing when we should return a 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 reckon that the default buffer size is not necessarily decided by Additionally, it appears that |
||
def __init__(self, stream_reader, client_connected_cb=None, loop=None, | ||
buffer_size=65536): | ||
super().__init__(stream_reader, | ||
client_connected_cb=client_connected_cb, | ||
loop=loop) | ||
self._buffer = memoryview(bytearray(buffer_size)) | ||
|
||
def get_buffer(self, sizehint): | ||
return self._buffer | ||
|
||
def buffer_updated(self, nbytes): | ||
reader = self._stream_reader | ||
if reader is not None: | ||
reader.feed_data(self._buffer[:nbytes]) | ||
|
||
|
||
class StreamWriter: | ||
"""Wraps a Transport. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
from . import events | ||
from . import exceptions | ||
from . import futures | ||
from . import protocols | ||
from . import selector_events | ||
from . import tasks | ||
from . import transports | ||
|
@@ -480,9 +481,13 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): | |
os.set_blocking(self._fileno, False) | ||
|
||
self._loop.call_soon(self._protocol.connection_made, self) | ||
|
||
# only start reading when connection_made() has been called | ||
self._loop.call_soon(self._loop._add_reader, | ||
self._fileno, self._read_ready) | ||
if isinstance(protocol, protocols.BufferedProtocol): | ||
self._read_ready = self._readinto_buffer_ready | ||
else: | ||
self._read_ready = self._read_buffer_ready | ||
self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) | ||
if waiter is not None: | ||
# only wake up the waiter when connection_made() has been called | ||
self._loop.call_soon(futures._set_result_unless_cancelled, | ||
|
@@ -509,7 +514,37 @@ def __repr__(self): | |
info.append('closed') | ||
return '<{}>'.format(' '.join(info)) | ||
|
||
def _read_ready(self): | ||
def _readinto_buffer_ready(self): | ||
try: | ||
buf = self._protocol.get_buffer(-1) | ||
if not len(buf): | ||
raise RuntimeError('get_buffer() returned an empty buffer') | ||
except (SystemExit, KeyboardInterrupt): | ||
raise | ||
except BaseException as exc: | ||
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.
Never mind, this is how we typically invoke callback methods. 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._fatal_error( | ||
exc, 'Fatal error: protocol.get_buffer() call failed.') | ||
return | ||
|
||
nbytes = 0 | ||
try: | ||
nbytes = self._pipe.readinto(buf) | ||
except (BlockingIOError, InterruptedError): | ||
pass | ||
except OSError as exc: | ||
self._fatal_error(exc, 'Fatal read error on pipe transport') | ||
else: | ||
if nbytes: | ||
self._protocol.buffer_updated(nbytes) | ||
else: | ||
if self._loop.get_debug(): | ||
logger.info("%r was closed by peer", self) | ||
self._closing = True | ||
self._loop._remove_reader(self._fileno) | ||
self._loop.call_soon(self._protocol.eof_received) | ||
self._loop.call_soon(self._call_connection_lost, None) | ||
|
||
def _read_buffer_ready(self): | ||
try: | ||
data = os.read(self._fileno, self.max_size) | ||
except (BlockingIOError, InterruptedError): | ||
|
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.
I don't see the corresponding changes in selector_events.py. Does it handle these use cases already?
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.
Yes it has the following bit of code: