From 8a6692afc6186309938f0584bd8f9f51ccd565ed Mon Sep 17 00:00:00 2001 From: David Lehman Date: Thu, 26 Jul 2012 16:41:13 -0500 Subject: [PATCH 04/21] Add size-set managers to keep a set of growable requests in sync. SameSizeSet is for mirrored container devices, and ensures that all members in the set end up the same size. TotalSizeSet is for non-mirrored container devices, and tries to achieve a target combined size for devices in the set. --- pyanaconda/storage/__init__.py | 1 + pyanaconda/storage/partitioning.py | 188 +++++++++++++++++++++++++++++++++--- 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py index 901c50b..fb08bd1 100644 --- a/pyanaconda/storage/__init__.py +++ b/pyanaconda/storage/__init__.py @@ -351,6 +351,7 @@ class Storage(object): self.eddDict = {} self.__luksDevs = {} + self.size_sets = [] self.iscsi = iscsi.iscsi() self.fcoe = fcoe.fcoe() diff --git a/pyanaconda/storage/partitioning.py b/pyanaconda/storage/partitioning.py index e9c06f6..b59b8e6 100644 --- a/pyanaconda/storage/partitioning.py +++ b/pyanaconda/storage/partitioning.py @@ -789,7 +789,7 @@ def doPartitioning(storage): free = getFreeRegions(disks) try: allocatePartitions(storage, disks, partitions, free) - growPartitions(disks, partitions, free) + growPartitions(disks, partitions, free, size_sets=storage.size_sets) finally: # The number and thus the name of partitions may have changed now, # allocatePartitions() takes care of this for new partitions, but not @@ -1188,6 +1188,8 @@ class Chunk(object): for req in requests: self.addRequest(req) + self.skip_list = [] + def __repr__(self): s = ("%(type)s instance --\n" "device = %(device)s length = %(length)d size = %(size)d\n" @@ -1212,6 +1214,22 @@ class Chunk(object): if not req.done: self.base += req.base + def reclaim(self, request, amount): + """ Reclaim units from a request and return them to the pool. """ + log.debug("reclaim: %s %d (%d MB)" % (request, amount, self.lengthToSize(amount))) + if request.growth < amount: + log.error("tried to reclaim %d from request with %d of growth" + % (amount, request.growth)) + raise ValueError("cannot reclaim more than request has grown") + + request.growth -= amount + self.pool += amount + + # put this request in the skip list so we don't try to grow it the + # next time we call growRequests to allocate the newly re-acquired pool + if request not in self.skip_list: + self.skip_list.append(request) + @property def growth(self): """ Sum of growth for all requests in this chunk. """ @@ -1291,7 +1309,7 @@ class Chunk(object): log.debug("%d requests and %d (%dMB) left in chunk" % (self.remaining, self.pool, self.lengthToSize(self.pool))) for p in self.requests: - if p.done: + if p.done or p in self.skip_list: continue if not uniform: @@ -1336,6 +1354,10 @@ class Chunk(object): if self.pool == 0: break + # requests that were skipped over this time through are back on the + # table next time + self.skip_list = [] + class DiskChunk(Chunk): """ A free region on disk from which partitions will be allocated """ @@ -1549,7 +1571,121 @@ def getDiskChunks(disk, partitions, free): return chunks -def growPartitions(disks, partitions, free): +class TotalSizeSet(object): + """ Set of device requests with a target combined size. + + This will be handled by growing the requests until the desired combined + size has been achieved. + """ + def __init__(self, devices, size): + self.devices = devices + self.size = size + + self.requests = [] + + self.allocated = sum([d.req_base_size for d in self.devices]) + log.debug("set.allocated = %d" % self.allocated) + + def allocate(self, amount): + log.debug("allocating %d to TotalSizeSet with %d/%d (%d needed)" + % (amount, self.allocated, self.size, self.needed)) + self.allocated += amount + + @property + def needed(self): + return self.size - self.allocated + + def deallocate(self, amount): + log.debug("deallocating %d from TotalSizeSet with %d/%d (%d needed)" + % (amount, self.allocated, self.size, self.needed)) + self.allocated -= amount + +class SameSizeSet(object): + """ Set of device requests with a common target size. """ + def __init__(self, devices, size, grow=False, max_size=None): + self.devices = devices + self.size = int(size / len(devices)) + self.grow = grow + self.max_size = max_size + + self.requests = [] + +def manageSizeSets(size_sets, chunks): + growth_by_request = {} + requests_by_device = {} + chunks_by_request = {} + for chunk in chunks: + for request in chunk.requests: + requests_by_device[request.device] = request + chunks_by_request[request] = chunk + growth_by_request[request] = 0 + + for i in range(2): + reclaimed = dict([(chunk, 0) for chunk in chunks]) + for ss in size_sets: + if isinstance(ss, TotalSizeSet): + # TotalSizeSet members are trimmed to achieve the requested + # total size + log.debug("set: %s %d/%d" % ([d.name for d in ss.devices], + ss.allocated, ss.size)) + + for device in ss.devices: + request = requests_by_device[device] + if request.done: + continue + + chunk = chunks_by_request[request] + new_growth = request.growth - growth_by_request[request] + ss.allocate(chunk.lengthToSize(new_growth)) + + # decide how much to take back from each request + # We may assume that all requests have the same base size. + # We're shooting for a roughly equal distribution by trimming + # growth from the requests that have grown the most first. + requests = sorted([requests_by_device[d] for d in ss.devices], + key=lambda r: r.growth, reverse=True) + for request in requests: + chunk = chunks_by_request[request] + log.debug("%s" % request) + log.debug("needed: %d" % ss.needed) + + if ss.needed < 0: + # it would be good to take back some from each device + # instead of taking all from the last one(s) + extra = min(-chunk.sizeToLength(ss.needed), + request.growth) + reclaimed[chunk] += extra + chunk.reclaim(request, extra) + ss.deallocate(chunk.lengthToSize(extra)) + + if ss.needed <= 0: + request.done = True + + elif isinstance(ss, SameSizeSet): + # SameSizeSet members all have the same size as the smallest + # member + requests = [requests_by_device[d] for d in ss.devices] + min_growth = min([r.growth for r in requests]) + for request in requests: + if request.growth > min_growth: + chunk = chunks_by_request[request] + extra = request.growth - min_growth + reclaimed[chunk] += extra + chunk.reclaim(request, extra) + request.done = True + elif request.growth == min_growth: + request.done = True + + # store previous growth amounts so we know how much was allocated in + # the latest growRequests call + for request in growth_by_request.keys(): + growth_by_request[request] = request.growth + + for chunk in chunks: + if reclaimed[chunk] and not chunk.done: + chunk.growRequests() + +def growPartitions(disks, partitions, free, size_sets=None): """ Grow all growable partition requests. Partitions have already been allocated from chunks of free space on @@ -1575,36 +1711,60 @@ def growPartitions(disks, partitions, free): log.debug("no growable partitions") return + if size_sets is None: + size_sets = [] + log.debug("growable partitions are %s" % [p.name for p in all_growable]) + # + # collect info about each disk and the requests it contains + # + chunks = [] for disk in disks: - log.debug("growing partitions on %s" % disk.name) sector_size = disk.format.partedDevice.sectorSize - # find any extended partition on this disk - extended_geometry = getattr(disk.format.extendedPartition, - "geometry", - None) # parted.Geometry - # list of free space regions on this disk prior to partition allocation disk_free = [f for f in free if f.device.path == disk.path] if not disk_free: log.debug("no free space on %s" % disk.name) continue - chunks = getDiskChunks(disk, partitions, disk_free) - log.debug("disk %s has %d chunks" % (disk.name, len(chunks))) - # grow the partitions in each chunk as a group + disk_chunks = getDiskChunks(disk, partitions, disk_free) + log.debug("disk %s has %d chunks" % (disk.name, len(disk_chunks))) + chunks.extend(disk_chunks) + + # + # grow the partitions in each chunk as a group + # + for chunk in chunks: + if not chunk.hasGrowable: + # no growable partitions in this chunk + continue + + chunk.growRequests() + + # adjust set members' growth amounts as needed + manageSizeSets(size_sets, chunks) + + for disk in disks: + log.debug("growing partitions on %s" % disk.name) for chunk in chunks: + if chunk.path != disk.path: + continue + if not chunk.hasGrowable: # no growable partitions in this chunk continue - chunk.growRequests() - # recalculate partition geometries disklabel = disk.format start = chunk.geometry.start + + # find any extended partition on this disk + extended_geometry = getattr(disklabel.extendedPartition, + "geometry", + None) # parted.Geometry + # align start sector as needed if not disklabel.alignment.isAligned(chunk.geometry, start): start = disklabel.alignment.alignUp(chunk.geometry, start) -- 1.7.7.6