/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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.apache.solr.cloud; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.lucene.util.LuceneTestCase; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.cloud.DocCollection; import org.apache.solr.common.cloud.Replica; import org.apache.solr.common.cloud.RoutingRule; import org.apache.solr.common.cloud.Slice; import org.apache.solr.util.TimeOut; import org.apache.zookeeper.KeeperException; import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @LuceneTestCase.Slow public class MigrateRouteKeyTest extends SolrCloudTestCase { @BeforeClass public static void setupCluster() throws Exception { configureCluster(2) .addConfig("conf", configset("cloud-minimal")) .configure(); if (usually()) { CollectionAdminRequest.setClusterProperty("legacyCloud", "false").process(cluster.getSolrClient()); log.info("Using legacyCloud=false for cluster"); } } private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private boolean waitForRuleToExpire(String collection, String shard, String splitKey, long finishTime) throws KeeperException, InterruptedException, SolrServerException, IOException { DocCollection state; Slice slice; boolean ruleRemoved = false; long expiryTime = finishTime + TimeUnit.NANOSECONDS.convert(60, TimeUnit.SECONDS); while (System.nanoTime() < expiryTime) { cluster.getSolrClient().getZkStateReader().forceUpdateCollection(collection); state = getCollectionState(collection); slice = state.getSlice(shard); Map<String,RoutingRule> routingRules = slice.getRoutingRules(); if (routingRules == null || routingRules.isEmpty() || !routingRules.containsKey(splitKey)) { ruleRemoved = true; break; } SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", splitKey + random().nextInt()); cluster.getSolrClient().add(collection, doc); Thread.sleep(1000); } return ruleRemoved; } protected void invokeCollectionMigration(CollectionAdminRequest.AsyncCollectionAdminRequest request) throws IOException, SolrServerException, InterruptedException { if (random().nextBoolean()) { cluster.getSolrClient().setSoTimeout(60000); // can take a while request.process(cluster.getSolrClient()); } else { request.processAndWait(cluster.getSolrClient(), 60000); } } @Test public void multipleShardMigrateTest() throws Exception { CollectionAdminRequest.createCollection("sourceCollection", "conf", 2, 1).process(cluster.getSolrClient()); cluster.getSolrClient().setDefaultCollection("sourceCollection"); final String splitKey = "a"; final int BIT_SEP = 1; final int[] splitKeyCount = new int[1]; for (int id = 0; id < 26*3; id++) { String shardKey = "" + (char) ('a' + (id % 26)); // See comment in ShardRoutingTest for hash distribution String key = shardKey; if (splitKey.equals(shardKey)) { key += "/" + BIT_SEP; // spread it over half the collection } SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", key + "!" + id); doc.addField("n_ti", id); cluster.getSolrClient().add("sourceCollection", doc); if (splitKey.equals(shardKey)) splitKeyCount[0]++; } assertTrue(splitKeyCount[0] > 0); String targetCollection = "migrate_multipleshardtest_targetCollection"; CollectionAdminRequest.createCollection(targetCollection, "conf", 1, 1).process(cluster.getSolrClient()); Indexer indexer = new Indexer(cluster.getSolrClient(), splitKey, 1, 30); indexer.start(); DocCollection state = getCollectionState(targetCollection); Replica replica = state.getReplicas().get(0); try (HttpSolrClient collectionClient = getHttpSolrClient(replica.getCoreUrl())) { SolrQuery solrQuery = new SolrQuery("*:*"); assertEquals("DocCount on target collection does not match", 0, collectionClient.query(solrQuery).getResults().getNumFound()); invokeCollectionMigration( CollectionAdminRequest.migrateData("sourceCollection", targetCollection, splitKey + "/" + BIT_SEP + "!") .setForwardTimeout(45)); long finishTime = System.nanoTime(); indexer.join(); splitKeyCount[0] += indexer.getSplitKeyCount(); try { cluster.getSolrClient().deleteById("a/" + BIT_SEP + "!104"); splitKeyCount[0]--; } catch (Exception e) { log.warn("Error deleting document a/" + BIT_SEP + "!104", e); } cluster.getSolrClient().commit(); collectionClient.commit(); solrQuery = new SolrQuery("*:*").setRows(1000); QueryResponse response = collectionClient.query(solrQuery); log.info("Response from target collection: " + response); assertEquals("DocCount on target collection does not match", splitKeyCount[0], response.getResults().getNumFound()); waitForState("Expected to find routing rule for split key " + splitKey, "sourceCollection", (n, c) -> { if (c == null) return false; Slice shard = c.getSlice("shard2"); if (shard == null) return false; if (shard.getRoutingRules() == null || shard.getRoutingRules().isEmpty()) return false; if (shard.getRoutingRules().get(splitKey + "!") == null) return false; return true; }); boolean ruleRemoved = waitForRuleToExpire("sourceCollection", "shard2", splitKey, finishTime); assertTrue("Routing rule was not expired", ruleRemoved); } } static class Indexer extends Thread { final int seconds; final CloudSolrClient cloudClient; final String splitKey; int splitKeyCount = 0; final int bitSep; public Indexer(CloudSolrClient cloudClient, String splitKey, int bitSep, int seconds) { this.seconds = seconds; this.cloudClient = cloudClient; this.splitKey = splitKey; this.bitSep = bitSep; } @Override public void run() { TimeOut timeout = new TimeOut(seconds, TimeUnit.SECONDS); for (int id = 26*3; id < 500 && ! timeout.hasTimedOut(); id++) { String shardKey = "" + (char) ('a' + (id % 26)); // See comment in ShardRoutingTest for hash distribution SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", shardKey + (bitSep != -1 ? "/" + bitSep : "") + "!" + id); doc.addField("n_ti", id); try { cloudClient.add(doc); if (splitKey.equals(shardKey)) splitKeyCount++; } catch (Exception e) { log.error("Exception while adding document id: " + doc.getField("id"), e); } try { Thread.sleep(50); } catch (InterruptedException e) { return; } } } public int getSplitKeyCount() { return splitKeyCount; } } }