From 6e0e1922af187243f5f420ca57cbae39f66c7c6e Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Fri, 20 Jun 2014 15:18:24 -0500
Subject: [PATCH 10/12] Add ChunkSet class for managing growth of requests
 across multiple chunks.

---
 blivet/partitioning.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 148 insertions(+), 9 deletions(-)

diff --git a/blivet/partitioning.py b/blivet/partitioning.py
index 515d889..39a54a4 100644
--- a/blivet/partitioning.py
+++ b/blivet/partitioning.py
@@ -1757,6 +1757,153 @@ class ThinPoolChunk(VGChunk):
         usable_extents = (pool.size / pool.vg.peSize)
         super(VGChunk, self).__init__(usable_extents, requests=requests) # pylint: disable=bad-super-call
 
+class ChunkSet(object):
+    """ A class to manage growth of requests across a set of chunks. """
+    def __init__(self, chunks=None, size_sets=None):
+        self.chunks = chunks or []
+        self.size_sets = size_sets or []
+        self.chunks_grown = False
+
+    def reset(self):
+        map(lambda c: c.reset(), self.chunks)
+        map(lambda s: s.reset(), self.size_sets)
+        self.chunks_grown = False
+
+    @property
+    def done(self):
+        return (all(c.done for c in self.chunks) or
+                (self.chunks_grown and all(s.done for s in self.size_sets)))
+
+    def growRequests(self, uniform=False):
+        """ Grow requests in all chunks while managing size sets. """
+
+        # growth is stored as Size since chunks could have varying sector sizes
+
+        # TODO: eliminate either device_chunks or request_chunks
+        request_chunks = dict()
+        """ request keys and chunk values """
+
+        device_requests = dict()
+        """ device keys and request values """
+
+        device_chunks = dict()
+        """ device keys and chunk values """
+
+        request_growth = dict()
+        """ request keys and size values (growth through previous iteration) """
+
+        # initialize the dicts
+        for chunk in self.chunks:
+            for request in chunk.requests:
+                request_chunks[request] = chunk
+                request_growth[request] = Size(0)
+                device_requests[request.device] = request
+                device_chunks[request.device] = chunk
+
+        while not self.done:
+            ## grow requests in each chunk
+            for chunk in self.chunks:
+                chunk.growRequests(uniform=uniform)
+
+            self.chunks_grown = True
+
+            ## adjust growth as needed to accommodate size sets
+            total_previous_growth = sum(request_growth.itervalues())
+            total_growth = sum(c.lengthToSize(c.growth) for c in self.chunks)
+            total_new_growth = total_growth - total_previous_growth
+            log.debug("total new chunk set growth: %s", total_new_growth)
+            if total_new_growth == 0:
+                log.debug("no new growth across chunk set; bailing")
+                break
+
+            # For uniform sets, all members must shrink to match the smallest
+            # member.
+            #
+            # For non-uniform sets, members must shrink until the total size is
+            # met. Members are shrunk such that they end up as close to equal in
+            # size as is possible.
+            for ss in self.size_sets:
+                log.debug("set: %s", ss)
+                growth = []     # total growth (size), indices match ss.devices
+                new_growth = [] # latest growth (size), indices match ss.devices
+                for dev in ss.devices:
+                    request = device_requests[dev]
+                    chunk = device_chunks[dev]
+                    growth_size = chunk.lengthToSize(request.growth)
+                    growth.append(growth_size)
+                    new_growth.append(growth_size - request_growth[request])
+
+                log.debug("total new set growth is %s", sum(new_growth))
+                if sum(new_growth) == 0:
+                    log.debug("set is done growing")
+                    ss.done = True
+                    continue
+
+                if ss.uniform:
+                    total_new_growth = sum(new_growth)
+                    if ss.total is not None and total_new_growth > ss.needed:
+                        log.debug("set is overgrown")
+                        member_growth = ss.needed / len(ss.devices)
+                        for dev in ss.devices:
+                            device_requests[dev].done = True
+                    else:
+                        log.debug("set is within limits")
+                        member_growth = min(growth)
+
+                    # normalize all requests' growth to the minimum
+                    for idx, dev in enumerate(ss.devices):
+                        request = device_requests[dev]
+                        chunk = device_chunks[dev]
+                        growth_size = chunk.lengthToSize(request.growth)
+                        if growth_size > member_growth:
+                            extra = growth_size - member_growth
+                            chunk.reclaim(request, chunk.sizeToLength(extra))
+                            growth_size = chunk.lengthToSize(request.growth)
+                            growth[idx] = growth_size
+                            new_growth[idx] = growth_size - request_growth[request]
+                else:
+                    # trim new growth up to the amount from this iteration from
+                    # requests with most total growth first until the total new
+                    # growth is not more than the set needs
+                    if ss.needed is not None:
+                        least_growth = min(growth)
+                        total_new_growth = sum(new_growth)
+                        # Starting with the full list of devices, we want to be
+                        # able to select the one among those that have not yet
+                        # been selected that has the most total growth.
+                        trimmed_growth = growth[:]
+                        if total_new_growth > ss.needed:
+                            # decide how much to reclaim from each request
+                            while total_new_growth > ss.needed:
+                                extra = total_new_growth - ss.needed
+                                idx = ss.devices.index(max(trimmed_growth))
+                                request = device_requests[idx]
+                                chunk = request_chunks[request]
+                                reclaim = min(new_growth[idx], extra)
+                                chunk.reclaim(request,
+                                              chunk.sizeToLength(reclaim))
+                                growth_size = chunk.lengthToSize(request.growth)
+                                growth[idx] = growth_size
+                                new_growth[idx] = growth_size - request_growth[idx]
+                                trimmed_growth.pop(idx)
+
+                if sum(new_growth) == 0:
+                    log.debug("set is done growing after trimming requests")
+                    ss.done = True
+                    continue
+
+                # now adjust the size set allocations to match the calculations
+                # from above
+                for idx, dev in enumerate(ss.devices):
+                    ss.allocate(new_growth[idx])
+                    request = device_requests[dev]
+                    chunk = device_chunks[dev]
+                    request_growth[request] = chunk.lengthToSize(request.growth)
+
+            if all(ss.done for ss in self.size_sets):
+                log.debug("all size sets done growing; bailing")
+                break
+
 def getDiskChunks(disk, partitions, free):
     """ Return a list of Chunk instances representing a disk.
 
@@ -1905,15 +2052,7 @@ def growPartitions(disks, partitions, free, size_sets=None):
     #
     # 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)
+    ChunkSet(chunks=chunks, size_sets=size_sets).growRequests()
 
     for disk in disks:
         log.debug("growing partitions on %s", disk.name)
-- 
1.9.3

