/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.cluster.metadata; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.EmptyClusterInfoService; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator; import org.elasticsearch.cluster.routing.allocation.decider.AllocationDeciders; import org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.indices.InvalidIndexNameException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.gateway.TestGatewayAllocator; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import static java.util.Collections.emptyMap; import static java.util.Collections.min; import static org.hamcrest.Matchers.endsWith; public class MetaDataCreateIndexServiceTests extends ESTestCase { private ClusterState createClusterState(String name, int numShards, int numReplicas, Settings settings) { MetaData.Builder metaBuilder = MetaData.builder(); IndexMetaData indexMetaData = IndexMetaData.builder(name).settings(settings(Version.CURRENT) .put(settings)) .numberOfShards(numShards).numberOfReplicas(numReplicas).build(); metaBuilder.put(indexMetaData, false); MetaData metaData = metaBuilder.build(); RoutingTable.Builder routingTableBuilder = RoutingTable.builder(); routingTableBuilder.addAsNew(metaData.index(name)); RoutingTable routingTable = routingTableBuilder.build(); ClusterState clusterState = ClusterState.builder(org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING .getDefault(Settings.EMPTY)) .metaData(metaData).routingTable(routingTable).blocks(ClusterBlocks.builder().addBlocks(indexMetaData)).build(); return clusterState; } public static boolean isShrinkable(int source, int target) { int x = source / target; assert source > target : source + " <= " + target; return target * x == source; } public void testValidateShrinkIndex() { int numShards = randomIntBetween(2, 42); ClusterState state = createClusterState("source", numShards, randomIntBetween(0, 10), Settings.builder().put("index.blocks.write", true).build()); assertEquals("index [source] already exists", expectThrows(ResourceAlreadyExistsException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(state, "target", Collections.emptySet(), "source", Settings.EMPTY) ).getMessage()); assertEquals("no such index", expectThrows(IndexNotFoundException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(state, "no such index", Collections.emptySet(), "target", Settings.EMPTY) ).getMessage()); assertEquals("can't shrink an index with only one shard", expectThrows(IllegalArgumentException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(createClusterState("source", 1, 0, Settings.builder().put("index.blocks.write", true).build()), "source", Collections.emptySet(), "target", Settings.EMPTY) ).getMessage()); assertEquals("the number of target shards must be less that the number of source shards", expectThrows(IllegalArgumentException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(createClusterState("source", 5, 0, Settings.builder().put("index.blocks.write", true).build()), "source", Collections.emptySet(), "target", Settings.builder().put("index.number_of_shards", 10).build()) ).getMessage()); assertEquals("index source must be read-only to shrink index. use \"index.blocks.write=true\"", expectThrows(IllegalStateException.class, () -> MetaDataCreateIndexService.validateShrinkIndex( createClusterState("source", randomIntBetween(2, 100), randomIntBetween(0, 10), Settings.EMPTY) , "source", Collections.emptySet(), "target", Settings.EMPTY) ).getMessage()); assertEquals("index source must have all shards allocated on the same node to shrink index", expectThrows(IllegalStateException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(state, "source", Collections.emptySet(), "target", Settings.EMPTY) ).getMessage()); assertEquals("the number of source shards [8] must be a must be a multiple of [3]", expectThrows(IllegalArgumentException.class, () -> MetaDataCreateIndexService.validateShrinkIndex(createClusterState("source", 8, randomIntBetween(0, 10), Settings.builder().put("index.blocks.write", true).build()), "source", Collections.emptySet(), "target", Settings.builder().put("index.number_of_shards", 3).build()) ).getMessage()); assertEquals("mappings are not allowed when shrinking indices, all mappings are copied from the source index", expectThrows(IllegalArgumentException.class, () -> { MetaDataCreateIndexService.validateShrinkIndex(state, "source", Collections.singleton("foo"), "target", Settings.EMPTY); } ).getMessage()); // create one that won't fail ClusterState clusterState = ClusterState.builder(createClusterState("source", numShards, 0, Settings.builder().put("index.blocks.write", true).build())).nodes(DiscoveryNodes.builder().add(newNode("node1"))) .build(); AllocationService service = new AllocationService(Settings.builder().build(), new AllocationDeciders(Settings.EMPTY, Collections.singleton(new MaxRetryAllocationDecider(Settings.EMPTY))), new TestGatewayAllocator(), new BalancedShardsAllocator(Settings.EMPTY), EmptyClusterInfoService.INSTANCE); RoutingTable routingTable = service.reroute(clusterState, "reroute").routingTable(); clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); // now we start the shard routingTable = service.applyStartedShards(clusterState, routingTable.index("source").shardsWithState(ShardRoutingState.INITIALIZING)).routingTable(); clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); int targetShards; do { targetShards = randomIntBetween(1, numShards/2); } while (isShrinkable(numShards, targetShards) == false); MetaDataCreateIndexService.validateShrinkIndex(clusterState, "source", Collections.emptySet(), "target", Settings.builder().put("index.number_of_shards", targetShards).build()); } public void testShrinkIndexSettings() { String indexName = randomAlphaOfLength(10); List<Version> versions = Arrays.asList(VersionUtils.randomVersion(random()), VersionUtils.randomVersion(random()), VersionUtils.randomVersion(random())); versions.sort((l, r) -> Long.compare(l.id, r.id)); Version version = versions.get(0); Version minCompat = versions.get(1); Version upgraded = versions.get(2); // create one that won't fail ClusterState clusterState = ClusterState.builder(createClusterState(indexName, randomIntBetween(2, 10), 0, Settings.builder() .put("index.blocks.write", true) .put("index.similarity.default.type", "BM25") .put("index.version.created", version) .put("index.version.upgraded", upgraded) .put("index.version.minimum_compatible", minCompat.luceneVersion) .put("index.analysis.analyzer.my_analyzer.tokenizer", "keyword") .build())).nodes(DiscoveryNodes.builder().add(newNode("node1"))) .build(); AllocationService service = new AllocationService(Settings.builder().build(), new AllocationDeciders(Settings.EMPTY, Collections.singleton(new MaxRetryAllocationDecider(Settings.EMPTY))), new TestGatewayAllocator(), new BalancedShardsAllocator(Settings.EMPTY), EmptyClusterInfoService.INSTANCE); RoutingTable routingTable = service.reroute(clusterState, "reroute").routingTable(); clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); // now we start the shard routingTable = service.applyStartedShards(clusterState, routingTable.index(indexName).shardsWithState(ShardRoutingState.INITIALIZING)).routingTable(); clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); Settings.Builder builder = Settings.builder(); MetaDataCreateIndexService.prepareShrinkIndexSettings( clusterState, Collections.emptySet(), builder, clusterState.metaData().index(indexName).getIndex(), "target"); assertEquals("similarity settings must be copied", "BM25", builder.build().get("index.similarity.default.type")); assertEquals("analysis settings must be copied", "keyword", builder.build().get("index.analysis.analyzer.my_analyzer.tokenizer")); assertEquals("node1", builder.build().get("index.routing.allocation.initial_recovery._id")); assertEquals("1", builder.build().get("index.allocation.max_retries")); assertEquals(version, builder.build().getAsVersion("index.version.created", null)); assertEquals(upgraded, builder.build().getAsVersion("index.version.upgraded", null)); } private DiscoveryNode newNode(String nodeId) { return new DiscoveryNode(nodeId, buildNewFakeTransportAddress(), emptyMap(), Collections.unmodifiableSet(new HashSet<>(Arrays.asList(DiscoveryNode.Role.MASTER, DiscoveryNode.Role.DATA))), Version.CURRENT); } public void testValidateIndexName() throws Exception { validateIndexName("index?name", "must not contain the following characters " + Strings.INVALID_FILENAME_CHARS); validateIndexName("index#name", "must not contain '#'"); validateIndexName("_indexname", "must not start with '_', '-', or '+'"); validateIndexName("-indexname", "must not start with '_', '-', or '+'"); validateIndexName("+indexname", "must not start with '_', '-', or '+'"); validateIndexName("INDEXNAME", "must be lowercase"); validateIndexName("..", "must not be '.' or '..'"); } private void validateIndexName(String indexName, String errorMessage) { InvalidIndexNameException e = expectThrows(InvalidIndexNameException.class, () -> MetaDataCreateIndexService.validateIndexName(indexName, ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING .getDefault(Settings.EMPTY)).build())); assertThat(e.getMessage(), endsWith(errorMessage)); } }