org.apache.solr.client.solrj.impl.CloudSolrServer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.client.solrj.impl.CloudSolrServer.java

Source

package org.apache.solr.client.solrj.impl;

/*
 * 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.
 */

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import org.apache.http.client.HttpClient;
import org.apache.solr.client.solrj.ResponseParser;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
import org.apache.solr.client.solrj.request.IsUpdateRequest;
import org.apache.solr.client.solrj.request.RequestWriter;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.DocRouter;
import org.apache.solr.common.cloud.ImplicitDocRouter;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkCoreNodeProps;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.cloud.ZooKeeperException;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.UpdateParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SolrjNamedThreadFactory;
import org.apache.solr.common.util.StrUtils;
import org.apache.zookeeper.KeeperException;

/**
 * SolrJ client class to communicate with SolrCloud.
 * Instances of this class communicate with Zookeeper to discover
 * Solr endpoints for SolrCloud collections, and then use the 
 * {@link LBHttpSolrServer} to issue requests.
 * 
 * This class assumes the id field for your documents is called
 * 'id' - if this is not the case, you must set the right name
 * with {@link #setIdField(String)}.
 */
public class CloudSolrServer extends SolrServer {
    private volatile ZkStateReader zkStateReader;
    private String zkHost; // the zk server address
    private int zkConnectTimeout = 10000;
    private int zkClientTimeout = 10000;
    private volatile String defaultCollection;
    private final LBHttpSolrServer lbServer;
    private final boolean shutdownLBHttpSolrServer;
    private HttpClient myClient;
    Random rand = new Random();

    private final boolean updatesToLeaders;
    private boolean parallelUpdates = true;
    private ExecutorService threadPool = Executors
            .newCachedThreadPool(new SolrjNamedThreadFactory("CloudSolrServer ThreadPool"));
    private String idField = "id";
    private final Set<String> NON_ROUTABLE_PARAMS;
    {
        NON_ROUTABLE_PARAMS = new HashSet<>();
        NON_ROUTABLE_PARAMS.add(UpdateParams.EXPUNGE_DELETES);
        NON_ROUTABLE_PARAMS.add(UpdateParams.MAX_OPTIMIZE_SEGMENTS);
        NON_ROUTABLE_PARAMS.add(UpdateParams.COMMIT);
        NON_ROUTABLE_PARAMS.add(UpdateParams.WAIT_SEARCHER);
        NON_ROUTABLE_PARAMS.add(UpdateParams.OPEN_SEARCHER);

        NON_ROUTABLE_PARAMS.add(UpdateParams.SOFT_COMMIT);
        NON_ROUTABLE_PARAMS.add(UpdateParams.PREPARE_COMMIT);
        NON_ROUTABLE_PARAMS.add(UpdateParams.OPTIMIZE);

        // Not supported via SolrCloud
        // NON_ROUTABLE_PARAMS.add(UpdateParams.ROLLBACK);

    }

    /**
     * @param zkHost The client endpoint of the zookeeper quorum containing the cloud state,
     * in the form HOST:PORT.
     */
    public CloudSolrServer(String zkHost) {
        this.zkHost = zkHost;
        this.myClient = HttpClientUtil.createClient(null);
        this.lbServer = new LBHttpSolrServer(myClient);
        this.lbServer.setRequestWriter(new BinaryRequestWriter());
        this.lbServer.setParser(new BinaryResponseParser());
        this.updatesToLeaders = true;
        shutdownLBHttpSolrServer = true;
    }

    public CloudSolrServer(String zkHost, boolean updatesToLeaders) throws MalformedURLException {
        this.zkHost = zkHost;
        this.myClient = HttpClientUtil.createClient(null);
        this.lbServer = new LBHttpSolrServer(myClient);
        this.lbServer.setRequestWriter(new BinaryRequestWriter());
        this.lbServer.setParser(new BinaryResponseParser());
        this.updatesToLeaders = updatesToLeaders;
        shutdownLBHttpSolrServer = true;
    }

    /**
     * @param zkHost The client endpoint of the zookeeper quorum containing the cloud state,
     * in the form HOST:PORT.
     * @param lbServer LBHttpSolrServer instance for requests. 
     */
    public CloudSolrServer(String zkHost, LBHttpSolrServer lbServer) {
        this.zkHost = zkHost;
        this.lbServer = lbServer;
        this.updatesToLeaders = true;
        shutdownLBHttpSolrServer = false;
    }

