Skip to content

Commit

Permalink
Simplify sendfile logic
Browse files Browse the repository at this point in the history
A safe and reliable check for whether a file descriptor supports mmap
is to directly check if it is seekable. However, some seekable file
descriptors may also report a zero size when calling fstat. If there
is no content length specified for the response and it cannot be
determined from the file descriptor then it is not possible to know
what chunk size to send to the client. In this case, is it necessary
to fall back to unwinding the body by iteration.

The above conditions together reveal a straightforward and reliable
way to check for sendfile support. This patch modifies the Response
class to assert these conditions using a try/catch block as part of
a new, simplified sendfile method. This method returns False if it
is not possible to serve the response using sendfile. Otherwise, it
serves the response and returns True. By returning False when SSL is
in use, the code is made even simpler by removing the special support
for SSL, which is served well enough by the iteration protocol.

Fix #1038
  • Loading branch information
tilgovi committed Nov 11, 2015
1 parent 9158ab2 commit 18d2b92
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 73 deletions.
95 changes: 36 additions & 59 deletions gunicorn/http/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from gunicorn._compat import unquote_to_wsgi_str
from gunicorn.six import string_types, binary_type, reraise
from gunicorn import SERVER_SOFTWARE
import gunicorn.six as six
import gunicorn.util as util

try:
Expand All @@ -24,6 +23,10 @@
except ImportError:
sendfile = None

# Send files in at most 1GB blocks as some operating systems can have problems
# with sending files in blocks over 2GB.
BLKSIZE = 0x3FFFFFFF

NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -344,77 +347,51 @@ def write(self, arg):
util.write(self.sock, arg, self.chunked)

def can_sendfile(self):
return (self.cfg.sendfile and (sendfile is not None))

def sendfile_all(self, fileno, sockno, offset, nbytes):
# Send file in at most 1GB blocks as some operating
# systems can have problems with sending files in blocks
# over 2GB.

BLKSIZE = 0x3FFFFFFF

if nbytes > BLKSIZE:
for m in range(0, nbytes, BLKSIZE):
self.sendfile_all(fileno, sockno, offset, min(nbytes, BLKSIZE))
offset += BLKSIZE
nbytes -= BLKSIZE
else:
sent = 0
sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
while sent != nbytes:
sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
return self.cfg.sendfile and sendfile is not None

def sendfile_use_send(self, fileno, fo_offset, nbytes):

# send file in blocks of 8182 bytes
BLKSIZE = 8192

sent = 0
while sent != nbytes:
data = os.read(fileno, BLKSIZE)
if not data:
break
def sendfile(self, respiter):
if self.cfg.is_ssl or not self.can_sendfile():
return False

sent += len(data)
if sent > nbytes:
data = data[:nbytes - sent]
try:
fileno = respiter.filelike.fileno()
offset = os.lseek(fileno, 0, os.SEEK_CUR)
if self.response_length is None:
filesize = os.fstat(fileno).st_size

util.write(self.sock, data, self.chunked)
# The file may be special and sendfile will fail.
# It may also be zero-length, but that is okay.
if filesize == 0:
return False

def write_file(self, respiter):
if self.can_sendfile() and util.is_fileobject(respiter.filelike):
# sometimes the fileno isn't a callable
if six.callable(respiter.filelike.fileno):
fileno = respiter.filelike.fileno()
nbytes = filesize - offset
else:
fileno = respiter.filelike.fileno
nbytes = self.response_length
except (OSError, io.UnsupportedOperation):
return False

fd_offset = os.lseek(fileno, 0, os.SEEK_CUR)
fo_offset = respiter.filelike.tell()
nbytes = max(os.fstat(fileno).st_size - fo_offset, 0)
self.send_headers()

if self.response_length:
nbytes = min(nbytes, self.response_length)
if self.is_chunked():
chunk_size = "%X\r\n" % nbytes
self.sock.sendall(chunk_size.encode('utf-8'))

if nbytes == 0:
return
sockno = self.sock.fileno()
sent = 0

self.send_headers()
for m in range(0, nbytes, BLKSIZE):
count = min(nbytes - sent, BLKSIZE)
sent += sendfile(sockno, fileno, offset + sent, count)

if self.cfg.is_ssl:
self.sendfile_use_send(fileno, fo_offset, nbytes)
else:
if self.is_chunked():
chunk_size = "%X\r\n" % nbytes
self.sock.sendall(chunk_size.encode('utf-8'))
if self.is_chunked():
self.sock.sendall(b"\r\n")

self.sendfile_all(fileno, self.sock.fileno(), fo_offset, nbytes)
os.lseek(fileno, offset, os.SEEK_SET)

if self.is_chunked():
self.sock.sendall(b"\r\n")
return True

os.lseek(fileno, fd_offset, os.SEEK_SET)
else:
def write_file(self, respiter):
if not self.sendfile(respiter):
for item in respiter:
self.write(item)

Expand Down
14 changes: 0 additions & 14 deletions gunicorn/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import email.utils
import fcntl
import io
import os
import pkg_resources
import random
Expand Down Expand Up @@ -517,19 +516,6 @@ def to_latin1(value):
return value.encode("latin-1")


def is_fileobject(obj):
if not hasattr(obj, "tell") or not hasattr(obj, "fileno"):
return False

# check BytesIO case and maybe others
try:
obj.fileno()
except (IOError, io.UnsupportedOperation):
return False

return True


def warn(msg):
print("!!!", file=sys.stderr)

Expand Down

0 comments on commit 18d2b92

Please sign in to comment.