Merge "Treat 404s as 204 on object delete in proxy"

This commit is contained in:
Jenkins
2014-09-16 14:29:35 +00:00
committed by Gerrit Code Review
5 changed files with 112 additions and 24 deletions

View File

@@ -28,6 +28,7 @@ import os
import time import time
import functools import functools
import inspect import inspect
import operator
from sys import exc_info from sys import exc_info
from swift import gettext_ as _ from swift import gettext_ as _
from urllib import quote from urllib import quote
@@ -1039,7 +1040,7 @@ class Controller(object):
{'method': method, 'path': path}) {'method': method, 'path': path})
def make_requests(self, req, ring, part, method, path, headers, def make_requests(self, req, ring, part, method, path, headers,
query_string=''): query_string='', overrides=None):
""" """
Sends an HTTP request to multiple nodes and aggregates the results. Sends an HTTP request to multiple nodes and aggregates the results.
It attempts the primary nodes concurrently, then iterates over the It attempts the primary nodes concurrently, then iterates over the
@@ -1054,6 +1055,8 @@ class Controller(object):
:param headers: a list of dicts, where each dict represents one :param headers: a list of dicts, where each dict represents one
backend request that should be made. backend request that should be made.
:param query_string: optional query string to send to the backend :param query_string: optional query string to send to the backend
:param overrides: optional return status override map used to override
the returned status of a request.
:returns: a swob.Response object :returns: a swob.Response object
""" """
start_nodes = ring.get_part_nodes(part) start_nodes = ring.get_part_nodes(part)
@@ -1083,7 +1086,7 @@ class Controller(object):
statuses, reasons, resp_headers, bodies = zip(*response) statuses, reasons, resp_headers, bodies = zip(*response)
return self.best_response(req, statuses, reasons, bodies, return self.best_response(req, statuses, reasons, bodies,
'%s %s' % (self.server_type, req.method), '%s %s' % (self.server_type, req.method),
headers=resp_headers) overrides=overrides, headers=resp_headers)
def have_quorum(self, statuses, node_count): def have_quorum(self, statuses, node_count):
""" """
@@ -1103,7 +1106,7 @@ class Controller(object):
return False return False
def best_response(self, req, statuses, reasons, bodies, server_type, def best_response(self, req, statuses, reasons, bodies, server_type,
etag=None, headers=None): etag=None, headers=None, overrides=None):
""" """
Given a list of responses from several servers, choose the best to Given a list of responses from several servers, choose the best to
return to the API. return to the API.
@@ -1117,14 +1120,49 @@ class Controller(object):
:param headers: headers of each response :param headers: headers of each response
:returns: swob.Response object with the correct status, body, etc. set :returns: swob.Response object with the correct status, body, etc. set
""" """
resp = self._compute_quorum_response(
req, statuses, reasons, bodies, etag, headers)
if overrides and not resp:
faked_up_status_indices = set()
transformed = []
for (i, (status, reason, hdrs, body)) in enumerate(zip(
statuses, reasons, headers, bodies)):
if status in overrides:
faked_up_status_indices.add(i)
transformed.append((overrides[status], '', '', ''))
else:
transformed.append((status, reason, hdrs, body))
statuses, reasons, headers, bodies = zip(*transformed)
resp = self._compute_quorum_response(
req, statuses, reasons, bodies, etag, headers,
indices_to_avoid=faked_up_status_indices)
if not resp:
resp = Response(request=req) resp = Response(request=req)
if len(statuses): self.app.logger.error(_('%(type)s returning 503 for %(statuses)s'),
{'type': server_type, 'statuses': statuses})
resp.status = '503 Internal Server Error'
return resp
def _compute_quorum_response(self, req, statuses, reasons, bodies, etag,
headers, indices_to_avoid=()):
if not statuses:
return None
for hundred in (HTTP_OK, HTTP_MULTIPLE_CHOICES, HTTP_BAD_REQUEST): for hundred in (HTTP_OK, HTTP_MULTIPLE_CHOICES, HTTP_BAD_REQUEST):
hstatuses = \ hstatuses = \
[s for s in statuses if hundred <= s < hundred + 100] [(i, s) for i, s in enumerate(statuses)
if hundred <= s < hundred + 100]
if len(hstatuses) >= quorum_size(len(statuses)): if len(hstatuses) >= quorum_size(len(statuses)):
status = max(hstatuses) resp = Response(request=req)
status_index = statuses.index(status) try:
status_index, status = max(
((i, stat) for i, stat in hstatuses
if i not in indices_to_avoid),
key=operator.itemgetter(1))
except ValueError:
# All statuses were indices to avoid
continue
resp.status = '%s %s' % (status, reasons[status_index]) resp.status = '%s %s' % (status, reasons[status_index])
resp.body = bodies[status_index] resp.body = bodies[status_index]
if headers: if headers:
@@ -1132,10 +1170,7 @@ class Controller(object):
if etag: if etag:
resp.headers['etag'] = etag.strip('"') resp.headers['etag'] = etag.strip('"')
return resp return resp
self.app.logger.error(_('%(type)s returning 503 for %(statuses)s'), return None
{'type': server_type, 'statuses': statuses})
resp.status = '503 Internal Server Error'
return resp
@public @public
def GET(self, req): def GET(self, req):