    /**
     * @param zkHost The client endpoint of the zookeeper quorum containing the cloud state,
     * in the form HOST:PORT.
     * @param lbServer LBHttpSolrServer instance for requests. 
     * @param updatesToLeaders sends updates only to leaders - defaults to true
     */
    public CloudSolrServer(String zkHost, LBHttpSolrServer lbServer, boolean updatesToLeaders) {
        this.zkHost = zkHost;
        this.lbServer = lbServer;
        this.updatesToLeaders = updatesToLeaders;
        shutdownLBHttpSolrServer = false;
    }

    public ResponseParser getParser() {
        return lbServer.getParser();
    }

    /**
     * Note: This setter method is <b>not thread-safe</b>.
     * 
     * @param processor
     *          Default Response Parser chosen to parse the response if the parser
     *          were not specified as part of the request.
     * @see org.apache.solr.client.solrj.SolrRequest#getResponseParser()
     */
    public void setParser(ResponseParser processor) {
        lbServer.setParser(processor);
    }

    public RequestWriter getRequestWriter() {
        return lbServer.getRequestWriter();
    }

    public void setRequestWriter(RequestWriter requestWriter) {
        lbServer.setRequestWriter(requestWriter);
    }

    public ZkStateReader getZkStateReader() {
        return zkStateReader;
    }

    /**
     * @param idField the field to route documents on.
     */
    public void setIdField(String idField) {
        this.idField = idField;
    }

    /**
     * @return the field that updates are routed on.
     */
    public String getIdField() {
        return idField;
    }

    /** Sets the default collection for request */
    public void setDefaultCollection(String collection) {
        this.defaultCollection = collection;
    }

    /** Gets the default collection for request */
    public String getDefaultCollection() {
        return defaultCollection;
    }

    /** Set the connect timeout to the zookeeper ensemble in ms */
    public void setZkConnectTimeout(int zkConnectTimeout) {
        this.zkConnectTimeout = zkConnectTimeout;
    }

    /** Set the timeout to the zookeeper ensemble in ms */
    public void setZkClientTimeout(int zkClientTimeout) {
        this.zkClientTimeout = zkClientTimeout;
    }

