Improve FakeSwift Backend-Ignore-Range support
In keeping with the trend as of late, this change makes FakeSwift behave more reliably like a real Swift backend. Swift backend object server's grew support for ignoring Range request headers when fetching SLO manifests in Jan-2020, and FakeSwift learned how to mimic the real behavior in Jul-2022. This change unifies the implementation details with a request_helper and consolidates the behavior in FakeSwift. It also makes the modern object-server behavior the default. Between 2020 and 2022 there was arguably some utility defaulting to legacy behavior, but in 2023 as we endeavor to refactor the SLO implementation and extend it's tests: a reliable FakeSwift is paramount. Since most of the existing tests for SLO's behavior responding to Range requests did not reliably assert behavior across new and old swift this change selects the most relevant tests to legacy behavior and has them opt-in to can_ignore_range = False, while the others merely have their backend request asserts cleaned-up to match the backend request pattern you would expect in a production environment that's upgraded in the last 3 years. Additional technical investment may be required to ensure older clusters can upgrade proxies before object servers w/o tracebacks until the upgrade finishes; however it appears the existing code is sufficiently robust despite the lack of explicit multi-inheritance testing like was done for the legacy manifest format change in Nov-2016 (N.B. unlike rolling upgrade bugs, data is forever). Related-Change-Id: I4ff2a178d0456e7e37d561109ef57dd0d92cbd4e Related-Change-Id: If3861e5b9c4f17ab3b82ea16673ddb29d07820a1 Related-Change-Id: Ia6ad32354105515560b005cea750aa64a88c96f9 Change-Id: I7ebfd557b9c8ec25498c628fcf0695cd52ad78d6
This commit is contained in:
parent
719dec6925
commit
451ae26a8b
|
@ -900,6 +900,24 @@ def update_ignore_range_header(req, name):
|
|||
req.headers[hdr] = csv_append(req.headers.get(hdr), name)
|
||||
|
||||
|
||||
def resolve_ignore_range_header(req, metadata):
|
||||
"""
|
||||
Helper function to remove Range header from request if metadata matching
|
||||
the X-Backend-Ignore-Range-If-Metadata-Present header is found.
|
||||
|
||||
:param req: a swob Request
|
||||
:param metadata: dictionary of object metadata
|
||||
"""
|
||||
ignore_range_headers = set(
|
||||
h.strip().lower()
|
||||
for h in req.headers.get(
|
||||
'X-Backend-Ignore-Range-If-Metadata-Present',
|
||||
'').split(','))
|
||||
if ignore_range_headers.intersection(
|
||||
h.lower() for h in metadata):
|
||||
req.headers.pop('Range', None)
|
||||
|
||||
|
||||
def is_use_replication_network(headers=None):
|
||||
"""
|
||||
Determine if replication network should be used.
|
||||
|
|
|
@ -42,7 +42,7 @@ from swift.common.exceptions import ConnectionTimeout, DiskFileQuarantined, \
|
|||
DiskFileNotExist, DiskFileCollision, DiskFileNoSpace, DiskFileDeleted, \
|
||||
DiskFileDeviceUnavailable, DiskFileExpired, ChunkReadTimeout, \
|
||||
ChunkReadError, DiskFileXattrNotSupported
|
||||
from swift.common.request_helpers import \
|
||||
from swift.common.request_helpers import resolve_ignore_range_header, \
|
||||
OBJECT_SYSMETA_CONTAINER_UPDATE_OVERRIDE_PREFIX
|
||||
from swift.obj import ssync_receiver
|
||||
from swift.common.http import is_success, HTTP_MOVED_PERMANENTLY
|
||||
|
@ -1090,14 +1090,7 @@ class ObjectController(BaseStorageServer):
|
|||
try:
|
||||
with disk_file.open(current_time=req_timestamp):
|
||||
metadata = disk_file.get_metadata()
|
||||
ignore_range_headers = set(
|
||||
h.strip().lower()
|
||||
for h in request.headers.get(
|
||||
'X-Backend-Ignore-Range-If-Metadata-Present',
|
||||
'').split(','))
|
||||
if ignore_range_headers.intersection(
|
||||
h.lower() for h in metadata):
|
||||
request.headers.pop('Range', None)
|
||||
resolve_ignore_range_header(request, metadata)
|
||||
obj_size = int(metadata['Content-Length'])
|
||||
file_x_ts = Timestamp(metadata['X-Timestamp'])
|
||||
keep_cache = (
|
||||
|
|
|
@ -20,7 +20,8 @@ from six.moves.urllib import parse
|
|||
from swift.common import swob
|
||||
from swift.common.header_key_dict import HeaderKeyDict
|
||||
from swift.common.request_helpers import is_user_meta, \
|
||||
is_object_transient_sysmeta, resolve_etag_is_at_header
|
||||
is_object_transient_sysmeta, resolve_etag_is_at_header, \
|
||||
resolve_ignore_range_header
|
||||
from swift.common.swob import HTTPNotImplemented
|
||||
from swift.common.utils import split_path, md5
|
||||
|
||||
|
@ -140,10 +141,11 @@ class FakeSwift(object):
|
|||
self.auto_create_account_prefix = '.'
|
||||
self.backend_user_agent = "fake_swift"
|
||||
self._pipeline_final_app = self
|
||||
# some tests want to opt in to mimicking the
|
||||
# X-Backend-Ignore-Range-If-Metadata-Present header behavior,
|
||||
# but default to old-swift behavior
|
||||
self.can_ignore_range = False
|
||||
# Object Servers learned to resolve_ignore_range_header in Jan-2020,
|
||||
# and although we still maintain some middleware tests that assert
|
||||
# proper behavior across rolling upgrades, having a FakeSwift not act
|
||||
# like modern swift is now opt-in.
|
||||
self.can_ignore_range = True
|
||||
|
||||
def _find_response(self, method, path):
|
||||
path = normalize_path(path)
|
||||
|
@ -192,9 +194,6 @@ class FakeSwift(object):
|
|||
return resp_class, HeaderKeyDict(headers), body
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
if self.can_ignore_range:
|
||||
# we might pop off the Range header
|
||||
env = dict(env)
|
||||
method = env['REQUEST_METHOD']
|
||||
if method not in self.ALLOWED_METHODS:
|
||||
raise HTTPNotImplemented()
|
||||
|
@ -217,12 +216,6 @@ class FakeSwift(object):
|
|||
|
||||
resp_class, headers, body = self._select_response(env, method, path)
|
||||
|
||||
ignore_range_meta = req.headers.get(
|
||||
'x-backend-ignore-range-if-metadata-present')
|
||||
if self.can_ignore_range and ignore_range_meta and set(
|
||||
ignore_range_meta.split(',')).intersection(headers.keys()):
|
||||
req.headers.pop('range', None)
|
||||
|
||||
# Update req.headers before capturing the request
|
||||
if method in ('GET', 'HEAD') and obj:
|
||||
req.headers['X-Backend-Storage-Policy-Index'] = headers.get(
|
||||
|
@ -279,6 +272,11 @@ class FakeSwift(object):
|
|||
# Apply conditional etag overrides
|
||||
conditional_etag = resolve_etag_is_at_header(req, headers)
|
||||
|
||||
if self.can_ignore_range:
|
||||
# avoid popping range from original environ
|
||||
req = swob.Request(dict(req.environ))
|
||||
resolve_ignore_range_header(req, headers)
|
||||
|
||||
# range requests ought to work, hence conditional_response=True
|
||||
if isinstance(body, list):
|
||||
resp = resp_class(
|
||||
|
@ -290,7 +288,7 @@ class FakeSwift(object):
|
|||
req=req, headers=headers, body=body,
|
||||
conditional_response=req.method in ('GET', 'HEAD'),
|
||||
conditional_etag=conditional_etag)
|
||||
wsgi_iter = resp(env, start_response)
|
||||
wsgi_iter = resp(req.environ, start_response)
|
||||
self.mark_opened((method, path))
|
||||
return LeakTrackingIter(wsgi_iter, self.mark_closed,
|
||||
self.mark_read, (method, path))
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import unittest
|
||||
|
||||
from swift.common.swob import Request, HTTPOk, HTTPNotFound, HTTPCreated
|
||||
from swift.common import request_helpers as rh
|
||||
from test.unit.common.middleware.helpers import FakeSwift
|
||||
|
||||
|
||||
|
@ -448,3 +449,62 @@ class TestFakeSwift(unittest.TestCase):
|
|||
self.assertEqual(b'not stuff', resp.body)
|
||||
self.assertEqual(2, swift.call_count)
|
||||
self.assertEqual(('GET', '/v1/a/c/o'), swift.calls[-1])
|
||||
|
||||
def test_range(self):
|
||||
swift = FakeSwift()
|
||||
swift.register('GET', '/v1/a/c/o', HTTPOk, {}, b'stuff')
|
||||
req = Request.blank('/v1/a/c/o', headers={'Range': 'bytes=0-2'})
|
||||
resp = req.get_response(swift)
|
||||
self.assertEqual(206, resp.status_int)
|
||||
self.assertEqual(b'stu', resp.body)
|
||||
self.assertEqual('bytes 0-2/5', resp.headers['Content-Range'])
|
||||
self.assertEqual('bytes=0-2', req.headers.get('Range'))
|
||||
self.assertEqual('bytes=0-2',
|
||||
swift.calls_with_headers[-1].headers.get('Range'))
|
||||
|
||||
def test_range_ignore_range_header(self):
|
||||
swift = FakeSwift()
|
||||
swift.register('GET', '/v1/a/c/o', HTTPOk, {
|
||||
# the value of the matching header doesn't matter
|
||||
'X-Object-Sysmeta-Magic': 'False'
|
||||
}, b'stuff')
|
||||
req = Request.blank('/v1/a/c/o', headers={'Range': 'bytes=0-2'})
|
||||
rh.update_ignore_range_header(req, 'X-Object-Sysmeta-Magic')
|
||||
resp = req.get_response(swift)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertEqual(b'stuff', resp.body)
|
||||
self.assertNotIn('Content-Range', resp.headers)
|
||||
self.assertEqual('bytes=0-2', req.headers.get('Range'))
|
||||
self.assertEqual('bytes=0-2',
|
||||
swift.calls_with_headers[-1].headers.get('Range'))
|
||||
|
||||
def test_range_ignore_range_header_old_swift(self):
|
||||
swift = FakeSwift()
|
||||
swift.can_ignore_range = False
|
||||
swift.register('GET', '/v1/a/c/o', HTTPOk, {
|
||||
# the value of the matching header doesn't matter
|
||||
'X-Object-Sysmeta-Magic': 'False'
|
||||
}, b'stuff')
|
||||
req = Request.blank('/v1/a/c/o', headers={'Range': 'bytes=0-2'})
|
||||
rh.update_ignore_range_header(req, 'X-Object-Sysmeta-Magic')
|
||||
resp = req.get_response(swift)
|
||||
self.assertEqual(206, resp.status_int)
|
||||
self.assertEqual(b'stu', resp.body)
|
||||
self.assertEqual('bytes 0-2/5', resp.headers['Content-Range'])
|
||||
self.assertEqual('bytes=0-2', req.headers.get('Range'))
|
||||
self.assertEqual('bytes=0-2',
|
||||
swift.calls_with_headers[-1].headers.get('Range'))
|
||||
|
||||
def test_range_ignore_range_header_ignored(self):
|
||||
swift = FakeSwift()
|
||||
# range is only ignored if registered response has matching metadata
|
||||
swift.register('GET', '/v1/a/c/o', HTTPOk, {}, b'stuff')
|
||||
req = Request.blank('/v1/a/c/o', headers={'Range': 'bytes=0-2'})
|
||||
rh.update_ignore_range_header(req, 'X-Object-Sysmeta-Magic')
|
||||
resp = req.get_response(swift)
|
||||
self.assertEqual(206, resp.status_int)
|
||||
self.assertEqual(b'stu', resp.body)
|
||||
self.assertEqual('bytes 0-2/5', resp.headers['Content-Range'])
|
||||
self.assertEqual('bytes=0-2', req.headers.get('Range'))
|
||||
self.assertEqual('bytes=0-2',
|
||||
swift.calls_with_headers[-1].headers.get('Range'))
|
||||
|
|
|
@ -1319,7 +1319,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
self.assertEqual(resp_data['Errors'], [])
|
||||
|
||||
def test_handle_multipart_delete_segment_404(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/man?multipart-manifest=delete',
|
||||
environ={'REQUEST_METHOD': 'DELETE',
|
||||
|
@ -1338,7 +1337,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
self.assertEqual(resp_data['Number Not Found'], 1)
|
||||
|
||||
def test_handle_multipart_delete_whole(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/man-all-there?multipart-manifest=delete',
|
||||
environ={'REQUEST_METHOD': 'DELETE'})
|
||||
|
@ -1383,7 +1381,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
('DELETE', ('/v1/AUTH_test/deltest/man-all-there'))]))
|
||||
|
||||
def test_handle_multipart_delete_non_ascii(self):
|
||||
self.app.can_ignore_range = True
|
||||
unicode_acct = u'AUTH_test-un\u00efcode'
|
||||
wsgi_acct = bytes_to_wsgi(unicode_acct.encode('utf-8'))
|
||||
req = Request.blank(
|
||||
|
@ -1411,7 +1408,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
('DELETE', ('/v1/%s/deltest/man-all-there' % wsgi_acct))]))
|
||||
|
||||
def test_handle_multipart_delete_nested(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-with-submanifest?' +
|
||||
'multipart-manifest=delete',
|
||||
|
@ -1431,7 +1427,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
('DELETE', '/v1/AUTH_test/deltest/manifest-with-submanifest')})
|
||||
|
||||
def test_handle_multipart_delete_nested_too_many_segments(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-with-too-many-segs?' +
|
||||
'multipart-manifest=delete',
|
||||
|
@ -1446,7 +1441,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
'Too many buffered slo segments to delete.')
|
||||
|
||||
def test_handle_multipart_delete_nested_404(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-missing-submanifest' +
|
||||
'?multipart-manifest=delete',
|
||||
|
@ -1470,7 +1464,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
self.assertEqual(resp_data['Errors'], [])
|
||||
|
||||
def test_handle_multipart_delete_nested_401(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.app.register(
|
||||
'GET', '/v1/AUTH_test/deltest/submanifest',
|
||||
swob.HTTPUnauthorized, {}, None)
|
||||
|
@ -1488,7 +1481,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
[['/deltest/submanifest', '401 Unauthorized']])
|
||||
|
||||
def test_handle_multipart_delete_nested_500(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.app.register(
|
||||
'GET', '/v1/AUTH_test/deltest/submanifest',
|
||||
swob.HTTPServerError, {}, None)
|
||||
|
@ -1507,7 +1499,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
'Unable to load SLO manifest or segment.']])
|
||||
|
||||
def test_handle_multipart_delete_not_a_manifest(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/a_1?multipart-manifest=delete',
|
||||
environ={'REQUEST_METHOD': 'DELETE',
|
||||
|
@ -1526,7 +1517,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
self.assertFalse(self.app.unread_requests)
|
||||
|
||||
def test_handle_multipart_delete_bad_json(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-badjson?multipart-manifest=delete',
|
||||
environ={'REQUEST_METHOD': 'DELETE',
|
||||
|
@ -1545,7 +1535,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
'Unable to load SLO manifest']])
|
||||
|
||||
def test_handle_multipart_delete_401(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-with-unauth-segment' +
|
||||
'?multipart-manifest=delete',
|
||||
|
@ -1569,7 +1558,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
[['/deltest-unauth/q_17', '401 Unauthorized']])
|
||||
|
||||
def test_handle_multipart_delete_client_content_type(self):
|
||||
self.app.can_ignore_range = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/man-all-there?multipart-manifest=delete',
|
||||
environ={'REQUEST_METHOD': 'DELETE', 'CONTENT_TYPE': 'foo/bar'},
|
||||
|
@ -1601,7 +1589,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
'/v1/AUTH_test/deltest/man_404?multipart-manifest=get')])
|
||||
|
||||
def test_handle_async_delete_turned_off(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = False
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/man-all-there?'
|
||||
|
@ -1622,7 +1609,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
('DELETE', '/v1/AUTH_test/deltest/man-all-there')]))
|
||||
|
||||
def test_handle_async_delete_whole(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = True
|
||||
now = Timestamp(time.time())
|
||||
exp_obj_cont = get_expirer_container(
|
||||
|
@ -1675,7 +1661,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
])
|
||||
|
||||
def test_handle_async_delete_non_ascii(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = True
|
||||
unicode_acct = u'AUTH_test-un\u00efcode'
|
||||
wsgi_acct = bytes_to_wsgi(unicode_acct.encode('utf-8'))
|
||||
|
@ -1750,7 +1735,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
])
|
||||
|
||||
def test_handle_async_delete_non_ascii_same_container(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = True
|
||||
unicode_acct = u'AUTH_test-un\u00efcode'
|
||||
wsgi_acct = bytes_to_wsgi(unicode_acct.encode('utf-8'))
|
||||
|
@ -1821,7 +1805,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
])
|
||||
|
||||
def test_handle_async_delete_nested(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = True
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/deltest/manifest-with-submanifest' +
|
||||
|
@ -1835,7 +1818,6 @@ class TestSloDeleteManifest(SloTestCase):
|
|||
'manifest-with-submanifest?multipart-manifest=get')])
|
||||
|
||||
def test_handle_async_delete_too_many_containers(self):
|
||||
self.app.can_ignore_range = True
|
||||
self.slo.allow_async_delete = True
|
||||
self.app.register(
|
||||
'GET', '/v1/AUTH_test/deltest/man',
|
||||
|
@ -2115,9 +2097,9 @@ class TestSloGetRawManifest(SloTestCase):
|
|||
body)
|
||||
|
||||
|
||||
class TestSloGetManifest(SloTestCase):
|
||||
class TestSloGetOldManifests(SloTestCase):
|
||||
def setUp(self):
|
||||
super(TestSloGetManifest, self).setUp()
|
||||
super(TestSloGetOldManifests, self).setUp()
|
||||
|
||||
# some plain old objects
|
||||
self.app.register(
|
||||
|
@ -2573,13 +2555,13 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '15')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 3-17/50')
|
||||
self.assertEqual(headers['Etag'], '"%s"' % self.manifest_abcd_etag)
|
||||
self.assertEqual(body, b'aabbbbbbbbbbccc')
|
||||
|
||||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
|
@ -2589,7 +2571,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(ranges, [
|
||||
'bytes=3-17',
|
||||
None,
|
||||
None,
|
||||
'bytes=3-',
|
||||
None,
|
||||
'bytes=0-2'])
|
||||
|
@ -2601,7 +2582,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None])
|
||||
# we set swift.source for everything but the first request
|
||||
self.assertIsNone(self.app.swift_sources[0])
|
||||
|
@ -2627,6 +2607,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
boundary = boundary.encode('utf-8')
|
||||
|
||||
self.assertEqual(len(body), int(headers['Content-Length']))
|
||||
# this is a multi-range resp
|
||||
self.assertNotIn('Content-Range', headers)
|
||||
|
||||
got_mime_docs = []
|
||||
for mime_doc_fh in iter_multipart_mime_documents(
|
||||
|
@ -2663,7 +2645,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
|
@ -2673,7 +2654,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
ranges = [c[2].get('Range') for c in self.app.calls_with_headers]
|
||||
self.assertEqual(ranges, [
|
||||
'bytes=3-17,20-24,35-999999', # initial GET
|
||||
None, # re-fetch top-level manifest
|
||||
None, # fetch manifest-bc as sub-slo
|
||||
'bytes=3-', # a_5
|
||||
None, # b_10
|
||||
|
@ -2722,9 +2702,12 @@ class TestSloGetManifest(SloTestCase):
|
|||
'bytes 29-49/50')
|
||||
self.assertEqual(second_range_body, b'cdddddddddddddddddddd')
|
||||
|
||||
def test_range_get_includes_whole_manifest(self):
|
||||
def test_old_swift_range_get_includes_whole_manifest(self):
|
||||
self.app.can_ignore_range = False
|
||||
# If the first range GET results in retrieval of the entire manifest
|
||||
# body (which we can detect by looking at Content-Range), then we
|
||||
# body (and not because of X-Backend-Ignore-Range-If-Metadata-Present,
|
||||
# but because the requested range happened to be sufficient which we
|
||||
# detected by looking at the Content-Range response header), then we
|
||||
# should not go make a second, non-ranged request just to retrieve the
|
||||
# same bytes again.
|
||||
req = Request.blank(
|
||||
|
@ -2747,7 +2730,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/d_20?multipart-manifest=get')])
|
||||
|
||||
def test_range_get_beyond_manifest(self):
|
||||
def test_old_swift_range_get_beyond_manifest(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
self.app.register(
|
||||
|
@ -2784,12 +2768,15 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.app.calls, [
|
||||
# has Range header, gets 416
|
||||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
# retry the first one
|
||||
# old swift can't ignore range request to manifest and we have
|
||||
# to refetch; new swift has exactly the same behavior but w/o
|
||||
# this extra refetch request as lots of other tests demonstrate
|
||||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
('GET',
|
||||
'/v1/AUTH_test/gettest/big_seg?multipart-manifest=get')])
|
||||
|
||||
def test_range_get_beyond_manifest_refetch_fails(self):
|
||||
def test_old_swift_range_get_beyond_manifest_refetch_fails(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
big_manifest = json.dumps(
|
||||
|
@ -2802,6 +2789,9 @@ class TestSloGetManifest(SloTestCase):
|
|||
'X-Backend-Timestamp': '1234',
|
||||
'Etag': md5hex(big_manifest)},
|
||||
big_manifest),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted
|
||||
# this second refetch which fails
|
||||
(swob.HTTPNotFound, {}, None)])
|
||||
|
||||
req = Request.blank(
|
||||
|
@ -2820,7 +2810,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
])
|
||||
|
||||
def test_range_get_beyond_manifest_refetch_finds_old(self):
|
||||
def test_old_swift_range_get_beyond_manifest_refetch_finds_old(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
big_manifest = json.dumps(
|
||||
|
@ -2833,6 +2824,9 @@ class TestSloGetManifest(SloTestCase):
|
|||
'X-Backend-Timestamp': '1234',
|
||||
'Etag': md5hex(big_manifest)},
|
||||
big_manifest),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted
|
||||
# this second refetch which is too old
|
||||
(swob.HTTPOk, {'X-Backend-Timestamp': '1233'}, [b'small body'])])
|
||||
|
||||
req = Request.blank(
|
||||
|
@ -2851,7 +2845,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
])
|
||||
|
||||
def test_range_get_beyond_manifest_refetch_small_non_slo(self):
|
||||
def test_old_swift_range_get_beyond_manifest_refetch_small_non_slo(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
big_manifest = json.dumps(
|
||||
|
@ -2864,6 +2859,9 @@ class TestSloGetManifest(SloTestCase):
|
|||
'X-Backend-Timestamp': '1234',
|
||||
'Etag': md5hex(big_manifest)},
|
||||
big_manifest),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted
|
||||
# this second refetch which isn't an SLO
|
||||
(swob.HTTPOk, {'X-Backend-Timestamp': '1235'}, [b'small body'])])
|
||||
|
||||
req = Request.blank(
|
||||
|
@ -2882,7 +2880,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
])
|
||||
|
||||
def test_range_get_beyond_manifest_refetch_big_non_slo(self):
|
||||
def test_old_swift_range_get_beyond_manifest_refetch_big_non_slo(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
big_manifest = json.dumps(
|
||||
|
@ -2895,6 +2894,9 @@ class TestSloGetManifest(SloTestCase):
|
|||
'X-Backend-Timestamp': '1234',
|
||||
'Etag': md5hex(big_manifest)},
|
||||
big_manifest),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted
|
||||
# this second refetch which isn't an SLO
|
||||
(swob.HTTPOk, {'X-Backend-Timestamp': '1235'},
|
||||
[b'x' * 1024 * 1024])])
|
||||
|
||||
|
@ -2916,7 +2918,8 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
])
|
||||
|
||||
def test_range_get_beyond_manifest_refetch_tombstone(self):
|
||||
def test_old_swift_range_get_beyond_manifest_refetch_tombstone(self):
|
||||
self.app.can_ignore_range = False
|
||||
big = 'e' * 1024 * 1024
|
||||
big_etag = md5hex(big)
|
||||
big_manifest = json.dumps(
|
||||
|
@ -2929,6 +2932,9 @@ class TestSloGetManifest(SloTestCase):
|
|||
'X-Backend-Timestamp': '1234',
|
||||
'Etag': md5hex(big_manifest)},
|
||||
big_manifest),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted
|
||||
# this second refetch which shows it was deleted
|
||||
(swob.HTTPNotFound, {'X-Backend-Timestamp': '1345'}, None)])
|
||||
|
||||
req = Request.blank(
|
||||
|
@ -2947,10 +2953,11 @@ class TestSloGetManifest(SloTestCase):
|
|||
('GET', '/v1/AUTH_test/gettest/big_manifest'),
|
||||
])
|
||||
|
||||
def test_range_get_bogus_content_range(self):
|
||||
def test_old_swift_range_get_bogus_content_range(self):
|
||||
self.app.can_ignore_range = False
|
||||
# Just a little paranoia; Swift currently sends back valid
|
||||
# Content-Range headers, but if somehow someone sneaks an invalid one
|
||||
# in there, we'll ignore it.
|
||||
# in there, we'll ignore it, when sniffing a 206 manifest response.
|
||||
|
||||
def content_range_breaker_factory(app):
|
||||
def content_range_breaker(env, start_response):
|
||||
|
@ -2977,6 +2984,11 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
# new swift would have ignored the range and got the whole
|
||||
# manifest on the first try and therefore never have attempted to
|
||||
# look at Content-Range; new swift has exactly the same behavior
|
||||
# but w/o this extra refetch request, however on new swift the
|
||||
# broken content-range in the resp isn't intresting or relevant
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
|
@ -2994,13 +3006,13 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '25')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 5-29/50')
|
||||
self.assertEqual(headers['Etag'], '"%s"' % self.manifest_abcd_etag)
|
||||
self.assertEqual(body, b'bbbbbbbbbbccccccccccccccc')
|
||||
|
||||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get')])
|
||||
|
@ -3010,7 +3022,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertIsNone(headers[1].get('Range'))
|
||||
self.assertIsNone(headers[2].get('Range'))
|
||||
self.assertIsNone(headers[3].get('Range'))
|
||||
self.assertIsNone(headers[4].get('Range'))
|
||||
|
||||
def test_range_get_manifest_first_byte(self):
|
||||
req = Request.blank(
|
||||
|
@ -3022,6 +3033,7 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '1')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 0-0/50')
|
||||
self.assertEqual(body, b'a')
|
||||
|
||||
# Make sure we don't get any objects we don't need, including
|
||||
|
@ -3029,7 +3041,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get')])
|
||||
|
||||
def test_range_get_manifest_sub_slo(self):
|
||||
|
@ -3041,6 +3052,7 @@ class TestSloGetManifest(SloTestCase):
|
|||
headers = HeaderKeyDict(headers)
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '6')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 25-30/50')
|
||||
self.assertEqual(body, b'cccccd')
|
||||
|
||||
# Make sure we don't get any objects we don't need, including
|
||||
|
@ -3048,7 +3060,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/d_20?multipart-manifest=get')])
|
||||
|
@ -3063,8 +3074,14 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '5')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 45-49/50')
|
||||
self.assertEqual(body, b'ddddd')
|
||||
|
||||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/d_20?multipart-manifest=get')])
|
||||
|
||||
def test_range_get_manifest_unsatisfiable(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
|
@ -3207,6 +3224,7 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '20')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 7-26/32')
|
||||
self.assertEqual(headers['Content-Type'], 'application/json')
|
||||
self.assertIn('Etag', headers)
|
||||
self.assertEqual(body, b'accccccccbbbbbbbbddd')
|
||||
|
@ -3214,7 +3232,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd-ranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd-ranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc-ranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
|
@ -3225,7 +3242,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(ranges, [
|
||||
'bytes=7-26',
|
||||
None,
|
||||
None,
|
||||
'bytes=4-',
|
||||
'bytes=0-3,11-',
|
||||
'bytes=4-7,2-5',
|
||||
|
@ -3245,13 +3261,13 @@ class TestSloGetManifest(SloTestCase):
|
|||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertEqual(headers['Content-Length'], '9')
|
||||
self.assertEqual(headers['Content-Range'], 'bytes 4-12/17')
|
||||
self.assertEqual(headers['Content-Type'], 'application/json')
|
||||
self.assertEqual(body, b'cdccbbbab')
|
||||
|
||||
self.assertEqual(
|
||||
self.app.calls,
|
||||
[('GET', '/v1/AUTH_test/gettest/manifest-abcd-subranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd-subranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd-ranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc-ranges'),
|
||||
('GET', '/v1/AUTH_test/gettest/c_15?multipart-manifest=get'),
|
||||
|
@ -3266,7 +3282,6 @@ class TestSloGetManifest(SloTestCase):
|
|||
'bytes=4-12',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
'bytes=2-2',
|
||||
'bytes=11-11',
|
||||
'bytes=13-',
|
||||
|
@ -3278,9 +3293,12 @@ class TestSloGetManifest(SloTestCase):
|
|||
self.assertEqual(self.app.swift_sources[1:],
|
||||
['SLO'] * (len(self.app.swift_sources) - 1))
|
||||
|
||||
def test_range_get_includes_whole_range_manifest(self):
|
||||
def test_old_swift_range_get_includes_whole_range_manifest(self):
|
||||
self.app.can_ignore_range = False
|
||||
# If the first range GET results in retrieval of the entire manifest
|
||||
# body (which we can detect by looking at Content-Range), then we
|
||||
# body (and not because of X-Backend-Ignore-Range-If-Metadata-Present,
|
||||
# but because the requested range happened to be sufficient which we
|
||||
# detected by looking at the Content-Range response header), then we
|
||||
# should not go make a second, non-ranged request just to retrieve the
|
||||
# same bytes again.
|
||||
req = Request.blank(
|
||||
|
@ -4512,7 +4530,8 @@ class TestSloConditionalGetOldManifest(SloTestCase):
|
|||
self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'X-Object-Sysmeta-Custom-Etag,x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_matches_and_range(self):
|
||||
def test_old_swift_if_match_matches_and_range(self):
|
||||
self.app.can_ignore_range = False
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
|
@ -4527,7 +4546,9 @@ class TestSloConditionalGetOldManifest(SloTestCase):
|
|||
|
||||
expected_app_calls = [
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
# Needed to re-fetch because Range (and, for old manifests, 412)
|
||||
# new-sytle manifest sysmeta was added 2016, but ignore-range
|
||||
# didn't get added until 2020, so both new and old manifest
|
||||
# will still require refetch with old-swift
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
|
@ -4537,6 +4558,38 @@ class TestSloConditionalGetOldManifest(SloTestCase):
|
|||
self.assertEqual(self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_matches_and_range(self):
|
||||
req = Request.blank(
|
||||
'/v1/AUTH_test/gettest/manifest-abcd',
|
||||
environ={'REQUEST_METHOD': 'GET'},
|
||||
headers={'If-Match': self.slo_etag,
|
||||
'Range': 'bytes=3-6'})
|
||||
status, headers, body = self.call_slo(req)
|
||||
|
||||
self.assertEqual(status, '206 Partial Content')
|
||||
self.assertIn(('Content-Length', '4'), headers)
|
||||
self.assertIn(('Content-Range', 'bytes 3-6/50'), headers)
|
||||
self.assertIn(('Etag', '"%s"' % self.manifest_abcd_etag), headers)
|
||||
self.assertEqual(body, b'aabb')
|
||||
|
||||
expected_app_calls = [
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
]
|
||||
if not self.manifest_has_sysmeta:
|
||||
# Needed to re-fetch because if-match can't find slo-etag
|
||||
expected_app_calls.append(
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-abcd'),
|
||||
)
|
||||
# and then fetch the segments
|
||||
expected_app_calls.extend([
|
||||
('GET', '/v1/AUTH_test/gettest/manifest-bc'),
|
||||
('GET', '/v1/AUTH_test/gettest/a_5?multipart-manifest=get'),
|
||||
('GET', '/v1/AUTH_test/gettest/b_10?multipart-manifest=get'),
|
||||
])
|
||||
self.assertEqual(self.app.calls, expected_app_calls)
|
||||
self.assertEqual(self.app.headers[0].get('X-Backend-Etag-Is-At'),
|
||||
'x-object-sysmeta-slo-etag')
|
||||
|
||||
def test_if_match_matches_passthrough(self):
|
||||
# first fetch and stash the manifest etag
|
||||
req = Request.blank(
|
||||
|
|
|
@ -653,3 +653,54 @@ class TestHTTPResponseToDocumentIters(unittest.TestCase):
|
|||
do_test()
|
||||
metadata = dict((k.upper(), v) for k, v in metadata.items())
|
||||
do_test()
|
||||
|
||||
def test_ignore_range_header(self):
|
||||
req = Request.blank('/v/a/c/o')
|
||||
self.assertIsNone(req.headers.get(
|
||||
'X-Backend-Ignore-Range-If-Metadata-Present'))
|
||||
rh.update_ignore_range_header(req, 'X-Static-Large-Object')
|
||||
self.assertEqual('X-Static-Large-Object', req.headers.get(
|
||||
'X-Backend-Ignore-Range-If-Metadata-Present'))
|
||||
rh.update_ignore_range_header(req, 'X-Static-Large-Object')
|
||||
self.assertEqual(
|
||||
'X-Static-Large-Object,X-Static-Large-Object',
|
||||
req.headers.get('X-Backend-Ignore-Range-If-Metadata-Present'))
|
||||
rh.update_ignore_range_header(req, 'X-Object-Sysmeta-Slo-Etag')
|
||||
self.assertEqual(
|
||||
'X-Static-Large-Object,X-Static-Large-Object,'
|
||||
'X-Object-Sysmeta-Slo-Etag',
|
||||
req.headers.get('X-Backend-Ignore-Range-If-Metadata-Present'))
|
||||
|
||||
def test_resolove_ignore_range_header(self):
|
||||
# no ignore header is no-op
|
||||
req = Request.blank('/v/a/c/o', headers={'Range': 'bytes=0-4'})
|
||||
self.assertEqual(str(req.range), 'bytes=0-4')
|
||||
rh.resolve_ignore_range_header(req, {
|
||||
'X-Static-Large-Object': True,
|
||||
'X-Object-Meta-Color': 'blue',
|
||||
})
|
||||
self.assertEqual(str(req.range), 'bytes=0-4')
|
||||
|
||||
# missing matching metadata is no-op
|
||||
rh.update_ignore_range_header(req, 'X-Static-Large-Object')
|
||||
rh.resolve_ignore_range_header(req, {
|
||||
'X-Object-Meta-Color': 'blue',
|
||||
})
|
||||
self.assertEqual(str(req.range), 'bytes=0-4')
|
||||
|
||||
# matching metadata pops range
|
||||
rh.resolve_ignore_range_header(req, {
|
||||
'X-Static-Large-Object': True,
|
||||
'X-Object-Meta-Color': 'blue',
|
||||
})
|
||||
self.assertIsNone(req.range)
|
||||
|
||||
def test_multiple_resolove_ignore_range_header(self):
|
||||
req = Request.blank('/v/a/c/o', headers={'Range': 'bytes=0-4'})
|
||||
rh.update_ignore_range_header(req, 'X-Static-Large-Object')
|
||||
rh.update_ignore_range_header(req, 'X-Object-Sysmeta-Slo-Etag')
|
||||
rh.resolve_ignore_range_header(req, {
|
||||
'X-Static-Large-Object': True,
|
||||
'X-Object-Meta-Color': 'blue',
|
||||
})
|
||||
self.assertIsNone(req.range)
|
||||
|
|
Loading…
Reference in New Issue