"""
Keeps the initial and target pg_num values
"""
+
def __init__(self, pool_id: int, pg_num: int, pg_num_target: int) -> None:
self.ev_id = str(uuid.uuid4())
self.pool_id = pool_id
desc = 'increasing' if self.pg_num < self.pg_num_target else 'decreasing'
module.remote('progress', 'update', self.ev_id,
ev_msg="PG autoscaler %s pool %d PGs from %d to %d" %
- (desc, self.pool_id, self.pg_num, self.pg_num_target),
+ (desc, self.pool_id, self.pg_num, self.pg_num_target),
ev_progress=progress,
refs=[("pool", self.pool_id)])
self.pool_count: Optional[int] = None
self.pool_used = 0
self.total_target_ratio = 0.0
- self.total_target_bytes = 0 # including replication / EC overhead
+ self.total_target_bytes = 0 # including replication / EC overhead
class PgAutoscaler(MgrModule):
osdmap = self.get_osdmap()
pools = osdmap.get_pools_by_name()
profile = self.autoscale_profile
- ps, root_map, pool_root = self._get_pool_status(osdmap, pools, profile)
+ ps, root_map = self._get_pool_status(osdmap, pools, profile)
if format in ('json', 'json-pretty'):
return 0, json.dumps(ps, indent=4, sort_keys=True), ''
self.log.info('Stopping pg_autoscaler')
self._shutdown.set()
- def get_subtree_resource_status(self,
- osdmap: OSDMap,
- crush: CRUSHMap) -> Tuple[Dict[int, CrushSubtreeResourceStatus],
- Dict[int, int]]:
- """
- For each CRUSH subtree of interest (i.e. the roots under which
- we have pools), calculate the current resource usages and targets,
- such as how many PGs there are, vs. how many PGs we would
- like there to be.
- """
- result: Dict[int, CrushSubtreeResourceStatus] = {}
- pool_root = {}
- roots = []
+ def identify_subtrees_and_overlaps(self,
+ osdmap: OSDMap,
+ crush: CRUSHMap,
+ result: Dict[int, CrushSubtreeResourceStatus],
+ overlapped_roots: Set[int],
+ roots: List[CrushSubtreeResourceStatus]) -> \
+ Tuple[List[CrushSubtreeResourceStatus],
+ Set[int]]:
- # identify subtrees (note that they may overlap!)
+ # We identify subtrees and overlapping roots from osdmap
for pool_id, pool in osdmap.get_pools().items():
crush_rule = crush.get_rule_by_id(pool['crush_rule'])
assert crush_rule is not None
cr_name = crush_rule['rule_name']
root_id = crush.get_rule_root(cr_name)
assert root_id is not None
- pool_root[pool_id] = root_id
osds = set(crush.get_osds_under(root_id))
- # do we intersect an existing root?
+ # Are there overlapping roots?
s = None
- for prev in result.values():
+ for prev_root_id, prev in result.items():
if osds & prev.osds:
s = prev
+ if prev_root_id != root_id:
+ overlapped_roots.add(prev_root_id)
+ overlapped_roots.add(root_id)
+ self.log.error('pool %d has overlapping roots: %s',
+ pool_id, overlapped_roots)
break
if not s:
s = CrushSubtreeResourceStatus()
target_bytes = pool['options'].get('target_size_bytes', 0)
if target_bytes:
s.total_target_bytes += target_bytes * osdmap.pool_raw_used_rate(pool_id)
+ return roots, overlapped_roots
+ def get_subtree_resource_status(self,
+ osdmap: OSDMap,
+ crush: CRUSHMap) -> Tuple[Dict[int, CrushSubtreeResourceStatus],
+ Set[int]]:
+ """
+ For each CRUSH subtree of interest (i.e. the roots under which
+ we have pools), calculate the current resource usages and targets,
+ such as how many PGs there are, vs. how many PGs we would
+ like there to be.
+ """
+ result: Dict[int, CrushSubtreeResourceStatus] = {}
+ roots: List[CrushSubtreeResourceStatus] = []
+ overlapped_roots: Set[int] = set()
+ # identify subtrees and overlapping roots
+ roots, overlapped_roots = self.identify_subtrees_and_overlaps(osdmap,
+ crush, result, overlapped_roots, roots)
# finish subtrees
all_stats = self.get('osd_stats')
for s in roots:
capacity += osd_stats['kb'] * 1024
s.capacity = capacity
-
self.log.debug('root_ids %s pools %s with %d osds, pg_target %d',
s.root_ids,
s.pool_ids,
s.osd_count,
s.pg_target)
- return result, pool_root
+ return result, overlapped_roots
def _calc_final_pg_target(
self,
"""
`profile` determines behaviour of the autoscaler.
`is_used` flag used to determine if this is the first
- pass where the caller tries to calculate/adjust pools that has
- used_ratio > even_ratio else this is the second pass,
- we calculate final_ratio by giving it 1 / pool_count
+ pass where the caller tries to calculate/adjust pools that has
+ used_ratio > even_ratio else this is the second pass,
+ we calculate final_ratio by giving it 1 / pool_count
of the root we are currently looking at.
"""
if profile == "scale-up":
assert pg_target is not None
pool_pg_target = (final_ratio * pg_target) / p['size'] * bias
final_pg_target = max(p.get('options', {}).get('pg_num_min', PG_NUM_MIN),
- nearest_power_of_two(pool_pg_target))
+ nearest_power_of_two(pool_pg_target))
else:
if is_used:
pool_pg_target = (final_ratio * root_map[root_id].pg_left) / p['size'] * bias
final_pg_target = max(p.get('options', {}).get('pg_num_min', PG_NUM_MIN),
- nearest_power_of_two(pool_pg_target))
+ nearest_power_of_two(pool_pg_target))
self.log.info("Pool '{0}' root_id {1} using {2} of space, bias {3}, "
"pg target {4} quantized to {5} (current {6})".format(
pools: Dict[str, Dict[str, Any]],
crush_map: CRUSHMap,
root_map: Dict[int, CrushSubtreeResourceStatus],
- pool_root: Dict[int, int],
pool_stats: Dict[int, Dict[str, int]],
ret: List[Dict[str, Any]],
threshold: float,
is_used: bool,
profile: 'ScaleModeT',
+ overlapped_roots: Set[int],
) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]:
"""
Calculates final_pg_target of each pools and determine if it needs
scaling, this depends on the profile of the autoscaler. For scale-down,
- we start out with a full complement of pgs and only descrease it when other
+ we start out with a full complement of pgs and only descrease it when other
pools needs more pgs due to increased usage. For scale-up, we start out with
the minimal amount of pgs and only scale when there is increase in usage.
"""
cr_name = crush_rule['rule_name']
root_id = crush_map.get_rule_root(cr_name)
assert root_id is not None
-
+ if root_id in overlapped_roots and profile == "scale-down":
+ # for scale-down profile skip pools
+ # with overlapping roots
+ self.log.warn("pool %d contains an overlapping root %d"
+ "... skipping scaling", pool_id, root_id)
+ continue
capacity = root_map[root_id].capacity
assert capacity is not None
if capacity == 0:
capacity)
capacity_ratio = max(capacity_ratio, target_ratio)
- final_ratio, pool_pg_target, final_pg_target = self._calc_final_pg_target(p,
- pool_name, root_map, root_id, capacity_ratio, even_pools, bias, is_used,
- profile,
- )
+ final_ratio, pool_pg_target, final_pg_target = self._calc_final_pg_target(
+ p, pool_name, root_map, root_id, capacity_ratio, even_pools, bias, is_used, profile)
if final_ratio is None:
continue
adjust = False
- if (final_pg_target > p['pg_num_target'] * threshold or \
- final_pg_target < p['pg_num_target'] / threshold) and \
- final_ratio >= 0.0 and \
- final_ratio <= 1.0:
+ if (final_pg_target > p['pg_num_target'] * threshold or
+ final_pg_target < p['pg_num_target'] / threshold) and \
+ final_ratio >= 0.0 and \
+ final_ratio <= 1.0:
adjust = True
assert pool_pg_target is not None
'pg_num_final': final_pg_target,
'would_adjust': adjust,
'bias': p.get('options', {}).get('pg_autoscale_bias', 1.0),
- });
+ })
return ret, even_pools
profile: 'ScaleModeT',
threshold: float = 3.0,
) -> Tuple[List[Dict[str, Any]],
- Dict[int, CrushSubtreeResourceStatus],
- Dict[int, int]]:
+ Dict[int, CrushSubtreeResourceStatus]]:
assert threshold >= 2.0
crush_map = osdmap.get_crush()
- root_map, pool_root = self.get_subtree_resource_status(osdmap, crush_map)
+ root_map, overlapped_roots = self.get_subtree_resource_status(osdmap, crush_map)
df = self.get('df')
pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
# First call of _calc_pool_targets() is to find/adjust pools that uses more capacaity than
# the even_ratio of other pools and we adjust those first.
# Second call make use of the even_pools we keep track of in the first call.
- # All we need to do is iterate over those and give them 1/pool_count of the
+ # All we need to do is iterate over those and give them 1/pool_count of the
# total pgs.
- ret, even_pools = self._calc_pool_targets(osdmap, pools, crush_map, root_map, pool_root,
- pool_stats, ret, threshold, True, profile)
+ ret, even_pools = self._calc_pool_targets(osdmap, pools, crush_map, root_map,
+ pool_stats, ret, threshold, True, profile, overlapped_roots)
if profile == "scale-down":
- # We only have adjust even_pools when we use scale-down profile
- ret, _ = self._calc_pool_targets(osdmap, even_pools, crush_map, root_map, pool_root,
- pool_stats, ret, threshold, False, profile)
+ # We only have adjust even_pools when we use scale-down profile
+ ret, _ = self._calc_pool_targets(osdmap, even_pools, crush_map, root_map,
+ pool_stats, ret, threshold, False, profile, overlapped_roots)
- return (ret, root_map, pool_root)
+ return (ret, root_map)
def _update_progress_events(self) -> None:
osdmap = self.get_osdmap()
return
pools = osdmap.get_pools_by_name()
profile = self.autoscale_profile
- ps, root_map, pool_root = self._get_pool_status(osdmap, pools, profile)
+ ps, root_map = self._get_pool_status(osdmap, pools, profile)
# Anyone in 'warn', set the health message for them and then
# drop them from consideration.
pool_id = p['pool_id']
pool_opts = pools[p['pool_name']]['options']
if pool_opts.get('target_size_ratio', 0) > 0 and pool_opts.get('target_size_bytes', 0) > 0:
- bytes_and_ratio.append('Pool %s has target_size_bytes and target_size_ratio set' % p['pool_name'])
+ bytes_and_ratio.append(
+ 'Pool %s has target_size_bytes and target_size_ratio set' % p['pool_name'])
total_bytes[p['crush_root_id']] += max(
p['actual_raw_used'],
p['target_bytes'] * p['raw_used_rate'])
-#python unit test
+# python unit test
import unittest
from tests import mock
import pytest
import json
from pg_autoscaler import module
+
class RootMapItem:
def __init__(self, pool_count, pg_target, pg_left):
self.pg_left = pg_left
self.pool_used = 0
+
class TestPgAutoscaler(object):
def setup(self):
# a bunch of attributes for testing.
self.autoscaler = module.PgAutoscaler('module_name', 0, 0)
- def helper_test(self, pools, root_map, bias, profile):
+ def helper_test(self, pools, root_map, bias, profile, overlapped_roots):
# Here we simulate how _calc_pool_target() works.
even_pools = {}
for pool_name, p in pools.items():
+ root_id = p['root_id']
+ if root_id in overlapped_roots and profile == "scale-down":
+ # for scale-down profile skip pools
+ # with overlapping roots
+ assert p['no_scale']
+ continue
+
final_ratio, pool_pg_target, final_pg_target = self.autoscaler._calc_final_pg_target(p, pool_name, root_map,
- p['root_id'], p['capacity_ratio'], even_pools, bias, True, profile)
+ p['root_id'], p['capacity_ratio'], even_pools, bias, True, profile)
if final_ratio == None:
# no final_ratio means current pool is an even pool
# and we do not have to do any assertion on it.
- # You will never hit this case with a scale up profile.
+ # You will never hit this case with a scale up profile.
continue
assert p['expected_final_pg_target'] == final_pg_target
if profile == "scale-down":
# We only care about even_pools when profile is a scale-down
- assert not p['even_pools'] and pool_name not in even_pools
+ assert not p['even_pools'] and pool_name not in even_pools
if profile == "scale-down":
for pool_name, p in even_pools.items():
final_ratio, pool_pg_target, final_pg_target = self.autoscaler._calc_final_pg_target(p, pool_name, root_map,
- p['root_id'], p['capacity_ratio'], even_pools, bias, False, profile)
+ p['root_id'], p['capacity_ratio'], even_pools, bias, False, profile)
assert p['expected_final_pg_target'] == final_pg_target
assert p['expected_final_ratio'] == final_ratio
def test_all_even_pools_scale_up(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 0.2,
- "even_pools": True,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 0.2,
- "even_pools": True,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 0.2,
- "even_pools": True,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 32,
- "expected_final_ratio": 0.1,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 0.2,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
root_map = {
- "0": RootMapItem(4, 400, 400),
- "1": RootMapItem(4, 400, 400),
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
- }
+ }
profile = "scale-up"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
def test_all_even_pools_scale_down(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 128,
- "expected_final_ratio": 0.25,
- "even_pools": True,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 128,
- "expected_final_ratio": 0.25,
- "even_pools": True,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.2,
- "root_id":"0",
- "expected_final_pg_target": 128,
- "expected_final_ratio": 0.25,
- "even_pools": True,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 128,
- "expected_final_ratio": 0.25,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.25,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.25,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.2,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.25,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 128,
+ "expected_final_ratio": 0.25,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
root_map = {
- "0": RootMapItem(4, 400, 400),
- "1": RootMapItem(4, 400, 400),
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
- }
+ }
- profile = "scale-down"
+ profile = "scale-down"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
def test_uneven_pools_scale_up(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id":"0",
- "expected_final_pg_target": 32,
- "expected_final_ratio": 0.1,
- "even_pools": True,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.5,
- "root_id":"0",
- "expected_final_pg_target": 256,
- "expected_final_ratio": 0.5,
- "even_pools": False,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id":"0",
- "expected_final_pg_target": 32,
- "expected_final_ratio": 0.1,
- "even_pools": True,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 32,
- "expected_final_ratio": 0.1,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 256,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 32,
+ "expected_final_ratio": 0.1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
root_map = {
- "0": RootMapItem(4, 400, 400),
- "1": RootMapItem(4, 400, 400),
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
- }
+ }
profile = "scale-up"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
def test_uneven_pools_scale_down(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id":"0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 1/3,
- "even_pools": True,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.5,
- "root_id":"0",
- "expected_final_pg_target": 256,
- "expected_final_ratio": 0.5,
- "even_pools": False,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id":"0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 1/3,
- "even_pools": True,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 64,
- "expected_final_ratio": 1/3,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 1/3,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 256,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 1/3,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 64,
+ "expected_final_ratio": 1/3,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
root_map = {
- "0": RootMapItem(4, 400, 400),
- "1": RootMapItem(4, 400, 400),
+ 0: RootMapItem(4, 400, 400),
+ 1: RootMapItem(4, 400, 400),
- }
+ }
- profile = "scale-down"
+ profile = "scale-down"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
def test_uneven_pools_with_diff_roots_scale_up(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.4,
- "root_id":"0",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.4,
- "even_pools": False,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.6,
- "root_id":"1",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.6,
- "even_pools": False,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.5,
- "root_id":"0",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.5,
- "even_pools": False,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 512,
- "expected_final_ratio": 0.1,
- "even_pools": True,
- "size": 1,
- },
-
- "test4":{
-
- "pool": 4,
- "pool_name": "test4",
- "pg_num_target": 32,
- "capacity_ratio": 0.4,
- "root_id": "1",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.4,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 512,
+ "expected_final_ratio": 0.1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test4": {
+
+ "pool": 4,
+ "pool_name": "test4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
root_map = {
- "0": RootMapItem(3, 5000, 5000),
- "1": RootMapItem(2, 5000, 5000),
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
- }
+ }
profile = "scale-up"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
def test_uneven_pools_with_diff_roots_scale_down(self):
pools = {
- "test0":{
-
- "pool": 0,
- "pool_name": "test0",
- "pg_num_target": 32,
- "capacity_ratio": 0.4,
- "root_id":"0",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.4,
- "even_pools": False,
- "size": 1,
- },
-
- "test1":{
-
- "pool": 1,
- "pool_name": "test1",
- "pg_num_target": 32,
- "capacity_ratio": 0.6,
- "root_id":"1",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.6,
- "even_pools": False,
- "size": 1,
- },
-
- "test2":{
-
- "pool": 2,
- "pool_name": "test2",
- "pg_num_target": 32,
- "capacity_ratio": 0.5,
- "root_id":"0",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 0.5,
- "even_pools": False,
- "size": 1,
- },
-
- "test3":{
-
- "pool": 3,
- "pool_name": "test3",
- "pg_num_target": 32,
- "capacity_ratio": 0.1,
- "root_id": "0",
- "expected_final_pg_target": 512,
- "expected_final_ratio": 1,
- "even_pools": True,
- "size": 1,
- },
-
- "test4":{
-
- "pool": 4,
- "pool_name": "test4",
- "pg_num_target": 32,
- "capacity_ratio": 0.4,
- "root_id": "1",
- "expected_final_pg_target": 2048,
- "expected_final_ratio": 1,
- "even_pools": True,
- "size": 1,
- },
-
- }
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 512,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ "test4": {
+
+ "pool": 4,
+ "pool_name": "test4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": False,
+ },
+
+ }
+
+ root_map = {
+
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
+
+ }
+
+ profile = "scale-down"
+ bias = 1
+ overlapped_roots = set()
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
+
+ def test_uneven_pools_with_overllaped_roots_scale_down(self):
+ pools = {
+
+ "test0": {
+
+ "pool": 0,
+ "pool_name": "test0",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.4,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test1": {
+
+ "pool": 1,
+ "pool_name": "test1",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.6,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.6,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test2": {
+
+ "pool": 2,
+ "pool_name": "test2",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.5,
+ "root_id": 0,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 0.5,
+ "even_pools": False,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test3": {
+
+ "pool": 3,
+ "pool_name": "test3",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.1,
+ "root_id": 0,
+ "expected_final_pg_target": 512,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ "test4": {
+
+ "pool": 4,
+ "pool_name": "test4",
+ "pg_num_target": 32,
+ "capacity_ratio": 0.4,
+ "root_id": 1,
+ "expected_final_pg_target": 2048,
+ "expected_final_ratio": 1,
+ "even_pools": True,
+ "size": 1,
+ "no_scale": True,
+ },
+
+ }
root_map = {
- "0": RootMapItem(3, 5000, 5000),
- "1": RootMapItem(2, 5000, 5000),
+ 0: RootMapItem(3, 5000, 5000),
+ 1: RootMapItem(2, 5000, 5000),
- }
+ }
profile = "scale-down"
bias = 1
- self.helper_test(pools, root_map, bias, profile)
+ overlapped_roots = {0, 1}
+ self.helper_test(pools, root_map, bias, profile, overlapped_roots)
from pg_autoscaler import effective_target_ratio
from pytest import approx
+
def check_simple_ratio(target_ratio, tot_ratio):
etr = effective_target_ratio(target_ratio, tot_ratio, 0, 0)
assert (target_ratio / tot_ratio) == approx(etr)
return etr
+
def test_simple():
etr1 = check_simple_ratio(0.2, 0.9)
etr2 = check_simple_ratio(2, 9)
etr2 = check_simple_ratio(0.5, 1.0)
assert etr1 == approx(etr2)
+
def test_total_bytes():
etr = effective_target_ratio(1, 10, 5, 10)
assert etr == approx(0.05)
--- /dev/null
+# python unit test
+import unittest
+from tests import mock
+import pytest
+import json
+from pg_autoscaler import module
+
+
+class OSDMAP:
+ def __init__(self, pools):
+ self.pools = pools
+
+ def get_pools(self):
+ return self.pools
+
+ def pool_raw_used_rate(pool_id):
+ return 1
+
+
+class CRUSH:
+ def __init__(self, rules, osd_dic):
+ self.rules = rules
+ self.osd_dic = osd_dic
+
+ def get_rule_by_id(self, rule_id):
+ for rule in self.rules:
+ if rule['rule_id'] == rule_id:
+ return rule
+
+ return None
+
+ def get_rule_root(self, rule_name):
+ for rule in self.rules:
+ if rule['rule_name'] == rule_name:
+ return rule['root_id']
+
+ return None
+
+ def get_osds_under(self, root_id):
+ return self.osd_dic[root_id]
+
+
+class TestPgAutoscaler(object):
+
+ def setup(self):
+ # a bunch of attributes for testing.
+ self.autoscaler = module.PgAutoscaler('module_name', 0, 0)
+
+ def helper_test(self, osd_dic, rules, pools, expected_overlapped_roots):
+ result = {}
+ roots = []
+ overlapped_roots = set()
+ osdmap = OSDMAP(pools)
+ crush = CRUSH(rules, osd_dic)
+ roots, overlapped_roots = self.autoscaler.identify_subtrees_and_overlaps(osdmap,
+ crush, result, overlapped_roots, roots)
+ assert overlapped_roots == expected_overlapped_roots
+
+ def test_subtrees_and_overlaps(self):
+ osd_dic = {
+ -1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ -40: [11, 12, 13, 14, 15],
+ -5: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ }
+
+ rules = [
+ {
+ "rule_id": 0,
+ "rule_name": "data",
+ "ruleset": 0,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -1,
+ },
+ {
+ "rule_id": 1,
+ "rule_name": "teuthology-data-ec",
+ "ruleset": 1,
+ "type": 3,
+ "min_size": 3,
+ "max_size": 6,
+ "root_id": -5,
+ },
+ {
+ "rule_id": 4,
+ "rule_name": "rep-ssd",
+ "ruleset": 4,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -40,
+ },
+ ]
+ pools = {
+ 0: {
+ "pool_name": "data",
+ "pg_num_target": 1024,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.1624,
+ "options": {
+ "pg_num_min": 1024,
+ },
+ "expected_final_pg_target": 1024,
+ },
+ 1: {
+ "pool_name": "metadata",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0144,
+ "options": {
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 64,
+ },
+ 4: {
+ "pool_name": "libvirt-pool",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0001,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 93: {
+ "pool_name": ".rgw.root",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 94: {
+ "pool_name": "default.rgw.control",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 95: {
+ "pool_name": "default.rgw.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 96: {
+ "pool_name": "default.rgw.log",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 97: {
+ "pool_name": "default.rgw.buckets.index",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0002,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 98: {
+ "pool_name": "default.rgw.buckets.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0457,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 99: {
+ "pool_name": "default.rgw.buckets.non-ec",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 100: {
+ "pool_name": "device_health_metrics",
+ "pg_num_target": 1,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {
+ "pg_num_min": 1
+ },
+ "expected_final_pg_target": 1,
+ },
+ 113: {
+ "pool_name": "cephfs.teuthology.meta",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.1389,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 512,
+ },
+ 114: {
+ "pool_name": "cephfs.teuthology.data",
+ "pg_num_target": 256,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0006,
+ "options": {
+ "pg_num_min": 128,
+ },
+ "expected_final_pg_target": 1024,
+ "expected_final_pg_target": 256,
+ },
+ 117: {
+ "pool_name": "cephfs.scratch.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0027,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 16,
+ },
+ "expected_final_pg_target": 64,
+ },
+ 118: {
+ "pool_name": "cephfs.scratch.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0027,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 119: {
+ "pool_name": "cephfs.teuthology.data-ec",
+ "pg_num_target": 1024,
+ "size": 6,
+ "crush_rule": 1,
+ "capacity_ratio": 0.8490,
+ "options": {
+ "pg_num_min": 1024
+ },
+ "expected_final_pg_target": 1024,
+ },
+ 121: {
+ "pool_name": "cephsqlite",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ }
+ expected_overlapped_roots = {-40, -1, -5}
+ self.helper_test(osd_dic, rules, pools, expected_overlapped_roots)
+
+ def test_no_overlaps(self):
+ osd_dic = {
+ -1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ -40: [11, 12, 13, 14, 15],
+ -5: [16, 17, 18],
+ }
+
+ rules = [
+ {
+ "rule_id": 0,
+ "rule_name": "data",
+ "ruleset": 0,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -1,
+ },
+ {
+ "rule_id": 1,
+ "rule_name": "teuthology-data-ec",
+ "ruleset": 1,
+ "type": 3,
+ "min_size": 3,
+ "max_size": 6,
+ "root_id": -5,
+ },
+ {
+ "rule_id": 4,
+ "rule_name": "rep-ssd",
+ "ruleset": 4,
+ "type": 1,
+ "min_size": 1,
+ "max_size": 10,
+ "root_id": -40,
+ },
+ ]
+ pools = {
+ 0: {
+ "pool_name": "data",
+ "pg_num_target": 1024,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.1624,
+ "options": {
+ "pg_num_min": 1024,
+ },
+ "expected_final_pg_target": 1024,
+ },
+ 1: {
+ "pool_name": "metadata",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0144,
+ "options": {
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 64,
+ },
+ 4: {
+ "pool_name": "libvirt-pool",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0001,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 93: {
+ "pool_name": ".rgw.root",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 94: {
+ "pool_name": "default.rgw.control",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 95: {
+ "pool_name": "default.rgw.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 96: {
+ "pool_name": "default.rgw.log",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 97: {
+ "pool_name": "default.rgw.buckets.index",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0002,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 98: {
+ "pool_name": "default.rgw.buckets.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0457,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 99: {
+ "pool_name": "default.rgw.buckets.non-ec",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 32,
+ },
+ 100: {
+ "pool_name": "device_health_metrics",
+ "pg_num_target": 1,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0,
+ "options": {
+ "pg_num_min": 1
+ },
+ "expected_final_pg_target": 1,
+ },
+ 113: {
+ "pool_name": "cephfs.teuthology.meta",
+ "pg_num_target": 64,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.1389,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 64,
+ },
+ "expected_final_pg_target": 512,
+ },
+ 114: {
+ "pool_name": "cephfs.teuthology.data",
+ "pg_num_target": 256,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0006,
+ "options": {
+ "pg_num_min": 128,
+ },
+ "expected_final_pg_target": 1024,
+ "expected_final_pg_target": 256,
+ },
+ 117: {
+ "pool_name": "cephfs.scratch.meta",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0.0027,
+ "options": {
+ "pg_autoscale_bias": 4,
+ "pg_num_min": 16,
+ },
+ "expected_final_pg_target": 64,
+ },
+ 118: {
+ "pool_name": "cephfs.scratch.data",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 0,
+ "capacity_ratio": 0.0027,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ 119: {
+ "pool_name": "cephfs.teuthology.data-ec",
+ "pg_num_target": 1024,
+ "size": 6,
+ "crush_rule": 1,
+ "capacity_ratio": 0.8490,
+ "options": {
+ "pg_num_min": 1024
+ },
+ "expected_final_pg_target": 1024,
+ },
+ 121: {
+ "pool_name": "cephsqlite",
+ "pg_num_target": 32,
+ "size": 3,
+ "crush_rule": 4,
+ "capacity_ratio": 0,
+ "options": {},
+ "expected_final_pg_target": 128,
+ },
+ }
+ expected_overlapped_roots = set()
+ self.helper_test(osd_dic, rules, pools, expected_overlapped_roots)