    /**
     * Connect to the zookeeper ensemble.
     * This is an optional method that may be used to force a connect before any other requests are sent.
     *
     */
    public void connect() {
        if (zkStateReader == null) {
            synchronized (this) {
                if (zkStateReader == null) {
                    ZkStateReader zk = null;
                    try {
                        zk = new ZkStateReader(zkHost, zkClientTimeout, zkConnectTimeout);
                        zk.createClusterStateWatchersAndUpdate();
                        zkStateReader = zk;
                    } catch (InterruptedException e) {
                        if (zk != null)
                            zk.close();
                        Thread.currentThread().interrupt();
                        throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
                    } catch (KeeperException e) {
                        if (zk != null)
                            zk.close();
                        throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
                    } catch (IOException e) {
                        if (zk != null)
                            zk.close();
                        throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
                    } catch (TimeoutException e) {
                        if (zk != null)
                            zk.close();
                        throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e);
                    } catch (Exception e) {
                        if (zk != null)
                            zk.close();
                        // do not wrap because clients may be relying on the underlying exception being thrown
                        throw e;
                    }
                }
            }
        }
    }

    public void setParallelUpdates(boolean parallelUpdates) {
        this.parallelUpdates = parallelUpdates;
    }

    private NamedList directUpdate(AbstractUpdateRequest request, ClusterState clusterState)
            throws SolrServerException {
        UpdateRequest updateRequest = (UpdateRequest) request;
        ModifiableSolrParams params = (ModifiableSolrParams) request.getParams();
        ModifiableSolrParams routableParams = new ModifiableSolrParams();
        ModifiableSolrParams nonRoutableParams = new ModifiableSolrParams();

        if (params != null) {
            nonRoutableParams.add(params);
            routableParams.add(params);
            for (String param : NON_ROUTABLE_PARAMS) {
                routableParams.remove(param);
            }
        }

        String collection = nonRoutableParams.get(UpdateParams.COLLECTION, defaultCollection);
        if (collection == null) {
            throw new SolrServerException(
                    "No collection param specified on request and no default collection has been set.");
        }

        //Check to see if the collection is an alias.
        Aliases aliases = zkStateReader.getAliases();
        if (aliases != null) {
            Map<String, String> collectionAliases = aliases.getCollectionAliasMap();
            if (collectionAliases != null && collectionAliases.containsKey(collection)) {
                collection = collectionAliases.get(collection);
            }
        }

        DocCollection col = clusterState.getCollection(collection);

        DocRouter router = col.getRouter();

        if (router instanceof ImplicitDocRouter) {
            // short circuit as optimization
            return null;
        }

        //Create the URL map, which is keyed on slice name.
        //The value is a list of URLs for each replica in the slice.
        //The first value in the list is the leader for the slice.
        Map<String, List<String>> urlMap = buildUrlMap(col);
        if (urlMap == null) {
            // we could not find a leader yet - use unoptimized general path
            return null;
        }

        NamedList<Throwable> exceptions = new NamedList<Throwable>();
        NamedList<NamedList> shardResponses = new NamedList<NamedList>();

        Map<String, LBHttpSolrServer.Req> routes = updateRequest.getRoutes(router, col, urlMap, routableParams,
                this.idField);
        if (routes == null) {
            return null;
        }

        long start = System.nanoTime();

        if (parallelUpdates) {
            final Map<String, Future<NamedList<?>>> responseFutures = new HashMap<>(routes.size());
            for (final Map.Entry<String, LBHttpSolrServer.Req> entry : routes.entrySet()) {
                final String url = entry.getKey();
                final LBHttpSolrServer.Req lbRequest = entry.getValue();
                responseFutures.put(url, threadPool.submit(new Callable<NamedList<?>>() {
                    @Override
                    public NamedList<?> call() throws Exception {
                        return lbServer.request(lbRequest).getResponse();
                    }
                }));
            }

            for (final Map.Entry<String, Future<NamedList<?>>> entry : responseFutures.entrySet()) {
                final String url = entry.getKey();
                final Future<NamedList<?>> responseFuture = entry.getValue();
                try {
                    shardResponses.add(url, responseFuture.get());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(e);
                } catch (ExecutionException e) {
                    exceptions.add(url, e.getCause());
                }
            }

            if (exceptions.size() > 0) {
                throw new RouteException(ErrorCode.SERVER_ERROR, exceptions, routes);
            }
        } else {
            for (Map.Entry<String, LBHttpSolrServer.Req> entry : routes.entrySet()) {
                String url = entry.getKey();
                LBHttpSolrServer.Req lbRequest = entry.getValue();
                try {
                    NamedList rsp = lbServer.request(lbRequest).getResponse();
                    shardResponses.add(url, rsp);
                } catch (Exception e) {
                    throw new SolrServerException(e);
                }
            }
        }

        UpdateRequest nonRoutableRequest = null;
        List<String> deleteQuery = updateRequest.getDeleteQuery();
        if (deleteQuery != null && deleteQuery.size() > 0) {
            UpdateRequest deleteQueryRequest = new UpdateRequest();
            deleteQueryRequest.setDeleteQuery(deleteQuery);
            nonRoutableRequest = deleteQueryRequest;
        }

        Set<String> paramNames = nonRoutableParams.getParameterNames();

        Set<String> intersection = new HashSet<>(paramNames);
        intersection.retainAll(NON_ROUTABLE_PARAMS);

        if (nonRoutableRequest != null || intersection.size() > 0) {
            if (nonRoutableRequest == null) {
                nonRoutableRequest = new UpdateRequest();
            }
            nonRoutableRequest.setParams(nonRoutableParams);
            List<String> urlList = new ArrayList<>();
            urlList.addAll(routes.keySet());
            Collections.shuffle(urlList, rand);
            LBHttpSolrServer.Req req = new LBHttpSolrServer.Req(nonRoutableRequest, urlList);
            try {
                LBHttpSolrServer.Rsp rsp = lbServer.request(req);
                shardResponses.add(urlList.get(0), rsp.getResponse());
            } catch (Exception e) {
                throw new SolrException(ErrorCode.SERVER_ERROR, urlList.get(0), e);
            }
        }

        long end = System.nanoTime();

        RouteResponse rr = condenseResponse(shardResponses, (long) ((end - start) / 1000000));
        rr.setRouteResponses(shardResponses);
        rr.setRoutes(routes);
        return rr;
    }

    private Map<String, List<String>> buildUrlMap(DocCollection col) {
        Map<String, List<String>> urlMap = new HashMap<>();
        Collection<Slice> slices = col.getActiveSlices();
        Iterator<Slice> sliceIterator = slices.iterator();
        while (sliceIterator.hasNext()) {
            Slice slice = sliceIterator.next();
            String name = slice.getName();
            List<String> urls = new ArrayList<>();
            Replica leader = slice.getLeader();
            if (leader == null) {
                // take unoptimized general path - we cannot find a leader yet
                return null;
            }
            ZkCoreNodeProps zkProps = new ZkCoreNodeProps(leader);
            String url = zkProps.getCoreUrl();
            urls.add(url);
            Collection<Replica> replicas = slice.getReplicas();
            Iterator<Replica> replicaIterator = replicas.iterator();
            while (replicaIterator.hasNext()) {
                Replica replica = replicaIterator.next();
                if (!replica.getNodeName().equals(leader.getNodeName())
                        && !replica.getName().equals(leader.getName())) {
                    ZkCoreNodeProps zkProps1 = new ZkCoreNodeProps(replica);
                    String url1 = zkProps1.getCoreUrl();
                    urls.add(url1);
                }
            }
            urlMap.put(name, urls);
        }
        return urlMap;
    }

    public RouteResponse condenseResponse(NamedList response, long timeMillis) {
        RouteResponse condensed = new RouteResponse();
        int status = 0;
        Integer rf = null;
        Integer minRf = null;
        for (int i = 0; i < response.size(); i++) {
            NamedList shardResponse = (NamedList) response.getVal(i);
            NamedList header = (NamedList) shardResponse.get("responseHeader");
            Integer shardStatus = (Integer) header.get("status");
            int s = shardStatus.intValue();
            if (s > 0) {
                status = s;
            }
            Object rfObj = header.get(UpdateRequest.REPFACT);
            if (rfObj != null && rfObj instanceof Integer) {
                Integer routeRf = (Integer) rfObj;
                if (rf == null || routeRf < rf)
                    rf = routeRf;
            }
            minRf = (Integer) header.get(UpdateRequest.MIN_REPFACT);
        }

        NamedList cheader = new NamedList();
        cheader.add("status", status);
        cheader.add("QTime", timeMillis);
        if (rf != null)
            cheader.add(UpdateRequest.REPFACT, rf);
        if (minRf != null)
            cheader.add(UpdateRequest.MIN_REPFACT, minRf);

        condensed.add("responseHeader", cheader);
        return condensed;
    }

    public static class RouteResponse extends NamedList {
        private NamedList routeResponses;
        private Map<String, LBHttpSolrServer.Req> routes;

        public void setRouteResponses(NamedList routeResponses) {
            this.routeResponses = routeResponses;
        }

        public NamedList getRouteResponses() {
            return routeResponses;
        }

        public void setRoutes(Map<String, LBHttpSolrServer.Req> routes) {
            this.routes = routes;
        }

        public Map<String, LBHttpSolrServer.Req> getRoutes() {
            return routes;
        }

    }

    public static class RouteException extends SolrException {

        private NamedList<Throwable> throwables;
        private Map<String, LBHttpSolrServer.Req> routes;

        public RouteException(ErrorCode errorCode, NamedList<Throwable> throwables,
                Map<String, LBHttpSolrServer.Req> routes) {
            super(errorCode, throwables.getVal(0).getMessage(), throwables.getVal(0));
            this.throwables = throwables;
            this.routes = routes;
        }

        public NamedList<Throwable> getThrowables() {
            return throwables;
        }

        public Map<String, LBHttpSolrServer.Req> getRoutes() {
            return this.routes;
        }
    }

    @Override
    public NamedList<Object> request(SolrRequest request) throws SolrServerException, IOException {
        connect();

        ClusterState clusterState = zkStateReader.getClusterState();

        boolean sendToLeaders = false;
        List<String> replicas = null;

        if (request instanceof IsUpdateRequest) {
            if (request instanceof UpdateRequest) {
                NamedList response = directUpdate((AbstractUpdateRequest) request, clusterState);
                if (response != null) {
                    return response;
                }
            }
            sendToLeaders = true;
            replicas = new ArrayList<>();
        }

        SolrParams reqParams = request.getParams();
        if (reqParams == null) {
            reqParams = new ModifiableSolrParams();
        }
        List<String> theUrlList = new ArrayList<>();
        if (request.getPath().equals("/admin/collections") || request.getPath().equals("/admin/cores")) {
            Set<String> liveNodes = clusterState.getLiveNodes();
            for (String liveNode : liveNodes) {
                theUrlList.add(zkStateReader.getBaseUrlForNodeName(liveNode));
            }
        } else {
            String collection = reqParams.get(UpdateParams.COLLECTION, defaultCollection);

            if (collection == null) {
                throw new SolrServerException(
                        "No collection param specified on request and no default collection has been set.");
            }

            Set<String> collectionsList = getCollectionList(clusterState, collection);
            if (collectionsList.size() == 0) {
                throw new SolrException(ErrorCode.BAD_REQUEST, "Could not find collection: " + collection);
            }

            String shardKeys = reqParams.get(ShardParams._ROUTE_);
            if (shardKeys == null) {
                shardKeys = reqParams.get(ShardParams.SHARD_KEYS); // deprecated
            }

            // TODO: not a big deal because of the caching, but we could avoid looking
            // at every shard
            // when getting leaders if we tweaked some things

            // Retrieve slices from the cloud state and, for each collection
            // specified,
            // add it to the Map of slices.
            Map<String, Slice> slices = new HashMap<>();
            for (String collectionName : collectionsList) {
                DocCollection col = clusterState.getCollection(collectionName);
                Collection<Slice> routeSlices = col.getRouter().getSearchSlices(shardKeys, reqParams, col);
                ClientUtils.addSlices(slices, collectionName, routeSlices, true);
            }
            Set<String> liveNodes = clusterState.getLiveNodes();

            List<String> leaderUrlList = null;
            List<String> urlList = null;
            List<String> replicasList = null;

            // build a map of unique nodes
            // TODO: allow filtering by group, role, etc
            Map<String, ZkNodeProps> nodes = new HashMap<>();
            List<String> urlList2 = new ArrayList<>();
            for (Slice slice : slices.values()) {
                for (ZkNodeProps nodeProps : slice.getReplicasMap().values()) {
                    ZkCoreNodeProps coreNodeProps = new ZkCoreNodeProps(nodeProps);
                    String node = coreNodeProps.getNodeName();
                    if (!liveNodes.contains(coreNodeProps.getNodeName())
                            || !coreNodeProps.getState().equals(ZkStateReader.ACTIVE))
                        continue;
                    if (nodes.put(node, nodeProps) == null) {
                        if (!sendToLeaders || (sendToLeaders && coreNodeProps.isLeader())) {
                            String url;
                            if (reqParams.get(UpdateParams.COLLECTION) == null) {
                                url = ZkCoreNodeProps.getCoreUrl(nodeProps.getStr(ZkStateReader.BASE_URL_PROP),
                                        defaultCollection);
                            } else {
                                url = coreNodeProps.getCoreUrl();
                            }
                            urlList2.add(url);
                        } else if (sendToLeaders) {
                            String url;
                            if (reqParams.get(UpdateParams.COLLECTION) == null) {
                                url = ZkCoreNodeProps.getCoreUrl(nodeProps.getStr(ZkStateReader.BASE_URL_PROP),
                                        defaultCollection);
                            } else {
                                url = coreNodeProps.getCoreUrl();
                            }
                            replicas.add(url);
                        }
                    }
                }
            }

            if (sendToLeaders) {
                leaderUrlList = urlList2;
                replicasList = replicas;
            } else {
                urlList = urlList2;
            }

            if (sendToLeaders) {
                theUrlList = new ArrayList<>(leaderUrlList.size());
                theUrlList.addAll(leaderUrlList);
            } else {
                theUrlList = new ArrayList<>(urlList.size());
                theUrlList.addAll(urlList);
            }
            Collections.shuffle(theUrlList, rand);
            if (sendToLeaders) {
                ArrayList<String> theReplicas = new ArrayList<>(replicasList.size());
                theReplicas.addAll(replicasList);
                Collections.shuffle(theReplicas, rand);
                // System.out.println("leaders:" + theUrlList);
                // System.out.println("replicas:" + theReplicas);
                theUrlList.addAll(theReplicas);
            }

        }

        // System.out.println("########################## MAKING REQUEST TO " +
        // theUrlList);

        LBHttpSolrServer.Req req = new LBHttpSolrServer.Req(request, theUrlList);
        LBHttpSolrServer.Rsp rsp = lbServer.request(req);
        return rsp.getResponse();
    }

    private Set<String> getCollectionList(ClusterState clusterState, String collection) {
        // Extract each comma separated collection name and store in a List.
        List<String> rawCollectionsList = StrUtils.splitSmart(collection, ",", true);
        Set<String> collectionsList = new HashSet<>();
        // validate collections
        for (String collectionName : rawCollectionsList) {
            if (!clusterState.getCollections().contains(collectionName)) {
                Aliases aliases = zkStateReader.getAliases();
                String alias = aliases.getCollectionAlias(collectionName);
                if (alias != null) {
                    List<String> aliasList = StrUtils.splitSmart(alias, ",", true);
                    collectionsList.addAll(aliasList);
                    continue;
                }

                throw new SolrException(ErrorCode.BAD_REQUEST, "Collection not found: " + collectionName);
            }

            collectionsList.add(collectionName);
        }
        return collectionsList;
    }

    @Override
    public void shutdown() {
        if (zkStateReader != null) {
            synchronized (this) {
                if (zkStateReader != null)
                    zkStateReader.close();
                zkStateReader = null;
            }
        }

        if (shutdownLBHttpSolrServer) {
            lbServer.shutdown();
        }

        if (myClient != null) {
            myClient.getConnectionManager().shutdown();
        }

        if (this.threadPool != null && !this.threadPool.isShutdown()) {
            this.threadPool.shutdown();
        }
    }

    public LBHttpSolrServer getLbServer() {
        return lbServer;
    }

    public boolean isUpdatesToLeaders() {
        return updatesToLeaders;
    }

    /**
     * Useful for determining the minimum achieved replication factor across
     * all shards involved in processing an update request, typically useful
     * for gauging the replication factor of a batch. 
     */
    @SuppressWarnings("rawtypes")
    public int getMinAchievedReplicationFactor(String collection, NamedList resp) {
        // it's probably already on the top-level header set by condense
        NamedList header = (NamedList) resp.get("responseHeader");
        Integer achRf = (Integer) header.get(UpdateRequest.REPFACT);
        if (achRf != null)
            return achRf.intValue();

        // not on the top-level header, walk the shard route tree
        Map<String, Integer> shardRf = getShardReplicationFactor(collection, resp);
        for (Integer rf : shardRf.values()) {
            if (achRf == null || rf < achRf) {
                achRf = rf;
            }
        }
        return (achRf != null) ? achRf.intValue() : -1;
    }

    /**
     * Walks the NamedList response after performing an update request looking for
     * the replication factor that was achieved in each shard involved in the request.
     * For single doc updates, there will be only one shard in the return value. 
     */
    @SuppressWarnings("rawtypes")
    public Map<String, Integer> getShardReplicationFactor(String collection, NamedList resp) {
        connect();

        Map<String, Integer> results = new HashMap<String, Integer>();
        if (resp instanceof CloudSolrServer.RouteResponse) {
            NamedList routes = ((CloudSolrServer.RouteResponse) resp).getRouteResponses();
            ClusterState clusterState = zkStateReader.getClusterState();
            Map<String, String> leaders = new HashMap<String, String>();
            for (Slice slice : clusterState.getActiveSlices(collection)) {
                Replica leader = slice.getLeader();
                if (leader != null) {
                    ZkCoreNodeProps zkProps = new ZkCoreNodeProps(leader);
                    String leaderUrl = zkProps.getBaseUrl() + "/" + zkProps.getCoreName();
                    leaders.put(leaderUrl, slice.getName());
                    String altLeaderUrl = zkProps.getBaseUrl() + "/" + collection;
                    leaders.put(altLeaderUrl, slice.getName());
                }
            }

            Iterator<Map.Entry<String, Object>> routeIter = routes.iterator();
            while (routeIter.hasNext()) {
                Map.Entry<String, Object> next = routeIter.next();
                String host = next.getKey();
                NamedList hostResp = (NamedList) next.getValue();
                Integer rf = (Integer) ((NamedList) hostResp.get("responseHeader")).get(UpdateRequest.REPFACT);
                if (rf != null) {
                    String shard = leaders.get(host);
                    if (shard == null) {
                        if (host.endsWith("/"))
                            shard = leaders.get(host.substring(0, host.length() - 1));
                        if (shard == null) {
                            shard = host;
                        }
                    }
                    results.put(shard, rf);
                }
            }
        }
        return results;
    }
}