Remove double GET on range requests.
The proxy server's ObjectController was performing multiple GET requests to the object server when processing requests with Range headers. This was a workaround for a bug in the proxy server's Controller.GETorHEAD_base method where a response code of 416 from the object server was incorrectly treated as a bad response from the backend, the same way a 404 or a 5xx would be. A 416 (Requested Range Not Satisfiable) from the object server is now considered a good response. Since the response headers from the object server already include the X-Object-Manifest header, there's no need to make a second request (sans Range header) to see if the object is a manifest. Bonus fix: updated message for status 416 to match RFC2616. Bonus fix 2: removed a leftover debugging print() in test/unit/proxy/test_server.py. Fixes bug 1065869. Change-Id: I156af0b463f02ef19a8cfe37092544a599d89b78
This commit is contained in:
parent
cee685e11a
commit
9cb7751fad
@ -70,7 +70,7 @@ RESPONSE_REASONS = {
|
|||||||
'server.'),
|
'server.'),
|
||||||
415: ('Unsupported Media Type', 'The request media type is not '
|
415: ('Unsupported Media Type', 'The request media type is not '
|
||||||
'supported by this server.'),
|
'supported by this server.'),
|
||||||
416: ('Request Range Not Satisfiable', 'The Range requested is not '
|
416: ('Requested Range Not Satisfiable', 'The Range requested is not '
|
||||||
'available.'),
|
'available.'),
|
||||||
417: ('Expectation Failed', 'Expectation failed.'),
|
417: ('Expectation Failed', 'Expectation failed.'),
|
||||||
422: ('Unprocessable Entity', 'Unable to process the contained '
|
422: ('Unprocessable Entity', 'Unable to process the contained '
|
||||||
@ -271,8 +271,6 @@ def _resp_status_property():
|
|||||||
When set to a str, it splits status_int and title apart.
|
When set to a str, it splits status_int and title apart.
|
||||||
When set to an integer, retrieves the correct title for that
|
When set to an integer, retrieves the correct title for that
|
||||||
response code from the RESPONSE_REASONS dict.
|
response code from the RESPONSE_REASONS dict.
|
||||||
|
|
||||||
:param header: name of the header, e.g. "Content-Length"
|
|
||||||
"""
|
"""
|
||||||
def getter(self):
|
def getter(self):
|
||||||
return '%s %s' % (self.status_int, self.title)
|
return '%s %s' % (self.status_int, self.title)
|
||||||
@ -383,8 +381,8 @@ def _resp_charset_property():
|
|||||||
def _resp_app_iter_property():
|
def _resp_app_iter_property():
|
||||||
"""
|
"""
|
||||||
Set and retrieve Response.app_iter
|
Set and retrieve Response.app_iter
|
||||||
Mostly a pass-through to Response._app_iter, it's a property so it can zero
|
Mostly a pass-through to Response._app_iter; it's a property so it can zero
|
||||||
out an exsisting content-length on assignment.
|
out an existing content-length on assignment.
|
||||||
"""
|
"""
|
||||||
def getter(self):
|
def getter(self):
|
||||||
return self._app_iter
|
return self._app_iter
|
||||||
|
@ -566,6 +566,13 @@ class Controller(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def is_good_source(self, src):
|
||||||
|
"""
|
||||||
|
Indicates whether or not the request made to the backend found
|
||||||
|
what it was looking for.
|
||||||
|
"""
|
||||||
|
return is_success(src.status) or is_redirection(src.status)
|
||||||
|
|
||||||
def GETorHEAD_base(self, req, server_type, partition, nodes, path,
|
def GETorHEAD_base(self, req, server_type, partition, nodes, path,
|
||||||
attempts):
|
attempts):
|
||||||
"""
|
"""
|
||||||
@ -609,8 +616,7 @@ class Controller(object):
|
|||||||
node, server_type, _('Trying to %(method)s %(path)s') %
|
node, server_type, _('Trying to %(method)s %(path)s') %
|
||||||
{'method': req.method, 'path': req.path})
|
{'method': req.method, 'path': req.path})
|
||||||
continue
|
continue
|
||||||
if is_success(possible_source.status) or \
|
if self.is_good_source(possible_source):
|
||||||
is_redirection(possible_source.status):
|
|
||||||
# 404 if we know we don't have a synced copy
|
# 404 if we know we don't have a synced copy
|
||||||
if not float(possible_source.getheader('X-PUT-Timestamp', 1)):
|
if not float(possible_source.getheader('X-PUT-Timestamp', 1)):
|
||||||
statuses.append(HTTP_NOT_FOUND)
|
statuses.append(HTTP_NOT_FOUND)
|
||||||
|
@ -276,6 +276,17 @@ class ObjectController(Controller):
|
|||||||
for obj in sublisting:
|
for obj in sublisting:
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
def is_good_source(self, src):
|
||||||
|
"""
|
||||||
|
Indicates whether or not the request made to the backend found
|
||||||
|
what it was looking for.
|
||||||
|
|
||||||
|
In the case of an object, a 416 indicates that we found a
|
||||||
|
backend with the object.
|
||||||
|
"""
|
||||||
|
return src.status == 416 or \
|
||||||
|
super(ObjectController, self).is_good_source(src)
|
||||||
|
|
||||||
def GETorHEAD(self, req):
|
def GETorHEAD(self, req):
|
||||||
"""Handle HTTP GET or HEAD requests."""
|
"""Handle HTTP GET or HEAD requests."""
|
||||||
container_info = self.container_info(self.account_name,
|
container_info = self.container_info(self.account_name,
|
||||||
@ -285,28 +296,13 @@ class ObjectController(Controller):
|
|||||||
aresp = req.environ['swift.authorize'](req)
|
aresp = req.environ['swift.authorize'](req)
|
||||||
if aresp:
|
if aresp:
|
||||||
return aresp
|
return aresp
|
||||||
|
|
||||||
partition, nodes = self.app.object_ring.get_nodes(
|
partition, nodes = self.app.object_ring.get_nodes(
|
||||||
self.account_name, self.container_name, self.object_name)
|
self.account_name, self.container_name, self.object_name)
|
||||||
shuffle(nodes)
|
shuffle(nodes)
|
||||||
resp = self.GETorHEAD_base(req, _('Object'), partition,
|
resp = self.GETorHEAD_base(req, _('Object'), partition,
|
||||||
self.iter_nodes(partition, nodes, self.app.object_ring),
|
self.iter_nodes(partition, nodes, self.app.object_ring),
|
||||||
req.path_info, len(nodes))
|
req.path_info, len(nodes))
|
||||||
# Whether we get a 416 Requested Range Not Satisfiable or not,
|
|
||||||
# we should request a manifest because size of manifest file
|
|
||||||
# can be not 0. After checking a manifest, redo the range request
|
|
||||||
# on the whole object.
|
|
||||||
if req.range:
|
|
||||||
req_range = req.range
|
|
||||||
req.range = None
|
|
||||||
resp2 = self.GETorHEAD_base(req, _('Object'), partition,
|
|
||||||
self.iter_nodes(partition,
|
|
||||||
nodes,
|
|
||||||
self.app.object_ring),
|
|
||||||
req.path_info, len(nodes))
|
|
||||||
if 'x-object-manifest' not in resp2.headers:
|
|
||||||
return resp
|
|
||||||
resp = resp2
|
|
||||||
req.range = str(req_range)
|
|
||||||
|
|
||||||
if 'x-object-manifest' in resp.headers:
|
if 'x-object-manifest' in resp.headers:
|
||||||
lcontainer, lprefix = \
|
lcontainer, lprefix = \
|
||||||
@ -367,6 +363,10 @@ class ObjectController(Controller):
|
|||||||
resp.last_modified = last_modified
|
resp.last_modified = last_modified
|
||||||
resp.etag = etag
|
resp.etag = etag
|
||||||
resp.headers['accept-ranges'] = 'bytes'
|
resp.headers['accept-ranges'] = 'bytes'
|
||||||
|
# In case of a manifest file of nonzero length, the
|
||||||
|
# backend may have sent back a Content-Range header for
|
||||||
|
# the manifest. It's wrong for the client, though.
|
||||||
|
resp.content_range = None
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -419,14 +419,14 @@ class TestResponse(unittest.TestCase):
|
|||||||
body = ''.join(resp([], start_response))
|
body = ''.join(resp([], start_response))
|
||||||
self.assertEquals(body, '')
|
self.assertEquals(body, '')
|
||||||
self.assertEquals(resp.content_length, 0)
|
self.assertEquals(resp.content_length, 0)
|
||||||
self.assertEquals(resp.status, '416 Request Range Not Satisfiable')
|
self.assertEquals(resp.status, '416 Requested Range Not Satisfiable')
|
||||||
|
|
||||||
resp = swift.common.swob.Response(
|
resp = swift.common.swob.Response(
|
||||||
body='1234567890', request=req,
|
body='1234567890', request=req,
|
||||||
conditional_response=True)
|
conditional_response=True)
|
||||||
body = ''.join(resp([], start_response))
|
body = ''.join(resp([], start_response))
|
||||||
self.assertEquals(body, '')
|
self.assertEquals(body, '')
|
||||||
self.assertEquals(resp.status, '416 Request Range Not Satisfiable')
|
self.assertEquals(resp.status, '416 Requested Range Not Satisfiable')
|
||||||
|
|
||||||
# Syntactically-invalid Range headers "MUST" be ignored
|
# Syntactically-invalid Range headers "MUST" be ignored
|
||||||
req = swift.common.swob.Request.blank(
|
req = swift.common.swob.Request.blank(
|
||||||
|
@ -1495,7 +1495,6 @@ class TestObjectController(unittest.TestCase):
|
|||||||
'container', 'object')
|
'container', 'object')
|
||||||
self.assert_status_map(controller.HEAD, (200, 200, 503, 200, 200),
|
self.assert_status_map(controller.HEAD, (200, 200, 503, 200, 200),
|
||||||
200)
|
200)
|
||||||
print controller.app.object_ring.devs
|
|
||||||
self.assertEquals(controller.app.object_ring.devs[0]['errors'], 2)
|
self.assertEquals(controller.app.object_ring.devs[0]['errors'], 2)
|
||||||
self.assert_('last_error' in controller.app.object_ring.devs[0])
|
self.assert_('last_error' in controller.app.object_ring.devs[0])
|
||||||
for _junk in xrange(self.app.error_suppression_limit):
|
for _junk in xrange(self.app.error_suppression_limit):
|
||||||
|
Loading…
Reference in New Issue
Block a user