org.apache.solr.cloud.AliasIntegrationTest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.cloud.AliasIntegrationTest.java

Source

/*
 * 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.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
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.request.UpdateRequest;
import org.apache.solr.client.solrj.request.V2Request;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.RequestStatusState;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.Utils;
import org.apache.zookeeper.KeeperException;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.apache.solr.common.cloud.ZkStateReader.ALIASES;

public class AliasIntegrationTest extends SolrCloudTestCase {

    private CloseableHttpClient httpClient;
    private CloudSolrClient solrClient;

    @BeforeClass
    public static void setupCluster() throws Exception {
        configureCluster(2).addConfig("conf", configset("cloud-minimal")).configure();
    }

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();
        solrClient = getCloudSolrClient(cluster);
        httpClient = (CloseableHttpClient) solrClient.getHttpClient();
    }

    @After
    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        IOUtils.close(solrClient, httpClient);

        // make sure all aliases created are removed for the next test method
        Map<String, String> aliases = new CollectionAdminRequest.ListAliases().process(cluster.getSolrClient())
                .getAliases();
        for (String alias : aliases.keySet()) {
            CollectionAdminRequest.deleteAlias(alias).process(cluster.getSolrClient());
        }

        cluster.deleteAllCollections();
    }

    @Test
    @BadApple(bugUrl = "https://issues.apache.org/jira/browse/SOLR-12028")
    public void testProperties() throws Exception {
        CollectionAdminRequest.createCollection("collection1meta", "conf", 2, 1).process(cluster.getSolrClient());
        CollectionAdminRequest.createCollection("collection2meta", "conf", 1, 1).process(cluster.getSolrClient());
        waitForState("Expected collection1 to be created with 2 shards and 1 replica", "collection1meta",
                clusterShape(2, 1));
        waitForState("Expected collection2 to be created with 1 shard and 1 replica", "collection2meta",
                clusterShape(1, 1));
        ZkStateReader zkStateReader = cluster.getSolrClient().getZkStateReader();
        zkStateReader.createClusterStateWatchersAndUpdate();
        List<String> aliases = zkStateReader.getAliases().resolveAliases("meta1");
        assertEquals(1, aliases.size());
        assertEquals("meta1", aliases.get(0));
        UnaryOperator<Aliases> op6 = a -> a.cloneWithCollectionAlias("meta1", "collection1meta,collection2meta");
        final ZkStateReader.AliasesManager aliasesManager = zkStateReader.aliasesManager;

        aliasesManager.applyModificationAndExportToZk(op6);
        aliases = zkStateReader.getAliases().resolveAliases("meta1");
        assertEquals(2, aliases.size());
        assertEquals("collection1meta", aliases.get(0));
        assertEquals("collection2meta", aliases.get(1));
        //ensure we have the back-compat format in ZK:
        final byte[] rawBytes = zkStateReader.getZkClient().getData(ALIASES, null, null, true);
        //noinspection unchecked
        assertTrue(((Map<String, Map<String, ?>>) Utils.fromJSON(rawBytes)).get("collection")
                .get("meta1") instanceof String);

        // set properties
        UnaryOperator<Aliases> op5 = a -> a.cloneWithCollectionAliasProperties("meta1", "foo", "bar");
        aliasesManager.applyModificationAndExportToZk(op5);
        Map<String, String> meta = zkStateReader.getAliases().getCollectionAliasProperties("meta1");
        assertNotNull(meta);
        assertTrue(meta.containsKey("foo"));
        assertEquals("bar", meta.get("foo"));

        // set more properties
        UnaryOperator<Aliases> op4 = a -> a.cloneWithCollectionAliasProperties("meta1", "foobar", "bazbam");
        aliasesManager.applyModificationAndExportToZk(op4);
        meta = zkStateReader.getAliases().getCollectionAliasProperties("meta1");
        assertNotNull(meta);

        // old properties still there
        assertTrue(meta.containsKey("foo"));
        assertEquals("bar", meta.get("foo"));

        // new properties added
        assertTrue(meta.containsKey("foobar"));
        assertEquals("bazbam", meta.get("foobar"));

        // remove properties
        UnaryOperator<Aliases> op3 = a -> a.cloneWithCollectionAliasProperties("meta1", "foo", null);
        aliasesManager.applyModificationAndExportToZk(op3);
        meta = zkStateReader.getAliases().getCollectionAliasProperties("meta1");
        assertNotNull(meta);

        // verify key was removed
        assertFalse(meta.containsKey("foo"));

        // but only the specified key was removed
        assertTrue(meta.containsKey("foobar"));
        assertEquals("bazbam", meta.get("foobar"));

        // removal of non existent key should succeed.
        UnaryOperator<Aliases> op2 = a -> a.cloneWithCollectionAliasProperties("meta1", "foo", null);
        aliasesManager.applyModificationAndExportToZk(op2);

        // chained invocations
        UnaryOperator<Aliases> op1 = a -> a.cloneWithCollectionAliasProperties("meta1", "foo2", "bazbam")
                .cloneWithCollectionAliasProperties("meta1", "foo3", "bazbam2");
        aliasesManager.applyModificationAndExportToZk(op1);

        // some other independent update (not overwritten)
        UnaryOperator<Aliases> op = a -> a.cloneWithCollectionAlias("meta3", "collection1meta,collection2meta");
        aliasesManager.applyModificationAndExportToZk(op);

        // competing went through
        assertEquals("collection1meta,collection2meta",
                zkStateReader.getAliases().getCollectionAliasMap().get("meta3"));

        meta = zkStateReader.getAliases().getCollectionAliasProperties("meta1");
        assertNotNull(meta);

        // old properties still there
        assertTrue(meta.containsKey("foobar"));
        assertEquals("bazbam", meta.get("foobar"));

        // competing update not overwritten
        assertEquals("collection1meta,collection2meta",
                zkStateReader.getAliases().getCollectionAliasMap().get("meta3"));

        // new properties added
        assertTrue(meta.containsKey("foo2"));
        assertEquals("bazbam", meta.get("foo2"));
        assertTrue(meta.containsKey("foo3"));
        assertEquals("bazbam2", meta.get("foo3"));

        // now check that an independently constructed ZkStateReader can see what we've done.
        // i.e. the data is really in zookeeper
        String zkAddress = cluster.getZkServer().getZkAddress();
        boolean createdZKSR = false;
        try (SolrZkClient zkClient = new SolrZkClient(zkAddress, 30000)) {

            ZkController.createClusterZkNodes(zkClient);

            zkStateReader = new ZkStateReader(zkClient);
            createdZKSR = true;
            zkStateReader.createClusterStateWatchersAndUpdate();

            meta = zkStateReader.getAliases().getCollectionAliasProperties("meta1");
            assertNotNull(meta);

            // verify key was removed in independent view
            assertFalse(meta.containsKey("foo"));

            // but only the specified key was removed
            assertTrue(meta.containsKey("foobar"));
            assertEquals("bazbam", meta.get("foobar"));

            Aliases a = zkStateReader.getAliases();
            Aliases clone = a.cloneWithCollectionAlias("meta1", null);
            meta = clone.getCollectionAliasProperties("meta1");
            assertEquals(0, meta.size());
        } finally {
            if (createdZKSR) {
                zkStateReader.close();
            }
        }
    }

    @Test
    public void testModifyPropertiesV2() throws Exception {
        final String aliasName = getTestName();
        ZkStateReader zkStateReader = createColectionsAndAlias(aliasName);
        final String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString();
        //TODO fix Solr test infra so that this /____v2/ becomes /api/
        HttpPost post = new HttpPost(baseUrl + "/____v2/c");
        post.setEntity(new StringEntity("{\n" + "\"set-alias-property\" : {\n" + "  \"name\": \"" + aliasName
                + "\",\n" + "  \"properties\" : {\n" + "    \"foo\": \"baz\",\n" + "    \"bar\": \"bam\"\n"
                + "    }\n" +
                //TODO should we use "NOW=" param?  Won't work with v2 and is kinda a hack any way since intended for distrib
                "  }\n" + "}", ContentType.APPLICATION_JSON));
        assertSuccess(post);
        checkFooAndBarMeta(aliasName, zkStateReader);
    }

    @Test
    public void testModifyPropertiesV1() throws Exception {
        // note we don't use TZ in this test, thus it's UTC
        final String aliasName = getTestName();
        ZkStateReader zkStateReader = createColectionsAndAlias(aliasName);
        final String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString();
        HttpGet get = new HttpGet(baseUrl + "/admin/collections?action=ALIASPROP" + "&wt=xml" + "&name=" + aliasName
                + "&property.foo=baz" + "&property.bar=bam");
        assertSuccess(get);
        checkFooAndBarMeta(aliasName, zkStateReader);
    }

    @Test
    @BadApple(bugUrl = "https://issues.apache.org/jira/browse/SOLR-12028")
    public void testModifyPropertiesCAR() throws Exception {
        // note we don't use TZ in this test, thus it's UTC
        final String aliasName = getTestName();
        ZkStateReader zkStateReader = createColectionsAndAlias(aliasName);
        CollectionAdminRequest.SetAliasProperty setAliasProperty = CollectionAdminRequest
                .setAliasProperty(aliasName);
        setAliasProperty.addProperty("foo", "baz");
        setAliasProperty.addProperty("bar", "bam");
        setAliasProperty.process(cluster.getSolrClient());
        checkFooAndBarMeta(aliasName, zkStateReader);

        // now verify we can delete
        setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
        setAliasProperty.addProperty("foo", "");
        setAliasProperty.process(cluster.getSolrClient());
        setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
        setAliasProperty.addProperty("bar", null);
        setAliasProperty.process(cluster.getSolrClient());
        setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);

        // whitespace value
        setAliasProperty.addProperty("foo", " ");
        setAliasProperty.process(cluster.getSolrClient());

    }

    private void checkFooAndBarMeta(String aliasName, ZkStateReader zkStateReader) throws Exception {
        zkStateReader.aliasesManager.update(); // ensure our view is up to date
        Map<String, String> meta = zkStateReader.getAliases().getCollectionAliasProperties(aliasName);
        assertNotNull(meta);
        assertTrue(meta.containsKey("foo"));
        assertEquals("baz", meta.get("foo"));
        assertTrue(meta.containsKey("bar"));
        assertEquals("bam", meta.get("bar"));
    }

    private ZkStateReader createColectionsAndAlias(String aliasName)
            throws SolrServerException, IOException, KeeperException, InterruptedException {
        CollectionAdminRequest.createCollection("collection1meta", "conf", 2, 1).process(cluster.getSolrClient());
        CollectionAdminRequest.createCollection("collection2meta", "conf", 1, 1).process(cluster.getSolrClient());
        waitForState("Expected collection1 to be created with 2 shards and 1 replica", "collection1meta",
                clusterShape(2, 1));
        waitForState("Expected collection2 to be created with 1 shard and 1 replica", "collection2meta",
                clusterShape(1, 1));
        ZkStateReader zkStateReader = cluster.getSolrClient().getZkStateReader();
        zkStateReader.createClusterStateWatchersAndUpdate();
        List<String> aliases = zkStateReader.getAliases().resolveAliases(aliasName);
        assertEquals(1, aliases.size());
        assertEquals(aliasName, aliases.get(0));
        UnaryOperator<Aliases> op6 = a -> a.cloneWithCollectionAlias(aliasName, "collection1meta,collection2meta");
        final ZkStateReader.AliasesManager aliasesManager = zkStateReader.aliasesManager;

        aliasesManager.applyModificationAndExportToZk(op6);
        aliases = zkStateReader.getAliases().resolveAliases(aliasName);
        assertEquals(2, aliases.size());
        assertEquals("collection1meta", aliases.get(0));
        assertEquals("collection2meta", aliases.get(1));
        return zkStateReader;
    }

    private void assertSuccess(HttpUriRequest msg) throws IOException {
        try (CloseableHttpResponse response = httpClient.execute(msg)) {
            if (200 != response.getStatusLine().getStatusCode()) {
                System.err.println(EntityUtils.toString(response.getEntity()));
                fail("Unexpected status: " + response.getStatusLine());
            }
        }
    }

    // Rather a long title, but it's common to recommend when people need to re-index for any reason that they:
    // 1> create a new collection
    // 2> index the corpus to the new collection and verify it
    // 3> create an alias pointing to the new collection WITH THE SAME NAME as their original collection
    // 4> delete the old collection.
    //
    // They may or may not have an alias already pointing to the old collection that's being replaced.
    // If they don't already have an alias, this leaves them with:
    //
    // > a collection named old_collection
    // > a collection named new_collection
    // > an alias old_collection->new_collection
    //
    // What happens when they delete old_collection now?
    //
    // Current behavior is that delete "does the right thing" and deletes old_collection rather than new_collection,
    // but if this behavior changes it could be disastrous for users so this test insures that this behavior.
    //
    @Test
    public void testDeleteAliasWithExistingCollectionName() throws Exception {
        CollectionAdminRequest.createCollection("collection_old", "conf", 2, 1).process(cluster.getSolrClient());
        CollectionAdminRequest.createCollection("collection_new", "conf", 1, 1).process(cluster.getSolrClient());
        waitForState("Expected collection_old to be created with 2 shards and 1 replica", "collection_old",
                clusterShape(2, 1));
        waitForState("Expected collection_new to be created with 1 shard and 1 replica", "collection_new",
                clusterShape(1, 1));

        new UpdateRequest().add("id", "6", "a_t", "humpty dumpy sat on a wall")
                .add("id", "7", "a_t", "humpty dumpy3 sat on a walls")
                .add("id", "8", "a_t", "humpty dumpy2 sat on a walled")
                .commit(cluster.getSolrClient(), "collection_old");

        new UpdateRequest().add("id", "1", "a_t", "humpty dumpy sat on an unfortunate wall")
                .commit(cluster.getSolrClient(), "collection_new");

        QueryResponse res = cluster.getSolrClient().query("collection_old", new SolrQuery("*:*"));
        assertEquals(3, res.getResults().getNumFound());

        // Let's insure we have a "handle" to the old collection
        CollectionAdminRequest.createAlias("collection_old_reserve", "collection_old")
                .process(cluster.getSolrClient());

        // This is the critical bit. The alias uses the _old collection name.
        CollectionAdminRequest.createAlias("collection_old", "collection_new").process(cluster.getSolrClient());

        // aliases: collection_old->collection_new, collection_old_reserve -> collection_old -> collection_new
        // collections: collection_new and collection_old

        // Now we should only see the doc in collection_new through the collection_old alias
        res = cluster.getSolrClient().query("collection_old", new SolrQuery("*:*"));
        assertEquals(1, res.getResults().getNumFound());

        // Now we should still transitively see collection_new
        res = cluster.getSolrClient().query("collection_old_reserve", new SolrQuery("*:*"));
        assertEquals(1, res.getResults().getNumFound());

        // Now delete the old collection. This should fail since the collection_old_reserve points to collection_old
        RequestStatusState delResp = CollectionAdminRequest.deleteCollection("collection_old")
                .processAndWait(cluster.getSolrClient(), 60);
        assertEquals("Should have failed to delete collection: ", delResp, RequestStatusState.FAILED);

        // assure ourselves that the old colletion is, indeed, still there.
        assertNotNull("collection_old should exist!",
                cluster.getSolrClient().getZkStateReader().getClusterState().getCollectionOrNull("collection_old"));

        // Now we should still succeed using the alias collection_old which points to collection_new
        // aliase: collection_old -> collection_new, collection_old_reserve -> collection_old -> collection_new
        // collections: collection_old, collection_new
        res = cluster.getSolrClient().query("collection_old", new SolrQuery("*:*"));
        assertEquals(1, res.getResults().getNumFound());

        Aliases aliases = cluster.getSolrClient().getZkStateReader().getAliases();
        assertTrue("collection_old should point to collection_new",
                aliases.resolveAliases("collection_old").contains("collection_new"));
        assertTrue("collection_old_reserve should point to collection_new",
                aliases.resolveAliases("collection_old_reserve").contains("collection_new"));

        // Clean up
        CollectionAdminRequest.deleteAlias("collection_old_reserve").processAndWait(cluster.getSolrClient(), 60);
        CollectionAdminRequest.deleteAlias("collection_old").processAndWait(cluster.getSolrClient(), 60);
        CollectionAdminRequest.deleteCollection("collection_new").processAndWait(cluster.getSolrClient(), 60);
        CollectionAdminRequest.deleteCollection("collection_old").processAndWait(cluster.getSolrClient(), 60);
        // collection_old already deleted as well as collection_old_reserve

        assertNull("collection_old_reserve should be gone", cluster.getSolrClient().getZkStateReader().getAliases()
                .getCollectionAliasMap().get("collection_old_reserve"));
        assertNull("collection_old should be gone", cluster.getSolrClient().getZkStateReader().getAliases()
                .getCollectionAliasMap().get("collection_old"));

        assertFalse("collection_new should be gone",
                cluster.getSolrClient().getZkStateReader().getClusterState().hasCollection("collection_new"));

        assertFalse("collection_old should be gone",
                cluster.getSolrClient().getZkStateReader().getClusterState().hasCollection("collection_old"));
    }

    // While writing the above test I wondered what happens when an alias points to two collections and one of them
    // is deleted.
    @Test
    public void testDeleteOneOfTwoCollectionsAliased() throws Exception {
        CollectionAdminRequest.createCollection("collection_one", "conf", 2, 1).process(cluster.getSolrClient());
        CollectionAdminRequest.createCollection("collection_two", "conf", 1, 1).process(cluster.getSolrClient());
        waitForState("Expected collection_one to be created with 2 shards and 1 replica", "collection_one",
                clusterShape(2, 1));
        waitForState("Expected collection_two to be created with 1 shard and 1 replica", "collection_two",
                clusterShape(1, 1));

        new UpdateRequest().add("id", "1", "a_t", "humpty dumpy sat on a wall").commit(cluster.getSolrClient(),
                "collection_one");

        new UpdateRequest().add("id", "10", "a_t", "humpty dumpy sat on a high wall")
                .add("id", "11", "a_t", "humpty dumpy sat on a low wall")
                .commit(cluster.getSolrClient(), "collection_two");

        // Create an alias pointing to both
        CollectionAdminRequest.createAlias("collection_alias_pair", "collection_one,collection_two")
                .process(cluster.getSolrClient());

        QueryResponse res = cluster.getSolrClient().query("collection_alias_pair", new SolrQuery("*:*"));
        assertEquals(3, res.getResults().getNumFound());

        // Now delete one of the collections, should fail since an alias points to it.
        RequestStatusState delResp = CollectionAdminRequest.deleteCollection("collection_one")
                .processAndWait(cluster.getSolrClient(), 60);

        assertEquals("Should have failed to delete collection: ", delResp, RequestStatusState.FAILED);

        // Now redefine the alias to only point to colletion two
        CollectionAdminRequest.createAlias("collection_alias_pair", "collection_two")
                .process(cluster.getSolrClient());

        //Delete collection_one.
        delResp = CollectionAdminRequest.deleteCollection("collection_one").processAndWait(cluster.getSolrClient(),
                60);

        assertEquals("Should not have failed to delete collection, it was removed from the alias: ", delResp,
                RequestStatusState.COMPLETED);

        // Should only see two docs now in second collection
        res = cluster.getSolrClient().query("collection_alias_pair", new SolrQuery("*:*"));
        assertEquals(2, res.getResults().getNumFound());

        // We shouldn't be able to ping the deleted collection directly as
        // was deleted (and, assuming that it only points to collection_old).
        try {
            cluster.getSolrClient().query("collection_one", new SolrQuery("*:*"));
        } catch (SolrServerException se) {
            assertTrue(se.getMessage().contains("No live SolrServers"));
        }

        // Clean up
        CollectionAdminRequest.deleteAlias("collection_alias_pair").processAndWait(cluster.getSolrClient(), 60);
        CollectionAdminRequest.deleteCollection("collection_two").processAndWait(cluster.getSolrClient(), 60);
        // collection_one already deleted

        assertNull("collection_alias_pair should be gone", cluster.getSolrClient().getZkStateReader().getAliases()
                .getCollectionAliasMap().get("collection_alias_pair"));

        assertFalse("collection_one should be gone",
                cluster.getSolrClient().getZkStateReader().getClusterState().hasCollection("collection_one"));

        assertFalse("collection_two should be gone",
                cluster.getSolrClient().getZkStateReader().getClusterState().hasCollection("collection_two"));

    }

    @Test
    public void test() throws Exception {
        CollectionAdminRequest.createCollection("collection1", "conf", 2, 1).process(cluster.getSolrClient());
        CollectionAdminRequest.createCollection("collection2", "conf", 1, 1).process(cluster.getSolrClient());
        waitForState("Expected collection1 to be created with 2 shards and 1 replica", "collection1",
                clusterShape(2, 1));
        waitForState("Expected collection2 to be created with 1 shard and 1 replica", "collection2",
                clusterShape(1, 1));

        new UpdateRequest().add("id", "6", "a_t", "humpty dumpy sat on a wall")
                .add("id", "7", "a_t", "humpty dumpy3 sat on a walls")
                .add("id", "8", "a_t", "humpty dumpy2 sat on a walled")
                .commit(cluster.getSolrClient(), "collection1");

        new UpdateRequest().add("id", "9", "a_t", "humpty dumpy sat on a wall")
                .add("id", "10", "a_t", "humpty dumpy3 sat on a walls")
                .commit(cluster.getSolrClient(), "collection2");

        ///////////////
        CollectionAdminRequest.createAlias("testalias1", "collection1").process(cluster.getSolrClient());
        sleepToAllowZkPropagation();
        // ensure that the alias has been registered
        assertEquals("collection1", new CollectionAdminRequest.ListAliases().process(cluster.getSolrClient())
                .getAliases().get("testalias1"));

        // search for alias
        searchSeveralWays("testalias1", new SolrQuery("*:*"), 3);

        // Use a comma delimited list, one of which is an alias
        searchSeveralWays("testalias1,collection2", new SolrQuery("*:*"), 5);

        ///////////////
        // test alias pointing to two collections.  collection2 first because it's not on every node
        CollectionAdminRequest.createAlias("testalias2", "collection2,collection1")
                .process(cluster.getSolrClient());

        searchSeveralWays("testalias2", new SolrQuery("*:*"), 5);

        ///////////////
        // update alias
        CollectionAdminRequest.createAlias("testalias2", "collection2").process(cluster.getSolrClient());
        sleepToAllowZkPropagation();

        searchSeveralWays("testalias2", new SolrQuery("*:*"), 2);

        ///////////////
        // alias pointing to alias.  One level of indirection is supported; more than that is not (may or may not work)
        // TODO dubious; remove?
        CollectionAdminRequest.createAlias("testalias3", "testalias2").process(cluster.getSolrClient());
        searchSeveralWays("testalias3", new SolrQuery("*:*"), 2);

        ///////////////
        // Test 2 aliases pointing to the same collection
        CollectionAdminRequest.createAlias("testalias4", "collection2").process(cluster.getSolrClient());
        CollectionAdminRequest.createAlias("testalias5", "collection2").process(cluster.getSolrClient());

        // add one document to testalias4, thus to collection2
        new UpdateRequest().add("id", "11", "a_t", "humpty dumpy4 sat on a walls").commit(cluster.getSolrClient(),
                "testalias4"); // thus gets added to collection2

        searchSeveralWays("testalias4", new SolrQuery("*:*"), 3);
        //searchSeveralWays("testalias4,testalias5", new SolrQuery("*:*"), 3);

        ///////////////
        // use v2 API
        new V2Request.Builder("/collections").withMethod(SolrRequest.METHOD.POST).withPayload(
                "{\"create-alias\": {\"name\": \"testalias6\", collections:[\"collection2\",\"collection1\"]}}")
                .build().process(cluster.getSolrClient());

        searchSeveralWays("testalias6", new SolrQuery("*:*"), 6);

        // add one document to testalias6, which will route to collection2 because it's the first
        new UpdateRequest().add("id", "12", "a_t", "humpty dumpy5 sat on a walls").commit(cluster.getSolrClient(),
                "testalias6"); // thus gets added to collection2
        searchSeveralWays("collection2", new SolrQuery("*:*"), 4);

        ///////////////
        for (int i = 1; i <= 6; i++) {
            CollectionAdminRequest.deleteAlias("testalias" + i).process(cluster.getSolrClient());
        }
        sleepToAllowZkPropagation();

        SolrException e = expectThrows(SolrException.class, () -> {
            SolrQuery q = new SolrQuery("*:*");
            q.set("collection", "testalias1");
            cluster.getSolrClient().query(q);
        });
        assertTrue("Unexpected exception message: " + e.getMessage(),
                e.getMessage().contains("Collection not found: testalias1"));
    }

    /**
     * Sleep a bit to allow Zookeeper state propagation.
     *
     * Solr's view of the cluster is eventually consistent. *Eventually* all nodes and CloudSolrClients will be aware of
     * alias changes, but not immediately. If a newly created alias is queried, things should work right away since Solr
     * will attempt to see if it needs to get the latest aliases when it can't otherwise resolve the name.  However
     * modifications to an alias will take some time.
     */
    private void sleepToAllowZkPropagation() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }

    private void searchSeveralWays(String collectionList, SolrParams solrQuery, int expectedNumFound)
            throws IOException, SolrServerException {
        searchSeveralWays(collectionList, solrQuery,
                res -> assertEquals(expectedNumFound, res.getResults().getNumFound()));
    }

    private void searchSeveralWays(String collectionList, SolrParams solrQuery,
            Consumer<QueryResponse> responseConsumer) throws IOException, SolrServerException {
        if (random().nextBoolean()) {
            // cluster's CloudSolrClient
            responseConsumer.accept(cluster.getSolrClient().query(collectionList, solrQuery));
        } else {
            // new CloudSolrClient (random shardLeadersOnly)
            try (CloudSolrClient solrClient = getCloudSolrClient(cluster)) {
                if (random().nextBoolean()) {
                    solrClient.setDefaultCollection(collectionList);
                    responseConsumer.accept(solrClient.query(null, solrQuery));
                } else {
                    responseConsumer.accept(solrClient.query(collectionList, solrQuery));
                }
            }
        }

        // note: collectionList could be null when we randomly recurse and put the actual collection list into the
        //  "collection" param and some bugs value into collectionList (including null).  Only CloudSolrClient supports null.
        if (collectionList != null) {
            // HttpSolrClient
            JettySolrRunner jetty = cluster.getRandomJetty(random());
            if (random().nextBoolean()) {
                try (HttpSolrClient client = getHttpSolrClient(
                        jetty.getBaseUrl().toString() + "/" + collectionList)) {
                    responseConsumer.accept(client.query(null, solrQuery));
                }
            } else {
                try (HttpSolrClient client = getHttpSolrClient(jetty.getBaseUrl().toString())) {
                    responseConsumer.accept(client.query(collectionList, solrQuery));
                }
            }

            // Recursively do again; this time with the &collection= param
            if (solrQuery.get("collection") == null) {
                // put in "collection" param
                ModifiableSolrParams newParams = new ModifiableSolrParams(solrQuery);
                newParams.set("collection", collectionList);
                String maskedColl = new String[] { null, "bogus", "collection2", "collection1" }[random()
                        .nextInt(4)];
                searchSeveralWays(maskedColl, newParams, responseConsumer);
            }
        }
    }

    @Test
    public void testErrorChecks() throws Exception {
        CollectionAdminRequest.createCollection("testErrorChecks-collection", "conf", 2, 1)
                .process(cluster.getSolrClient());
        waitForState("Expected testErrorChecks-collection to be created with 2 shards and 1 replica",
                "testErrorChecks-collection", clusterShape(2, 1));

        ignoreException(".");

        // Invalid Alias name
        SolrException e = expectThrows(SolrException.class, () -> CollectionAdminRequest
                .createAlias("test:alias", "testErrorChecks-collection").process(cluster.getSolrClient()));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code()));

        // Target collection doesn't exists
        e = expectThrows(SolrException.class, () -> CollectionAdminRequest.createAlias("testalias", "doesnotexist")
                .process(cluster.getSolrClient()));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code()));
        assertTrue(e.getMessage().contains(
                "Can't create collection alias for collections='doesnotexist', 'doesnotexist' is not an existing collection or alias"));

        // One of the target collections doesn't exist
        e = expectThrows(SolrException.class,
                () -> CollectionAdminRequest.createAlias("testalias", "testErrorChecks-collection,doesnotexist")
                        .process(cluster.getSolrClient()));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code()));
        assertTrue(e.getMessage().contains(
                "Can't create collection alias for collections='testErrorChecks-collection,doesnotexist', 'doesnotexist' is not an existing collection or alias"));

        // Valid
        CollectionAdminRequest.createAlias("testalias", "testErrorChecks-collection")
                .process(cluster.getSolrClient());
        // TODO dubious; remove?
        CollectionAdminRequest.createAlias("testalias2", "testalias").process(cluster.getSolrClient());

        // Alias + invalid
        e = expectThrows(SolrException.class, () -> CollectionAdminRequest
                .createAlias("testalias3", "testalias2,doesnotexist").process(cluster.getSolrClient()));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code()));
        unIgnoreException(".");

        CollectionAdminRequest.deleteAlias("testalias").process(cluster.getSolrClient());
        CollectionAdminRequest.deleteAlias("testalias2").process(cluster.getSolrClient());
        CollectionAdminRequest.deleteCollection("testErrorChecks-collection");
    }

}