diff --git a/doc/source/config/container_server_config.rst b/doc/source/config/container_server_config.rst index 1684acfcaf..7961f50e8f 100644 --- a/doc/source/config/container_server_config.rst +++ b/doc/source/config/container_server_config.rst @@ -329,6 +329,18 @@ rows_per_shard 500000 This defines the initial containers. The default is shard_container_threshold // 2. +minimum_shard_size 100000 Minimum size of the final + shard range. If this is + greater than one then the + final shard range may be + extended to more than + rows_per_shard in order + to avoid a further shard + range with less than + minimum_shard_size rows. + The default value is + rows_per_shard // 5. + shrink_threshold This defines the object count below which a 'donor' shard container diff --git a/etc/container-server.conf-sample b/etc/container-server.conf-sample index 262010e429..8ae1a71db5 100644 --- a/etc/container-server.conf-sample +++ b/etc/container-server.conf-sample @@ -374,6 +374,12 @@ use = egg:swift#xprofile # default is shard_container_threshold // 2 # rows_per_shard = 500000 # +# Minimum size of the final shard range. If this is greater than one then the +# final shard range may be extended to more than rows_per_shard in order to +# avoid a further shard range with less than minimum_shard_size rows. The +# default value is rows_per_shard // 5. +# minimum_shard_size = 100000 +# # When auto-sharding is enabled shrink_threshold defines the object count # below which a 'donor' shard container will be considered for shrinking into # another 'acceptor' shard container. The default is determined by diff --git a/swift/cli/manage_shard_ranges.py b/swift/cli/manage_shard_ranges.py index 9ecdcf1a6d..fc5db89bc7 100644 --- a/swift/cli/manage_shard_ranges.py +++ b/swift/cli/manage_shard_ranges.py @@ -301,7 +301,8 @@ def _find_ranges(broker, args, status_file=None): start = last_report = time.time() limit = 5 if status_file else -1 shard_data, last_found = broker.find_shard_ranges( - args.rows_per_shard, limit=limit) + args.rows_per_shard, limit=limit, + minimum_shard_size=args.minimum_shard_size) if shard_data: while not last_found: if last_report + 10 < time.time(): @@ -311,7 +312,8 @@ def _find_ranges(broker, args, status_file=None): # prefix doesn't matter since we aren't persisting it found_ranges = make_shard_ranges(broker, shard_data, '.shards_') more_shard_data, last_found = broker.find_shard_ranges( - args.rows_per_shard, existing_ranges=found_ranges, limit=5) + args.rows_per_shard, existing_ranges=found_ranges, limit=5, + minimum_shard_size=args.minimum_shard_size) shard_data.extend(more_shard_data) return shard_data, time.time() - start @@ -709,6 +711,13 @@ def _add_find_args(parser): 'Default is half of the shard_container_threshold value if that is ' 'given in a conf file specified with --config, otherwise %s.' % DEFAULT_SHARDER_CONF['rows_per_shard']) + parser.add_argument( + '--minimum-shard-size', type=_positive_int, + default=USE_SHARDER_DEFAULT, + help='Minimum size of the final shard range. If this is greater than ' + 'one then the final shard range may be extended to more than ' + 'rows_per_shard in order to avoid a further shard range with less ' + 'than minimum-shard-size rows.') def _add_replace_args(parser): @@ -906,18 +915,18 @@ def main(cli_args=None): return EXIT_INVALID_ARGS try: - # load values from conf file or sharder defaults conf = {} if args.conf_file: conf = readconf(args.conf_file, 'container-sharder') + conf.update(dict((k, v) for k, v in vars(args).items() + if v != USE_SHARDER_DEFAULT)) conf_args = ContainerSharderConf(conf) except (OSError, IOError) as exc: print('Error opening config file %s: %s' % (args.conf_file, exc), file=sys.stderr) return EXIT_ERROR except (TypeError, ValueError) as exc: - print('Error loading config file %s: %s' % (args.conf_file, exc), - file=sys.stderr) + print('Error loading config: %s' % exc, file=sys.stderr) return EXIT_INVALID_ARGS for k, v in vars(args).items(): diff --git a/swift/container/backend.py b/swift/container/backend.py index 334f578fbc..0e062e9429 100644 --- a/swift/container/backend.py +++ b/swift/container/backend.py @@ -2234,7 +2234,8 @@ class ContainerBroker(DatabaseBroker): row = connection.execute(sql, args).fetchone() return row['name'] if row else None - def find_shard_ranges(self, shard_size, limit=-1, existing_ranges=None): + def find_shard_ranges(self, shard_size, limit=-1, existing_ranges=None, + minimum_shard_size=1): """ Scans the container db for shard ranges. Scanning will start at the upper bound of the any ``existing_ranges`` that are given, otherwise @@ -2253,6 +2254,10 @@ class ContainerBroker(DatabaseBroker): given, this list should be sorted in order of upper bounds; the scan for new shard ranges will start at the upper bound of the last existing ShardRange. + :param minimum_shard_size: Minimum size of the final shard range. If + this is greater than one then the final shard range may be extended + to more than shard_size in order to avoid a further shard range + with less minimum_shard_size rows. :return: a tuple; the first value in the tuple is a list of dicts each having keys {'index', 'lower', 'upper', 'object_count'} in order of ascending 'upper'; the second value in the tuple is a @@ -2260,8 +2265,9 @@ class ContainerBroker(DatabaseBroker): otherwise. """ existing_ranges = existing_ranges or [] + minimum_shard_size = max(minimum_shard_size, 1) object_count = self.get_info().get('object_count', 0) - if shard_size >= object_count: + if shard_size + minimum_shard_size > object_count: # container not big enough to shard return [], False @@ -2292,9 +2298,10 @@ class ContainerBroker(DatabaseBroker): sub_broker = self.get_brokers()[0] index = len(existing_ranges) while limit is None or limit < 0 or len(found_ranges) < limit: - if progress + shard_size >= object_count: - # next shard point is at or beyond final object name so don't - # bother with db query + if progress + shard_size + minimum_shard_size > object_count: + # next shard point is within minimum_size rows of the final + # object name, or beyond it, so don't bother with db query. + # This shard will have <= shard_size + (minimum_size - 1) rows. next_shard_upper = None else: try: diff --git a/swift/container/sharder.py b/swift/container/sharder.py index a56fa1ac00..4a4bf68db1 100644 --- a/swift/container/sharder.py +++ b/swift/container/sharder.py @@ -630,10 +630,23 @@ class ContainerSharderConf(object): self.rows_per_shard = get_val( 'rows_per_shard', config_positive_int_value, max(self.shard_container_threshold // 2, 1)) + self.minimum_shard_size = get_val( + 'minimum_shard_size', config_positive_int_value, + max(self.rows_per_shard // 5, 1)) + + self.validate_conf() def percent_of_threshold(self, val): return int(config_percent_value(val) * self.shard_container_threshold) + def validate_conf(self): + keys = ('minimum_shard_size', 'rows_per_shard') + vals = [getattr(self, key) for key in keys] + if not vals[0] <= vals[1]: + raise ValueError( + '%s (%d) must be less than %s (%d)' + % (keys[0], vals[0], keys[1], vals[1])) + DEFAULT_SHARDER_CONF = vars(ContainerSharderConf()) @@ -1482,7 +1495,8 @@ class ContainerSharder(ContainerSharderConf, ContainerReplicator): start = time.time() shard_data, last_found = broker.find_shard_ranges( self.rows_per_shard, limit=self.shard_scanner_batch_size, - existing_ranges=shard_ranges) + existing_ranges=shard_ranges, + minimum_shard_size=self.minimum_shard_size) elapsed = time.time() - start if not shard_data: diff --git a/test/probe/test_sharder.py b/test/probe/test_sharder.py index 2e8647c21f..f702a73b23 100644 --- a/test/probe/test_sharder.py +++ b/test/probe/test_sharder.py @@ -2789,7 +2789,7 @@ class TestManagedContainerSharding(BaseTestContainerSharding): self.sharders.once(**kwargs) def test_manage_shard_ranges(self): - obj_names = self._make_object_names(4) + obj_names = self._make_object_names(7) self.put_objects(obj_names) client.post_container(self.url, self.admin_token, self.container_name, @@ -2807,7 +2807,7 @@ class TestManagedContainerSharding(BaseTestContainerSharding): self.assert_subprocess_success([ 'swift-manage-shard-ranges', self.get_db_file(self.brain.part, self.brain.nodes[0]), - 'find_and_replace', '2', '--enable']) + 'find_and_replace', '3', '--enable', '--minimum-shard-size', '2']) self.assert_container_state(self.brain.nodes[0], 'unsharded', 2) # "Run container-replicator to replicate them to other nodes." diff --git a/test/unit/cli/test_manage_shard_ranges.py b/test/unit/cli/test_manage_shard_ranges.py index 79e84251f0..439295cf80 100644 --- a/test/unit/cli/test_manage_shard_ranges.py +++ b/test/unit/cli/test_manage_shard_ranges.py @@ -142,6 +142,7 @@ class TestManageShardRanges(unittest.TestCase): rows_per_shard = 600 max_shrinking = 33 max_expanding = 31 + minimum_shard_size = 88 """ conf_file = os.path.join(self.testdir, 'sharder.conf') @@ -159,7 +160,8 @@ class TestManageShardRanges(unittest.TestCase): rows_per_shard=500000, subcommand='find', force_commits=False, - verbose=0) + verbose=0, + minimum_shard_size=100000) mocked.assert_called_once_with(mock.ANY, expected) # conf file @@ -173,13 +175,15 @@ class TestManageShardRanges(unittest.TestCase): rows_per_shard=600, subcommand='find', force_commits=False, - verbose=0) + verbose=0, + minimum_shard_size=88) mocked.assert_called_once_with(mock.ANY, expected) # cli options override conf file with mock.patch('swift.cli.manage_shard_ranges.find_ranges', return_value=0) as mocked: - ret = main([db_file, '--config', conf_file, 'find', '12345']) + ret = main([db_file, '--config', conf_file, 'find', '12345', + '--minimum-shard-size', '99']) self.assertEqual(0, ret) expected = Namespace(conf_file=conf_file, path_to_file=mock.ANY, @@ -187,7 +191,8 @@ class TestManageShardRanges(unittest.TestCase): rows_per_shard=12345, subcommand='find', force_commits=False, - verbose=0) + verbose=0, + minimum_shard_size=99) mocked.assert_called_once_with(mock.ANY, expected) # default values @@ -377,7 +382,7 @@ class TestManageShardRanges(unittest.TestCase): ret = main([db_file, '--config', conf_file, 'compact']) self.assertEqual(2, ret) err_lines = err.getvalue().split('\n') - self.assert_starts_with(err_lines[0], 'Error loading config file') + self.assert_starts_with(err_lines[0], 'Error loading config') self.assertIn('shard_container_threshold', err_lines[0]) def test_conf_file_invalid_deprecated_options(self): @@ -403,7 +408,7 @@ class TestManageShardRanges(unittest.TestCase): with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): main([db_file, '--config', conf_file, 'compact']) err_lines = err.getvalue().split('\n') - self.assert_starts_with(err_lines[0], 'Error loading config file') + self.assert_starts_with(err_lines[0], 'Error loading config') self.assertIn('shard_shrink_point', err_lines[0]) def test_conf_file_does_not_exist(self): @@ -457,7 +462,7 @@ class TestManageShardRanges(unittest.TestCase): out = StringIO() err = StringIO() with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): - ret = main([db_file, 'find', '99']) + ret = main([db_file, 'find', '99', '--minimum-shard-size', '1']) self.assertEqual(0, ret) self.assert_formatted_json(out.getvalue(), [ {'index': 0, 'lower': '', 'upper': 'obj98', 'object_count': 99}, @@ -496,6 +501,91 @@ class TestManageShardRanges(unittest.TestCase): self.assert_starts_with(err_lines[0], 'Loaded db broker for ') self.assert_starts_with(err_lines[1], 'Found 10 ranges in ') + def test_find_shard_ranges_with_minimum_size(self): + db_file = os.path.join(self.testdir, 'hash.db') + broker = ContainerBroker(db_file) + broker.account = 'a' + broker.container = 'c' + broker.initialize() + ts = utils.Timestamp.now() + # with 105 objects and rows_per_shard = 50 there is the potential for a + # tail shard of size 5 + broker.merge_items([ + {'name': 'obj%03d' % i, 'created_at': ts.internal, 'size': 0, + 'content_type': 'application/octet-stream', 'etag': 'not-really', + 'deleted': 0, 'storage_policy_index': 0, + 'ctype_timestamp': ts.internal, 'meta_timestamp': ts.internal} + for i in range(105)]) + + def assert_tail_shard_not_extended(minimum): + out = StringIO() + err = StringIO() + with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): + ret = main([db_file, 'find', '50', + '--minimum-shard-size', str(minimum)]) + self.assertEqual(0, ret) + self.assert_formatted_json(out.getvalue(), [ + {'index': 0, 'lower': '', 'upper': 'obj049', + 'object_count': 50}, + {'index': 1, 'lower': 'obj049', 'upper': 'obj099', + 'object_count': 50}, + {'index': 2, 'lower': 'obj099', 'upper': '', + 'object_count': 5}, + ]) + err_lines = err.getvalue().split('\n') + self.assert_starts_with(err_lines[0], 'Loaded db broker for ') + self.assert_starts_with(err_lines[1], 'Found 3 ranges in ') + + # tail shard size > minimum + assert_tail_shard_not_extended(1) + assert_tail_shard_not_extended(4) + assert_tail_shard_not_extended(5) + + def assert_tail_shard_extended(minimum): + out = StringIO() + err = StringIO() + if minimum is not None: + extra_args = ['--minimum-shard-size', str(minimum)] + else: + extra_args = [] + with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): + ret = main([db_file, 'find', '50'] + extra_args) + self.assertEqual(0, ret) + err_lines = err.getvalue().split('\n') + self.assert_formatted_json(out.getvalue(), [ + {'index': 0, 'lower': '', 'upper': 'obj049', + 'object_count': 50}, + {'index': 1, 'lower': 'obj049', 'upper': '', + 'object_count': 55}, + ]) + self.assert_starts_with(err_lines[1], 'Found 2 ranges in ') + self.assert_starts_with(err_lines[0], 'Loaded db broker for ') + + # sanity check - no minimum specified, defaults to rows_per_shard/5 + assert_tail_shard_extended(None) + assert_tail_shard_extended(6) + assert_tail_shard_extended(50) + + def assert_too_large_value_handled(minimum): + out = StringIO() + err = StringIO() + with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): + ret = main([db_file, 'find', '50', + '--minimum-shard-size', str(minimum)]) + self.assertEqual(2, ret) + self.assertEqual( + 'Error loading config: minimum_shard_size (%s) must be less ' + 'than rows_per_shard (50)' % minimum, err.getvalue().strip()) + + assert_too_large_value_handled(51) + assert_too_large_value_handled(52) + + out = StringIO() + err = StringIO() + with mock.patch('sys.stdout', out), mock.patch('sys.stderr', err): + with self.assertRaises(SystemExit): + main([db_file, 'find', '50', '--minimum-shard-size', '-1']) + def test_info(self): broker = self._make_broker() broker.update_metadata({'X-Container-Sysmeta-Sharding': diff --git a/test/unit/container/test_backend.py b/test/unit/container/test_backend.py index baa5a07874..85d07d0a97 100644 --- a/test/unit/container/test_backend.py +++ b/test/unit/container/test_backend.py @@ -4340,7 +4340,7 @@ class TestContainerBroker(unittest.TestCase): container_name = 'test_container' def do_test(expected_bounds, expected_last_found, shard_size, limit, - start_index=0, existing=None): + start_index=0, existing=None, minimum_size=1): # expected_bounds is a list of tuples (lower, upper, object_count) # build expected shard ranges expected_shard_ranges = [ @@ -4352,7 +4352,8 @@ class TestContainerBroker(unittest.TestCase): with mock.patch('swift.common.utils.time.time', return_value=float(ts_now.normal)): ranges, last_found = broker.find_shard_ranges( - shard_size, limit=limit, existing_ranges=existing) + shard_size, limit=limit, existing_ranges=existing, + minimum_shard_size=minimum_size) self.assertEqual(expected_shard_ranges, ranges) self.assertEqual(expected_last_found, last_found) @@ -4397,6 +4398,20 @@ class TestContainerBroker(unittest.TestCase): do_test(expected, True, shard_size=4, limit=4) do_test(expected, True, shard_size=4, limit=-1) + # check use of minimum_shard_size + expected = [(c_lower, 'obj03', 4), ('obj03', 'obj07', 4), + ('obj07', c_upper, 2)] + do_test(expected, True, shard_size=4, limit=None, minimum_size=2) + # crazy values ignored... + do_test(expected, True, shard_size=4, limit=None, minimum_size=0) + do_test(expected, True, shard_size=4, limit=None, minimum_size=-1) + # minimum_size > potential final shard + expected = [(c_lower, 'obj03', 4), ('obj03', c_upper, 6)] + do_test(expected, True, shard_size=4, limit=None, minimum_size=3) + # extended shard size >= object_count + do_test([], False, shard_size=6, limit=None, minimum_size=5) + do_test([], False, shard_size=6, limit=None, minimum_size=500) + # increase object count to 11 broker.put_object( 'obj10', next(self.ts).internal, 0, 'text/plain', 'etag') diff --git a/test/unit/container/test_sharder.py b/test/unit/container/test_sharder.py index 79ca866ede..07647c8023 100644 --- a/test/unit/container/test_sharder.py +++ b/test/unit/container/test_sharder.py @@ -4222,6 +4222,7 @@ class TestSharder(BaseTestSharder): broker, objects = self._setup_old_style_find_ranges( account, cont, lower, upper) with self._mock_sharder(conf={'shard_container_threshold': 199, + 'minimum_shard_size': 1, 'auto_create_account_prefix': '.int_'} ) as sharder: with mock_timestamp_now() as now: @@ -4237,6 +4238,7 @@ class TestSharder(BaseTestSharder): # second invocation finds none with self._mock_sharder(conf={'shard_container_threshold': 199, + 'minimum_shard_size': 1, 'auto_create_account_prefix': '.int_'} ) as sharder: num_found = sharder._find_shard_ranges(broker) @@ -4317,10 +4319,11 @@ class TestSharder(BaseTestSharder): self._assert_shard_ranges_equal(expected_ranges, broker.get_shard_ranges()) - # first invocation finds both ranges + # first invocation finds both ranges, sizes 99 and 1 broker, objects = self._setup_find_ranges( account, cont, lower, upper) with self._mock_sharder(conf={'shard_container_threshold': 199, + 'minimum_shard_size': 1, 'auto_create_account_prefix': '.int_'} ) as sharder: with mock_timestamp_now() as now: @@ -4371,9 +4374,11 @@ class TestSharder(BaseTestSharder): now, objects[89][0], upper, 10), ] # first invocation finds 2 ranges + # (third shard range will be > minimum_shard_size) with self._mock_sharder( conf={'shard_container_threshold': 90, - 'shard_scanner_batch_size': 2}) as sharder: + 'shard_scanner_batch_size': 2, + 'minimum_shard_size': 10}) as sharder: with mock_timestamp_now(now): num_found = sharder._find_shard_ranges(broker) self.assertEqual(45, sharder.rows_per_shard) @@ -4388,8 +4393,9 @@ class TestSharder(BaseTestSharder): self.assertGreaterEqual(stats['max_time'], stats['min_time']) # second invocation finds third shard range - with self._mock_sharder(conf={'shard_container_threshold': 199, - 'shard_scanner_batch_size': 2} + with self._mock_sharder(conf={'shard_container_threshold': 90, + 'shard_scanner_batch_size': 2, + 'minimum_shard_size': 10} ) as sharder: with mock_timestamp_now(now): num_found = sharder._find_shard_ranges(broker) @@ -4405,7 +4411,8 @@ class TestSharder(BaseTestSharder): # third invocation finds none with self._mock_sharder(conf={'shard_container_threshold': 199, - 'shard_scanner_batch_size': 2} + 'shard_scanner_batch_size': 2, + 'minimum_shard_size': 10} ) as sharder: sharder._send_shard_ranges = mock.MagicMock(return_value=True) num_found = sharder._find_shard_ranges(broker) @@ -4425,6 +4432,41 @@ class TestSharder(BaseTestSharder): def test_find_shard_ranges_finds_three_shard(self): self._check_find_shard_ranges_finds_three('.shards_a', 'c_', 'l', 'u') + def test_find_shard_ranges_with_minimum_size(self): + cont = 'c_' + lower = 'l' + upper = 'u' + broker, objects = self._setup_find_ranges( + '.shards_a', 'c_', lower, upper) + now = Timestamp.now() + expected_ranges = [ + ShardRange( + ShardRange.make_path('.shards_a', 'c', cont, now, 0), + now, lower, objects[44][0], 45), + ShardRange( + ShardRange.make_path('.shards_a', 'c', cont, now, 1), + now, objects[44][0], upper, 55), + ] + # first invocation finds 2 ranges - second has been extended to avoid + # final shard range < minimum_size + with self._mock_sharder( + conf={'shard_container_threshold': 90, + 'shard_scanner_batch_size': 2, + 'minimum_shard_size': 11}) as sharder: + with mock_timestamp_now(now): + num_found = sharder._find_shard_ranges(broker) + self.assertEqual(45, sharder.rows_per_shard) + self.assertEqual(11, sharder.minimum_shard_size) + self.assertEqual(2, num_found) + self.assertEqual(2, len(broker.get_shard_ranges())) + self._assert_shard_ranges_equal(expected_ranges[:2], + broker.get_shard_ranges()) + expected_stats = {'attempted': 1, 'success': 1, 'failure': 0, + 'found': 2, 'min_time': mock.ANY, + 'max_time': mock.ANY} + stats = self._assert_stats(expected_stats, sharder, 'scanned') + self.assertGreaterEqual(stats['max_time'], stats['min_time']) + def test_sharding_enabled(self): broker = self._make_broker() self.assertFalse(sharding_enabled(broker)) @@ -5561,13 +5603,14 @@ class TestSharder(BaseTestSharder): def test_find_and_enable_sharding_candidates_bootstrap(self): broker = self._make_broker() with self._mock_sharder( - conf={'shard_container_threshold': 1}) as sharder: + conf={'shard_container_threshold': 2}) as sharder: sharder._find_and_enable_sharding_candidates(broker) self.assertEqual(ShardRange.ACTIVE, broker.get_own_shard_range().state) - broker.put_object('obj', next(self.ts_iter).internal, 1, '', '') - self.assertEqual(1, broker.get_info()['object_count']) + broker.put_object('obj1', next(self.ts_iter).internal, 1, '', '') + broker.put_object('obj2', next(self.ts_iter).internal, 1, '', '') + self.assertEqual(2, broker.get_info()['object_count']) with self._mock_sharder( - conf={'shard_container_threshold': 1}) as sharder: + conf={'shard_container_threshold': 2}) as sharder: with mock_timestamp_now() as now: sharder._find_and_enable_sharding_candidates( broker, [broker.get_own_shard_range()]) @@ -5578,7 +5621,7 @@ class TestSharder(BaseTestSharder): # check idempotency with self._mock_sharder( - conf={'shard_container_threshold': 1}) as sharder: + conf={'shard_container_threshold': 2}) as sharder: with mock_timestamp_now(): sharder._find_and_enable_sharding_candidates( broker, [broker.get_own_shard_range()]) @@ -7284,7 +7327,8 @@ class TestContainerSharderConf(unittest.TestCase): 'auto_shard': False, 'shrink_threshold': 100000, 'expansion_limit': 750000, - 'rows_per_shard': 500000} + 'rows_per_shard': 500000, + 'minimum_shard_size': 100000} self.assertEqual(expected, vars(ContainerSharderConf())) self.assertEqual(expected, vars(ContainerSharderConf(None))) self.assertEqual(expected, DEFAULT_SHARDER_CONF) @@ -7303,7 +7347,8 @@ class TestContainerSharderConf(unittest.TestCase): 'auto_shard': True, 'shrink_threshold': 100001, 'expansion_limit': 750001, - 'rows_per_shard': 500001} + 'rows_per_shard': 500001, + 'minimum_shard_size': 20} expected = dict(conf) conf.update({'unexpected': 'option'}) self.assertEqual(expected, vars(ContainerSharderConf(conf))) @@ -7319,7 +7364,8 @@ class TestContainerSharderConf(unittest.TestCase): 'recon_candidates_limit': 6, 'recon_sharded_timeout': 43201, 'conn_timeout': 5.1, - 'auto_shard': True} + 'auto_shard': True, + 'minimum_shard_size': 1} # percent options work deprecated_conf = {'shard_shrink_point': 9, @@ -7357,7 +7403,8 @@ class TestContainerSharderConf(unittest.TestCase): 'shrink_threshold': not_int, 'expansion_limit': not_int, 'shard_shrink_point': not_percent, - 'shard_shrink_merge_point': not_percent} + 'shard_shrink_merge_point': not_percent, + 'minimum_shard_size': not_positive_int} for key, bad_values in bad.items(): for bad_value in bad_values: