sharding: constrain fill_gaps to own shard range bounds

Restrict the fill_gaps behaviour of ContainerBroker.get_shard_ranges()
to only insert a filler range to the upper bound of its own shard
range.

Change-Id: I16d91a5a3e5c785244497bbc2882b305d2b3ca56
Closes-Bug: #1922386
This commit is contained in:
Alistair Coles 2021-04-06 12:12:32 +01:00
parent b79ca57fa9
commit 7bbc73a91c
3 changed files with 136 additions and 4 deletions

View File

@ -1774,8 +1774,10 @@ class ContainerBroker(DatabaseBroker):
names do not match the broker's path are included in the returned
list. If True, those rows are not included, otherwise they are
included. Default is False.
:param fill_gaps: if True, insert own shard range to fill any gaps in
at the tail of other shard ranges.
:param fill_gaps: if True, insert a modified copy of own shard range to
fill any gap between the end of any found shard ranges and the
upper bound of own shard range. Gaps enclosed within the found
shard ranges are not filled.
:return: a list of instances of :class:`swift.common.utils.ShardRange`
"""
if reverse:
@ -1796,11 +1798,14 @@ class ContainerBroker(DatabaseBroker):
marker, end_marker)
if not includes and fill_gaps:
own_shard_range = self._own_shard_range()
if shard_ranges:
last_upper = shard_ranges[-1].upper
else:
last_upper = marker or ShardRange.MIN
required_upper = end_marker or ShardRange.MAX
last_upper = max(marker or own_shard_range.lower,
own_shard_range.lower)
required_upper = min(end_marker or own_shard_range.upper,
own_shard_range.upper)
if required_upper > last_upper:
filler_sr = self.get_own_shard_range()
filler_sr.lower = last_upper

View File

@ -4062,6 +4062,37 @@ class TestContainerBroker(unittest.TestCase):
[dict(sr) for sr in [shard_ranges[2], shard_ranges[4]]],
[dict(sr) for sr in actual])
# fill gaps
filler = own_shard_range.copy()
filler.lower = 'h'
with mock_timestamp_now() as now:
actual = broker.get_shard_ranges(fill_gaps=True)
filler.meta_timestamp = now
self.assertEqual([dict(sr) for sr in undeleted + [filler]],
[dict(sr) for sr in actual])
with mock_timestamp_now() as now:
actual = broker.get_shard_ranges(fill_gaps=True, marker='a')
filler.meta_timestamp = now
self.assertEqual([dict(sr) for sr in undeleted + [filler]],
[dict(sr) for sr in actual])
with mock_timestamp_now() as now:
actual = broker.get_shard_ranges(fill_gaps=True, end_marker='z')
filler.meta_timestamp = now
self.assertEqual([dict(sr) for sr in undeleted + [filler]],
[dict(sr) for sr in actual])
with mock_timestamp_now() as now:
actual = broker.get_shard_ranges(fill_gaps=True, end_marker='k')
filler.meta_timestamp = now
filler.upper = 'k'
self.assertEqual([dict(sr) for sr in undeleted + [filler]],
[dict(sr) for sr in actual])
# no filler needed...
actual = broker.get_shard_ranges(fill_gaps=True, end_marker='h')
self.assertEqual([dict(sr) for sr in undeleted],
[dict(sr) for sr in actual])
actual = broker.get_shard_ranges(fill_gaps=True, end_marker='a')
self.assertEqual([], [dict(sr) for sr in actual])
# get everything
actual = broker.get_shard_ranges(include_own=True)
self.assertEqual([dict(sr) for sr in undeleted + [own_shard_range]],

View File

@ -2972,6 +2972,102 @@ class TestContainerController(unittest.TestCase):
self.assertFalse(self.controller.logger.get_lines_for_level('warning'))
self.assertFalse(self.controller.logger.get_lines_for_level('error'))
def test_GET_shard_ranges_from_compacted_shard(self):
# make a shrunk shard container with two acceptors that overlap with
# the shard's namespace
shard_path = '.shards_a/c_f'
ts_iter = make_timestamp_iter()
ts_now = Timestamp.now() # used when mocking Timestamp.now()
own_shard_range = ShardRange(shard_path, next(ts_iter),
'b', 'f', 100, 1000,
meta_timestamp=next(ts_iter),
state=ShardRange.SHRUNK,
state_timestamp=next(ts_iter),
epoch=next(ts_iter))
shard_ranges = []
for lower, upper in (('a', 'd'), ('d', 'g')):
shard_ranges.append(
ShardRange('.shards_a/c_%s' % upper, next(ts_iter),
lower, upper, 100, 1000,
meta_timestamp=next(ts_iter),
state=ShardRange.ACTIVE,
state_timestamp=next(ts_iter)))
# create container
headers = {'X-Timestamp': next(ts_iter).normal}
req = Request.blank(
'/sda1/p/%s' % shard_path, method='PUT', headers=headers)
self.assertIn(
req.get_response(self.controller).status_int, (201, 202))
# PUT the acceptor shard ranges and own shard range
headers = {'X-Timestamp': next(ts_iter).normal,
'X-Container-Sysmeta-Shard-Root': 'a/c',
'X-Backend-Record-Type': 'shard'}
body = json.dumps(
[dict(sr) for sr in shard_ranges + [own_shard_range]])
req = Request.blank('/sda1/p/%s' % shard_path, method='PUT',
headers=headers, body=body)
self.assertEqual(202, req.get_response(self.controller).status_int)
def do_get(params, extra_headers, expected):
expected = [dict(sr, last_modified=sr.timestamp.isoformat)
for sr in expected]
headers = {'X-Backend-Record-Type': 'shard'}
headers.update(extra_headers)
req = Request.blank('/sda1/p/%s?format=json%s' %
(shard_path, params), method='GET',
headers=headers)
with mock_timestamp_now(ts_now):
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 200)
self.assertEqual(resp.content_type, 'application/json')
self.assertEqual(expected, json.loads(resp.body))
self.assertIn('X-Backend-Record-Type', resp.headers)
self.assertEqual('shard', resp.headers['X-Backend-Record-Type'])
return resp
# unsharded shard container...
do_get('', {}, shard_ranges)
do_get('&marker=e', {}, shard_ranges[1:])
do_get('&end_marker=d', {}, shard_ranges[:1])
do_get('&end_marker=k', {}, shard_ranges)
do_get('&marker=b&end_marker=f&states=listing', {}, shard_ranges)
do_get('&marker=b&end_marker=c&states=listing', {}, shard_ranges[:1])
do_get('&marker=b&end_marker=z&states=listing', {}, shard_ranges)
do_get('&states=listing', {}, shard_ranges)
# send X-Backend-Override-Shard-Name-Filter, but db is not yet sharded
# so this has no effect
extra_headers = {'X-Backend-Override-Shard-Name-Filter': 'sharded'}
resp = do_get('', extra_headers, shard_ranges)
self.assertNotIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
resp = do_get('&marker=e', extra_headers, shard_ranges[1:])
self.assertNotIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
resp = do_get('&end_marker=d', extra_headers, shard_ranges[:1])
self.assertNotIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
resp = do_get('&states=listing', {}, shard_ranges)
self.assertNotIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
# set broker to sharded state so X-Backend-Override-Shard-Name-Filter
# does have effect
shard_broker = self.controller._get_container_broker(
'sda1', 'p', '.shards_a', 'c_f')
self.assertTrue(shard_broker.set_sharding_state())
self.assertTrue(shard_broker.set_sharded_state())
resp = do_get('', extra_headers, shard_ranges)
self.assertIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
self.assertTrue(resp.headers['X-Backend-Override-Shard-Name-Filter'])
resp = do_get('&marker=e', extra_headers, shard_ranges)
self.assertIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
self.assertTrue(resp.headers['X-Backend-Override-Shard-Name-Filter'])
resp = do_get('&end_marker=d', extra_headers, shard_ranges)
self.assertIn('X-Backend-Override-Shard-Name-Filter', resp.headers)
self.assertTrue(resp.headers['X-Backend-Override-Shard-Name-Filter'])
def test_GET_shard_ranges_using_state_aliases(self):
# make a shard container
ts_iter = make_timestamp_iter()