Merge "ring: Track more properties of the ring"

This commit is contained in:
Zuul 2019-10-05 02:17:35 +00:00 committed by Gerrit Code Review
commit ff91df2302
5 changed files with 164 additions and 12 deletions

View File

@ -1167,7 +1167,7 @@ swift-ring-builder <ring_file> write_builder [min_part_hours]
'parts': ring.partition_count, 'parts': ring.partition_count,
'devs': ring.devs, 'devs': ring.devs,
'devs_changed': False, 'devs_changed': False,
'version': 0, 'version': ring.version or 0,
'_replica2part2dev': ring._replica2part2dev_id, '_replica2part2dev': ring._replica2part2dev_id,
'_last_part_moves_epoch': None, '_last_part_moves_epoch': None,
'_last_part_moves': None, '_last_part_moves': None,

View File

@ -364,13 +364,15 @@ class RingBuilder(object):
# shift an unsigned int >I right to obtain the partition for the # shift an unsigned int >I right to obtain the partition for the
# int). # int).
if not self._replica2part2dev: if not self._replica2part2dev:
self._ring = RingData([], devs, self.part_shift) self._ring = RingData([], devs, self.part_shift,
version=self.version)
else: else:
self._ring = \ self._ring = \
RingData([array('H', p2d) for p2d in RingData([array('H', p2d) for p2d in
self._replica2part2dev], self._replica2part2dev],
devs, self.part_shift, devs, self.part_shift,
self.next_part_power) self.next_part_power,
self.version)
return self._ring return self._ring
def add_dev(self, dev): def add_dev(self, dev):

View File

@ -22,11 +22,11 @@ from os.path import getmtime
import struct import struct
from time import time from time import time
import os import os
from io import BufferedReader
from hashlib import md5 from hashlib import md5
from itertools import chain, count from itertools import chain, count
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import sys import sys
import zlib
from six.moves import range from six.moves import range
@ -41,15 +41,77 @@ def calc_replica_count(replica2part2dev_id):
return base + extra return base + extra
class RingReader(object):
chunk_size = 2 ** 16
def __init__(self, filename):
self.fp = open(filename, 'rb')
self._reset()
def _reset(self):
self._buffer = b''
self.size = 0
self.raw_size = 0
self._md5 = md5()
self._decomp = zlib.decompressobj(32 + zlib.MAX_WBITS)
@property
def close(self):
return self.fp.close
def seek(self, pos, ref=0):
if (pos, ref) != (0, 0):
raise NotImplementedError
self._reset()
return self.fp.seek(pos, ref)
def _buffer_chunk(self):
chunk = self.fp.read(self.chunk_size)
if not chunk:
return False
self.size += len(chunk)
self._md5.update(chunk)
chunk = self._decomp.decompress(chunk)
self.raw_size += len(chunk)
self._buffer += chunk
return True
def read(self, amount=-1):
if amount < 0:
raise IOError("don't be greedy")
while amount > len(self._buffer):
if not self._buffer_chunk():
break
result, self._buffer = self._buffer[:amount], self._buffer[amount:]
return result
def readline(self):
# apparently pickle needs this?
while b'\n' not in self._buffer:
if not self._buffer_chunk():
break
line, sep, self._buffer = self._buffer.partition(b'\n')
return line + sep
@property
def md5(self):
return self._md5.hexdigest()
class RingData(object): class RingData(object):
"""Partitioned consistent hashing ring data (used for serialization).""" """Partitioned consistent hashing ring data (used for serialization)."""
def __init__(self, replica2part2dev_id, devs, part_shift, def __init__(self, replica2part2dev_id, devs, part_shift,
next_part_power=None): next_part_power=None, version=None):
self.devs = devs self.devs = devs
self._replica2part2dev_id = replica2part2dev_id self._replica2part2dev_id = replica2part2dev_id
self._part_shift = part_shift self._part_shift = part_shift
self.next_part_power = next_part_power self.next_part_power = next_part_power
self.version = version
self.md5 = self.size = self.raw_size = None
for dev in self.devs: for dev in self.devs:
if dev is not None: if dev is not None:
@ -104,7 +166,7 @@ class RingData(object):
:param bool metadata_only: If True, only load `devs` and `part_shift`. :param bool metadata_only: If True, only load `devs` and `part_shift`.
:returns: A RingData instance containing the loaded data. :returns: A RingData instance containing the loaded data.
""" """
gz_file = BufferedReader(GzipFile(filename, 'rb')) gz_file = RingReader(filename)
# See if the file is in the new format # See if the file is in the new format
magic = gz_file.read(4) magic = gz_file.read(4)
@ -124,7 +186,10 @@ class RingData(object):
if not hasattr(ring_data, 'devs'): if not hasattr(ring_data, 'devs'):
ring_data = RingData(ring_data['replica2part2dev_id'], ring_data = RingData(ring_data['replica2part2dev_id'],
ring_data['devs'], ring_data['part_shift'], ring_data['devs'], ring_data['part_shift'],
ring_data.get('next_part_power')) ring_data.get('next_part_power'),
ring_data.get('version'))
for attr in ('md5', 'size', 'raw_size'):
setattr(ring_data, attr, getattr(gz_file, attr))
return ring_data return ring_data
def serialize_v1(self, file_obj): def serialize_v1(self, file_obj):
@ -138,6 +203,9 @@ class RingData(object):
'replica_count': len(ring['replica2part2dev_id']), 'replica_count': len(ring['replica2part2dev_id']),
'byteorder': sys.byteorder} 'byteorder': sys.byteorder}
if ring['version'] is not None:
_text['version'] = ring['version']
next_part_power = ring.get('next_part_power') next_part_power = ring.get('next_part_power')
if next_part_power is not None: if next_part_power is not None:
_text['next_part_power'] = next_part_power _text['next_part_power'] = next_part_power
@ -175,7 +243,8 @@ class RingData(object):
return {'devs': self.devs, return {'devs': self.devs,
'replica2part2dev_id': self._replica2part2dev_id, 'replica2part2dev_id': self._replica2part2dev_id,
'part_shift': self._part_shift, 'part_shift': self._part_shift,
'next_part_power': self.next_part_power} 'next_part_power': self.next_part_power,
'version': self.version}
class Ring(object): class Ring(object):
@ -239,6 +308,10 @@ class Ring(object):
self._rebuild_tier_data() self._rebuild_tier_data()
self._update_bookkeeping() self._update_bookkeeping()
self._next_part_power = ring_data.next_part_power self._next_part_power = ring_data.next_part_power
self._version = ring_data.version
self._md5 = ring_data.md5
self._size = ring_data.size
self._raw_size = ring_data.raw_size
def _update_bookkeeping(self): def _update_bookkeeping(self):
# Do this now, when we know the data has changed, rather than # Do this now, when we know the data has changed, rather than
@ -257,12 +330,19 @@ class Ring(object):
zones = set() zones = set()
ips = set() ips = set()
self._num_devs = 0 self._num_devs = 0
self._num_assigned_devs = 0
self._num_weighted_devs = 0
for dev in self._devs: for dev in self._devs:
if dev and dev['id'] in dev_ids_with_parts: if dev is None:
continue
self._num_devs += 1
if dev.get('weight', 0) > 0:
self._num_weighted_devs += 1
if dev['id'] in dev_ids_with_parts:
regions.add(dev['region']) regions.add(dev['region'])
zones.add((dev['region'], dev['zone'])) zones.add((dev['region'], dev['zone']))
ips.add((dev['region'], dev['zone'], dev['ip'])) ips.add((dev['region'], dev['zone'], dev['ip']))
self._num_devs += 1 self._num_assigned_devs += 1
self._num_regions = len(regions) self._num_regions = len(regions)
self._num_zones = len(zones) self._num_zones = len(zones)
self._num_ips = len(ips) self._num_ips = len(ips)
@ -275,6 +355,22 @@ class Ring(object):
def part_power(self): def part_power(self):
return 32 - self._part_shift return 32 - self._part_shift
@property
def version(self):
return self._version
@property
def md5(self):
return self._md5
@property
def size(self):
return self._size
@property
def raw_size(self):
return self._raw_size
def _rebuild_tier_data(self): def _rebuild_tier_data(self):
self.tier2devs = defaultdict(list) self.tier2devs = defaultdict(list)
for dev in self._devs: for dev in self._devs:
@ -301,6 +397,21 @@ class Ring(object):
"""Number of partitions in the ring.""" """Number of partitions in the ring."""
return len(self._replica2part2dev_id[0]) return len(self._replica2part2dev_id[0])
@property
def device_count(self):
"""Number of devices in the ring."""
return self._num_devs
@property
def weighted_device_count(self):
"""Number of devices with weight in the ring."""
return self._num_weighted_devs
@property
def assigned_device_count(self):
"""Number of devices with assignments in the ring."""
return self._num_assigned_devs
@property @property
def devs(self): def devs(self):
"""devices in the ring""" """devices in the ring"""
@ -490,7 +601,7 @@ class Ring(object):
hit_all_ips = True hit_all_ips = True
break break
hit_all_devs = len(used) == self._num_devs hit_all_devs = len(used) == self._num_assigned_devs
for handoff_part in chain(range(start, parts, inc), for handoff_part in chain(range(start, parts, inc),
range(inc - ((parts - start) % inc), range(inc - ((parts - start) % inc),
start, inc)): start, inc)):
@ -505,6 +616,6 @@ class Ring(object):
dev = self._devs[dev_id] dev = self._devs[dev_id]
yield dict(dev, handoff_index=next(index)) yield dict(dev, handoff_index=next(index))
used.add(dev_id) used.add(dev_id)
if len(used) == self._num_devs: if len(used) == self._num_assigned_devs:
hit_all_devs = True hit_all_devs = True
break break