View File

@@ -867,9 +867,11 @@ class ObjectController(Controller):
headers = self._backend_requests( headers = self._backend_requests(
req, len(nodes), container_partition, containers) req, len(nodes), container_partition, containers)
# When deleting objects treat a 404 status as 204.
status_overrides = {404: 204}
resp = self.make_requests(req, obj_ring, resp = self.make_requests(req, obj_ring,
partition, 'DELETE', req.swift_entity_path, partition, 'DELETE', req.swift_entity_path,
headers) headers, overrides=status_overrides)
return resp return resp
@public @public

View File

@@ -550,6 +550,32 @@ class TestFuncs(unittest.TestCase):
self.assertEqual(base.have_quorum([404, 404], 2), True) self.assertEqual(base.have_quorum([404, 404], 2), True)
self.assertEqual(base.have_quorum([201, 404, 201, 201], 4), True) self.assertEqual(base.have_quorum([201, 404, 201, 201], 4), True)
def test_best_response_overrides(self):
base = Controller(self.app)
responses = [
(302, 'Found', '', 'The resource has moved temporarily.'),
(100, 'Continue', '', ''),
(404, 'Not Found', '', 'Custom body'),
]
server_type = "Base DELETE"
req = Request.blank('/v1/a/c/o', method='DELETE')
statuses, reasons, headers, bodies = zip(*responses)
# First test that you can't make a quorum with only overridden
# responses
overrides = {302: 204, 100: 204}
resp = base.best_response(req, statuses, reasons, bodies, server_type,
headers=headers, overrides=overrides)
self.assertEqual(resp.status, '503 Internal Server Error')
# next make a 404 quorum and make sure the last delete (real) 404
# status is the one returned.
overrides = {100: 404}
resp = base.best_response(req, statuses, reasons, bodies, server_type,
headers=headers, overrides=overrides)
self.assertEqual(resp.status, '404 Not Found')
self.assertEqual(resp.body, 'Custom body')
def test_range_fast_forward(self): def test_range_fast_forward(self):
req = Request.blank('/') req = Request.blank('/')
handler = GetOrHeadHandler(None, req, None, None, None, None, {}) handler = GetOrHeadHandler(None, req, None, None, None, None, {})

View File

@@ -226,6 +226,31 @@ class TestObjController(unittest.TestCase):
resp = req.get_response(self.app) resp = req.get_response(self.app)
self.assertEquals(resp.status_int, 204) self.assertEquals(resp.status_int, 204)
def test_DELETE_half_not_found_statuses(self):
self.obj_ring.set_replicas(4)
req = swift.common.swob.Request.blank('/v1/a/c/o', method='DELETE')
with set_http_connect(404, 204, 404, 204):
resp = req.get_response(self.app)
self.assertEquals(resp.status_int, 204)
def test_DELETE_half_not_found_headers_and_body(self):
# Transformed responses have bogus bodies and headers, so make sure we
# send the client headers and body from a real node's response.
self.obj_ring.set_replicas(4)
status_codes = (404, 404, 204, 204)
bodies = ('not found', 'not found', '', '')
headers = [{}, {}, {'Pick-Me': 'yes'}, {'Pick-Me': 'yes'}]
req = swift.common.swob.Request.blank('/v1/a/c/o', method='DELETE')
with set_http_connect(*status_codes, body_iter=bodies,
headers=headers):
resp = req.get_response(self.app)
self.assertEquals(resp.status_int, 204)
self.assertEquals(resp.headers.get('Pick-Me'), 'yes')
self.assertEquals(resp.body, '')
def test_DELETE_not_found(self): def test_DELETE_not_found(self):
req = swift.common.swob.Request.blank('/v1/a/c/o', method='DELETE') req = swift.common.swob.Request.blank('/v1/a/c/o', method='DELETE')
with set_http_connect(404, 404, 204): with set_http_connect(404, 404, 204):

View File

@@ -1899,9 +1899,9 @@ class TestObjectController(unittest.TestCase):
test_status_map((200, 200, 204, 204, 204), 204) test_status_map((200, 200, 204, 204, 204), 204)
test_status_map((200, 200, 204, 204, 500), 204) test_status_map((200, 200, 204, 204, 500), 204)
test_status_map((200, 200, 204, 404, 404), 404) test_status_map((200, 200, 204, 404, 404), 404)
test_status_map((200, 200, 204, 500, 404), 503) test_status_map((200, 204, 500, 500, 404), 503)
test_status_map((200, 200, 404, 404, 404), 404) test_status_map((200, 200, 404, 404, 404), 404)
test_status_map((200, 200, 404, 404, 500), 404) test_status_map((200, 200, 400, 400, 400), 400)
def test_HEAD(self): def test_HEAD(self):
with save_globals(): with save_globals():