Repository navigation
bpo-32622: Implement loop.sendfile() #5271
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 1 commit
fa07b35
8ebd388
c7194f7
1632e0a
535be77
bd8c515
fab4669
2f51d6a
d36fcdf
defc61c
2ae8bf5
6b30924
9271eb1
d2bb432
2bf14d3
c1ad2f4
57f02cf
968c8c5
b905d1d
2969cf4
a9af6c7
e5f24b6
bc458b3
e1a173e
032bd40
b466ed3
359181d
1169407
312e2c5
cdc898d
df556f2
5a9d056
7f99184
b8f2c1a
4224bde
3975b5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ | |
| from . import coroutines | ||
| from . import events | ||
| from . import futures | ||
| from . import protocols | ||
| from . import sslproto | ||
| from . import tasks | ||
| from .log import logger | ||
|
|
@@ -155,6 +156,60 @@ def _run_until_complete_cb(fut): | |
| futures._get_loop(fut).stop() | ||
|
|
||
|
|
||
|
|
||
| class _SendfileProtocol(protocols.Protocol): | ||
| def __init__(self, transp): | ||
| # transport should be _FlowControlMixin instance | ||
|
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. Can you add an
Contributor
Author
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. Perform the check in debug mode only maybe?
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._transport = transp | ||
| self._proto = transp.get_protocol() | ||
| self._resume_reading = transp.is_reading() | ||
| self._write_paused = transp._protocol_paused | ||
|
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 call these '_should_resume_reading' and '_should_resume_writing' |
||
| transp.pause_reading() | ||
| transp.set_protocol(self) | ||
| if self._write_paused: | ||
| self._paused = self._transport._loop.create_future() | ||
|
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. Can you add a comment explaining the purpose of
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. Maybe we can give
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. Let's rename it to
Contributor
Author
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.
But the discussed attribute reports about write pausing state.
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. You can call it
Contributor
Author
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.
|
||
| else: | ||
| self._paused = None | ||
|
|
||
| def connection_made(self, transport): | ||
| raise RuntimeError("Broken state, " | ||
| "connection should be established already.") | ||
|
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. "Invalid state: connection should have been established already"
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. "should be" -> "should have been" |
||
|
|
||
| def connection_lost(self, exc): | ||
| if self._paused is not None: | ||
| if exc is None: | ||
| self._paused.set_result(True) | ||
| else: | ||
| self._paused.set_exception(exc) | ||
| self._proto.connection_lost(exc) | ||
|
|
||
| def pause_writing(self): | ||
| if self._paused is not None: | ||
| return | ||
| self._paused = self._transport._loop.create_future() | ||
|
|
||
| def resume_writing(self): | ||
| if self._paused is None: | ||
| return | ||
| self._paused.set_result(False) | ||
| self._paused = None | ||
|
|
||
| def data_received(self, data): | ||
| raise RuntimeError("Broken state, reading should be paused") | ||
|
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. ditto |
||
|
|
||
| def eof_received(self): | ||
| raise RuntimeError("Broken state, reading should be paused") | ||
|
|
||
| async def restore(self): | ||
| self._transport.set_protocol(self._proto) | ||
| if self._resume_reading: | ||
| self._transport.resume_reading() | ||
| if self._paused is not None: | ||
| await self._paused | ||
|
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. This code is complex so I want it to be crystal clear. Please add a comment before the await explaining what exactly we are waiting for.
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. Something akin to: if self._can_write is not None:
# Wait until the transport's buffer is drained and
# writing can be resumed. This is important to
# allow the replaced protocol to resume its operations.
await self._can_write
self._can_write = None |
||
| if self._write_paused: | ||
| self._proto.resume_writing() | ||
|
|
||
|
|
||
| class Server(events.AbstractServer): | ||
|
|
||
| def __init__(self, loop, sockets): | ||
|
|
@@ -866,6 +921,62 @@ async def _create_connection_transport( | |
|
|
||
| return transport, protocol | ||
|
|
||
| async def sendfile(self, transport, file, offset=0, count=None, | ||
| *, fallback=True): | ||
| """Send a file through a transport. | ||
|
|
||
| Return amount of sent bytes. | ||
| """ | ||
|
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. Copy the relevant snippet from the documentation to make it a proper docstring.
Contributor
Author
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. Did it but I feel that docstring is too long. |
||
| mode = getattr(transport, '_sendfile_compatible', | ||
| constants._SendfileMode.UNSUPPORTED) | ||
| if mode is constants._SendfileMode.UNSUPPORTED: | ||
| raise RuntimeError( | ||
| f"sendfile is not supported for transport {transport!r}") | ||
| if mode is constants._SendfileMode.NATIVE: | ||
|
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 call it TRY_NATIVE |
||
| try: | ||
| await self._sendfile_native(transport, file, | ||
| offset, count) | ||
| return | ||
| except events.SendfileNotAvailableError as exc: | ||
| if not fallback: | ||
| raise | ||
| # the mode is FALLBACK or fallback is True | ||
|
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 put an |
||
| await self._sendfile_fallback(transport, file, | ||
| offset, count) | ||
|
|
||
| async def _sendfile_native(self, transp, file, offset, count): | ||
| raise events.SendfileNotAvailableError( | ||
| "sendfile syscall is not supported") | ||
|
|
||
| async def _sendfile_fallback(self, transp, file, offset, count): | ||
| if offset: | ||
| file.seek(offset) | ||
| blocksize = min(count, 16384) if count else 16384 | ||
| buf = bytearray(blocksize) | ||
| total_sent = 0 | ||
| proto = _SendfileProtocol(transp) | ||
| try: | ||
| while True: | ||
| if count: | ||
| blocksize = min(count - total_sent, blocksize) | ||
| if blocksize <= 0: | ||
| break | ||
| fut = proto._paused | ||
| if fut is not None: | ||
|
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 think it would be more readable if await proto.drain()
...
transp.write(..)
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. Where async def drain():
if self._can_write is None:
return
await self._can_write
self._can_write = None
Contributor
Author
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. No. Also I don't line
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.
Yes, because the code is super hard to read right now. Since it's a private method and the code will certainly look nicer/tighter, then why not? |
||
| if await fut: | ||
|
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. Let's use another enum for the values of |
||
| # eof received | ||
| return | ||
| view = memoryview(buf)[:blocksize] | ||
| read = file.readinto(view) | ||
| if not read: | ||
| break # EOF | ||
| transp.write(view) | ||
| total_sent += read | ||
| finally: | ||
| if total_sent > 0 and hasattr(file, 'seek'): | ||
| file.seek(offset + total_sent) | ||
| await proto.restore() | ||
|
|
||
| async def start_tls(self, transport, protocol, sslcontext, *, | ||
| server_side=False, | ||
| server_hostname=None, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import enum | ||
|
|
||
| # After the connection is lost, log warnings after this many write()s. | ||
| LOG_THRESHOLD_FOR_CONNLOST_WRITES = 5 | ||
|
|
||
|
|
@@ -11,3 +13,10 @@ | |
|
|
||
| # Number of seconds to wait for SSL handshake to complete | ||
| SSL_HANDSHAKE_TIMEOUT = 10.0 | ||
|
|
||
| # The enum should be here to break circular dependencies between | ||
| # base_events and sslproto | ||
| class _SendfileMode(enum.Enum): | ||
|
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. This feels like a wrong place to define an enum. Maybe base_events?
Contributor
Author
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. See comment above about circular deps. Moreover they do import each other.
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. OK. |
||
| UNSUPPORTED = enum.auto() | ||
| NATIVE = enum.auto() | ||
| FALLBACK = enum.auto() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -540,6 +540,22 @@ def _sock_accept(self, fut, registered, sock): | |
| else: | ||
| fut.set_result((conn, address)) | ||
|
|
||
| async def _sendfile_native(self, transp, file, offset, count): | ||
| del self._transports[transp._sock_fd] | ||
| resume_reading = transp.is_reading() | ||
| transp.pause_reading() | ||
| fut = transp._make_empty_waiter() | ||
| await fut | ||
| sock = transp._sock | ||
|
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. Do you need the |
||
| try: | ||
|
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.
|
||
| return await self.sock_sendfile(sock, file, offset, count, | ||
| fallback=False) | ||
| finally: | ||
| transp._reset_empty_waiter() | ||
| if resume_reading: | ||
| transp.resume_reading() | ||
| self._transports[transp._sock_fd] = transp | ||
|
|
||
| def _process_events(self, event_list): | ||
| for key, mask in event_list: | ||
| fileobj, (reader, writer) = key.fileobj, key.data | ||
|
|
@@ -695,12 +711,14 @@ def get_write_buffer_size(self): | |
| class _SelectorSocketTransport(_SelectorTransport): | ||
|
|
||
| _start_tls_compatible = True | ||
| _sendfile_compatible = constants._SendfileMode.NATIVE | ||
|
|
||
| def __init__(self, loop, sock, protocol, waiter=None, | ||
| extra=None, server=None): | ||
| super().__init__(loop, sock, protocol, extra, server) | ||
| self._eof = False | ||
| self._paused = False | ||
| self._empty_waiter = None | ||
|
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.
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. This case is still isn't handled.
Contributor
Author
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. Cancelling is confusing.
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. Whatever, just make sure that if something awaits on |
||
|
|
||
| # Disable the Nagle algorithm -- small writes will be | ||
| # sent without waiting for the TCP ACK. This generally | ||
|
|
@@ -765,6 +783,9 @@ def write(self, data): | |
| f'not {type(data).__name__!r}') | ||
| if self._eof: | ||
| raise RuntimeError('Cannot call write() after write_eof()') | ||
| if self._empty_waiter is not None: | ||
| raise RuntimeError('Cannot call write() when loop.sendfile() ' | ||
| 'is not finished') | ||
|
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 not data: | ||
| return | ||
|
|
||
|
|
@@ -798,6 +819,8 @@ def _write_ready(self): | |
| assert self._buffer, 'Data should not be empty' | ||
|
|
||
| if self._conn_lost: | ||
| if self._empty_waiter is not None: | ||
| self._empty_waiter.set_result(True) | ||
| return | ||
| try: | ||
| n = self._sock.send(self._buffer) | ||
|
|
@@ -807,12 +830,16 @@ def _write_ready(self): | |
| self._loop._remove_writer(self._sock_fd) | ||
| self._buffer.clear() | ||
| self._fatal_error(exc, 'Fatal write error on socket transport') | ||
| if self._empty_waiter is not None: | ||
| self._empty_waiter.set_exception(exc) | ||
| else: | ||
| if n: | ||
| del self._buffer[:n] | ||
| self._maybe_resume_protocol() # May append to buffer. | ||
| if not self._buffer: | ||
| self._loop._remove_writer(self._sock_fd) | ||
| if self._empty_waiter is not None: | ||
| self._empty_waiter.set_result(False) | ||
| if self._closing: | ||
| self._call_connection_lost(None) | ||
| elif self._eof: | ||
|
|
@@ -828,6 +855,17 @@ def write_eof(self): | |
| def can_write_eof(self): | ||
| return True | ||
|
|
||
| def _make_empty_waiter(self): | ||
| if self._empty_waiter is not None: | ||
| raise RuntimeError("Empty waiter is already set") | ||
| self._empty_waiter = self._loop.create_future() | ||
| if not self._buffer: | ||
| self._empty_waiter.set_result(None) | ||
| return self._empty_waiter | ||
|
|
||
| def _reset_empty_waiter(self): | ||
| self._empty_waiter = None | ||
|
|
||
|
|
||
| class _SelectorDatagramTransport(_SelectorTransport): | ||
|
|
||
|
|
||
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.
Rename to
_SendfileFallbackProtocol?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.
Sorry, maybe I'm too tired but I want to ask explicitly: do you prefer java-stylish
VeryLongNameDesribingTheUniversityAndAllRelatedThingsnames?The
_SendfileProtocolname is private. We can change it at any moment. User should never see/touch it. There is not name conflict, and the name is not cryptic abbreviation.Why use longer version?
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 care about longer version because I want the code of asyncio to be easy to read.
I know about your work on this PR and we've discussed it many times privately, but still, it took me a while to read and understand the patch. Here I was thinking that this protocol is used even for native-sendfile implementation.
_SendfileProtocolvs_SendfileFallbackProtocolis not a Java-like name. It's about as long as_UnixSelectorEventLoop. Feel free to grep Python source code and discover that there are plenty of way longer class names.In this case,
_SendfileFallbackProtocoldisambiguates the purpose of this protocol, suggesting the reader what is the actual purpose of this piece of code.