View File

@ -2263,10 +2263,34 @@ class TestCommands(unittest.TestCase, RunSwiftRingBuilderMixin):
# Note that we've picked up an extension # Note that we've picked up an extension
builder = RingBuilder.load(self.tmpfile + '.builder') builder = RingBuilder.load(self.tmpfile + '.builder')
# Version was recorded in the .ring.gz!
self.assertEqual(builder.version, 5)
# Note that this is different from the original! But it more-closely # Note that this is different from the original! But it more-closely
# reflects the reality that we have an extra replica for 12 of 64 parts # reflects the reality that we have an extra replica for 12 of 64 parts
self.assertEqual(builder.replicas, 1.1875) self.assertEqual(builder.replicas, 1.1875)
def test_write_builder_no_version(self):
self.create_sample_ring()
rb = RingBuilder.load(self.tmpfile)
rb.rebalance()
# Make sure we write down the ring in the old way, with no version
rd = rb.get_ring()
rd.version = None
rd.save(self.tmpfile + ".ring.gz")
ring_file = os.path.join(os.path.dirname(self.tmpfile),
os.path.basename(self.tmpfile) + ".ring.gz")
os.remove(self.tmpfile) # loses file...
argv = ["", ring_file, "write_builder", "24"]
self.assertIsNone(ringbuilder.main(argv))
# Note that we've picked up an extension
builder = RingBuilder.load(self.tmpfile + '.builder')
# No version in the .ring.gz; default to 0
self.assertEqual(builder.version, 0)
def test_write_builder_after_device_removal(self): def test_write_builder_after_device_removal(self):
# Test regenerating builder file after having removed a device # Test regenerating builder file after having removed a device
# and lost the builder file # and lost the builder file

View File

@ -16,6 +16,7 @@
import array import array
import collections import collections
import six.moves.cPickle as pickle import six.moves.cPickle as pickle
import hashlib
import os import os
import unittest import unittest
import stat import stat
@ -63,6 +64,8 @@ class TestRingData(unittest.TestCase):
rd_got._replica2part2dev_id) rd_got._replica2part2dev_id)
self.assertEqual(rd_expected.devs, rd_got.devs) self.assertEqual(rd_expected.devs, rd_got.devs)
self.assertEqual(rd_expected._part_shift, rd_got._part_shift) self.assertEqual(rd_expected._part_shift, rd_got._part_shift)
self.assertEqual(rd_expected.next_part_power, rd_got.next_part_power)
self.assertEqual(rd_expected.version, rd_got.version)
def test_attrs(self): def test_attrs(self):
r2p2d = [[0, 1, 0, 1], [0, 1, 0, 1]] r2p2d = [[0, 1, 0, 1], [0, 1, 0, 1]]
@ -230,6 +233,17 @@ class TestRing(TestRingBase):
self.assertEqual(self.ring.devs, self.intended_devs) self.assertEqual(self.ring.devs, self.intended_devs)
self.assertEqual(self.ring.reload_time, self.intended_reload_time) self.assertEqual(self.ring.reload_time, self.intended_reload_time)
self.assertEqual(self.ring.serialized_path, self.testgz) self.assertEqual(self.ring.serialized_path, self.testgz)
self.assertIsNone(self.ring.version)
with open(self.testgz, 'rb') as fp:
expected_md5 = hashlib.md5()
expected_size = 0
for chunk in iter(lambda: fp.read(2 ** 16), b''):
expected_md5.update(chunk)
expected_size += len(chunk)
self.assertEqual(self.ring.md5, expected_md5.hexdigest())
self.assertEqual(self.ring.size, expected_size)
# test invalid endcap # test invalid endcap
with mock.patch.object(utils, 'HASH_PATH_SUFFIX', b''), \ with mock.patch.object(utils, 'HASH_PATH_SUFFIX', b''), \
mock.patch.object(utils, 'HASH_PATH_PREFIX', b''), \ mock.patch.object(utils, 'HASH_PATH_PREFIX', b''), \
@ -900,6 +914,7 @@ class TestRing(TestRingBase):
rb.rebalance() rb.rebalance()
rb.get_ring().save(self.testgz) rb.get_ring().save(self.testgz)
r = ring.Ring(self.testdir, ring_name='whatever') r = ring.Ring(self.testdir, ring_name='whatever')
self.assertEqual(r.version, rb.version)
class CountingRingTable(object): class CountingRingTable(object):