diff options
author | Andrew Svetlov <andrew.svetlov@gmail.com> | 2018-01-16 17:59:34 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-16 17:59:34 (GMT) |
commit | 6b5a27975a415108a5eac12ee302bf2b3233f4d4 (patch) | |
tree | 09e3233c5c9c9b269c5cc47a0ed97a151280daac /Lib/asyncio | |
parent | c495e799ed376af91ae2ddf6c4bcc592490fe294 (diff) | |
download | cpython-6b5a27975a415108a5eac12ee302bf2b3233f4d4.zip cpython-6b5a27975a415108a5eac12ee302bf2b3233f4d4.tar.gz cpython-6b5a27975a415108a5eac12ee302bf2b3233f4d4.tar.bz2 |
bpo-32410: Implement loop.sock_sendfile() (#4976)
Diffstat (limited to 'Lib/asyncio')
-rw-r--r-- | Lib/asyncio/base_events.py | 70 | ||||
-rw-r--r-- | Lib/asyncio/events.py | 4 | ||||
-rw-r--r-- | Lib/asyncio/unix_events.py | 93 |
3 files changed, 167 insertions, 0 deletions
diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index ab00231..b6a9384 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -154,6 +154,10 @@ def _run_until_complete_cb(fut): futures._get_loop(fut).stop() +class _SendfileNotAvailable(RuntimeError): + pass + + class Server(events.AbstractServer): def __init__(self, loop, sockets): @@ -647,6 +651,72 @@ class BaseEventLoop(events.AbstractEventLoop): return await self.run_in_executor( None, socket.getnameinfo, sockaddr, flags) + async def sock_sendfile(self, sock, file, offset=0, count=None, + *, fallback=True): + if self._debug and sock.gettimeout() != 0: + raise ValueError("the socket must be non-blocking") + self._check_sendfile_params(sock, file, offset, count) + try: + return await self._sock_sendfile_native(sock, file, + offset, count) + except _SendfileNotAvailable as exc: + if fallback: + return await self._sock_sendfile_fallback(sock, file, + offset, count) + else: + raise RuntimeError(exc.args[0]) from None + + async def _sock_sendfile_native(self, sock, file, offset, count): + # NB: sendfile syscall is not supported for SSL sockets and + # non-mmap files even if sendfile is supported by OS + raise _SendfileNotAvailable( + f"syscall sendfile is not available for socket {sock!r} " + "and file {file!r} combination") + + async def _sock_sendfile_fallback(self, sock, file, offset, count): + if offset: + file.seek(offset) + blocksize = min(count, 16384) if count else 16384 + buf = bytearray(blocksize) + total_sent = 0 + try: + while True: + if count: + blocksize = min(count - total_sent, blocksize) + if blocksize <= 0: + break + view = memoryview(buf)[:blocksize] + read = file.readinto(view) + if not read: + break # EOF + await self.sock_sendall(sock, view) + total_sent += read + return total_sent + finally: + if total_sent > 0 and hasattr(file, 'seek'): + file.seek(offset + total_sent) + + def _check_sendfile_params(self, sock, file, offset, count): + if 'b' not in getattr(file, 'mode', 'b'): + raise ValueError("file should be opened in binary mode") + if not sock.type == socket.SOCK_STREAM: + raise ValueError("only SOCK_STREAM type sockets are supported") + if count is not None: + if not isinstance(count, int): + raise TypeError( + "count must be a positive integer (got {!r})".format(count)) + if count <= 0: + raise ValueError( + "count must be a positive integer (got {!r})".format(count)) + if not isinstance(offset, int): + raise TypeError( + "offset must be a non-negative integer (got {!r})".format( + offset)) + if offset < 0: + raise ValueError( + "offset must be a non-negative integer (got {!r})".format( + offset)) + async def create_connection( self, protocol_factory, host=None, port=None, *, ssl=None, family=0, diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index af4545b..b06721f 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -464,6 +464,10 @@ class AbstractEventLoop: async def sock_accept(self, sock): raise NotImplementedError + async def sock_sendfile(self, sock, file, offset=0, count=None, + *, fallback=None): + raise NotImplementedError + # Signal handling. def add_signal_handler(self, sig, callback, *args): diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 4f6beb4..f40ef12 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -1,6 +1,7 @@ """Selector event loop for Unix with signal handling.""" import errno +import io import os import selectors import signal @@ -308,6 +309,98 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop): ssl_handshake_timeout=ssl_handshake_timeout) return server + async def _sock_sendfile_native(self, sock, file, offset, count): + try: + os.sendfile + except AttributeError as exc: + raise base_events._SendfileNotAvailable( + "os.sendfile() is not available") + try: + fileno = file.fileno() + except (AttributeError, io.UnsupportedOperation) as err: + raise base_events._SendfileNotAvailable("not a regular file") + try: + fsize = os.fstat(fileno).st_size + except OSError as err: + raise base_events._SendfileNotAvailable("not a regular file") + blocksize = count if count else fsize + if not blocksize: + return 0 # empty file + + fut = self.create_future() + self._sock_sendfile_native_impl(fut, None, sock, fileno, + offset, count, blocksize, 0) + return await fut + + def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno, + offset, count, blocksize, total_sent): + fd = sock.fileno() + if registered_fd is not None: + # Remove the callback early. It should be rare that the + # selector says the fd is ready but the call still returns + # EAGAIN, and I am willing to take a hit in that case in + # order to simplify the common case. + self.remove_writer(registered_fd) + if fut.cancelled(): + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + return + if count: + blocksize = count - total_sent + if blocksize <= 0: + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + fut.set_result(total_sent) + return + + try: + sent = os.sendfile(fd, fileno, offset, blocksize) + except (BlockingIOError, InterruptedError): + if registered_fd is None: + self._sock_add_cancellation_callback(fut, sock) + self.add_writer(fd, self._sock_sendfile_native_impl, fut, + fd, sock, fileno, + offset, count, blocksize, total_sent) + except OSError as exc: + if total_sent == 0: + # We can get here for different reasons, the main + # one being 'file' is not a regular mmap(2)-like + # file, in which case we'll fall back on using + # plain send(). + err = base_events._SendfileNotAvailable( + "os.sendfile call failed") + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + fut.set_exception(err) + else: + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + fut.set_exception(exc) + except Exception as exc: + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + fut.set_exception(exc) + else: + if sent == 0: + # EOF + self._sock_sendfile_update_filepos(fileno, offset, total_sent) + fut.set_result(total_sent) + else: + offset += sent + total_sent += sent + if registered_fd is None: + self._sock_add_cancellation_callback(fut, sock) + self.add_writer(fd, self._sock_sendfile_native_impl, fut, + fd, sock, fileno, + offset, count, blocksize, total_sent) + + def _sock_sendfile_update_filepos(self, fileno, offset, total_sent): + if total_sent > 0: + os.lseek(fileno, offset, os.SEEK_SET) + + def _sock_add_cancellation_callback(self, fut, sock): + def cb(fut): + if fut.cancelled(): + fd = sock.fileno() + if fd != -1: + self.remove_writer(fd) + fut.add_done_callback(cb) + class _UnixReadPipeTransport(transports.ReadTransport): |