/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.solr.factory.impl; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.math.NumberUtils; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.response.CollectionAdminResponse; import org.apache.zookeeper.KeeperException; import org.codice.solr.factory.SolrClientFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.jodah.failsafe.Failsafe; import net.jodah.failsafe.RetryPolicy; /** * Factory class used to create new {@link CloudSolrClient} clients. * <br/> * Uses the following system properties when creating an instance: * <ul> * <li>solr.cloud.replicationFactor: Replication factor used when creating a new collection</li> * <li>solr.cloud.shardCount: Shard count used when creating a new collection</li> * <li>solr.cloud.maxShardPerNode: Maximum shard per node value used when creating a new collection</li> * <li>solr.cloud.zookeeper: Comma-separated list of Zookeeper hosts</li> * <li>org.codice.ddf.system.threadPoolSize: Solr query thread pool size</li> * </ul> */ public class SolrCloudClientFactory implements SolrClientFactory { private static final Logger LOGGER = LoggerFactory.getLogger(SolrCloudClientFactory.class); private static final int SHARD_COUNT = NumberUtils.toInt(System.getProperty( "solr.cloud.shardCount"), 2); private static final int REPLICATION_FACTOR = NumberUtils.toInt(System.getProperty( "solr.cloud.replicationFactor"), 2); private static final int MAXIMUM_SHARDS_PER_NODE = NumberUtils.toInt(System.getProperty( "solr.cloud.maxShardPerNode"), 2); private static final int THREAD_POOL_DEFAULT_SIZE = 128; private static final ScheduledExecutorService EXECUTOR_SERVICE = createExecutorService(); @Override public Future<SolrClient> newClient(String core) { String zookeeperHosts = System.getProperty("solr.cloud.zookeeper"); if (zookeeperHosts == null) { LOGGER.warn( "Cannot create Solr Cloud client without Zookeeper host list system property [solr.cloud.zookeeper] being set."); return null; } return getClient(zookeeperHosts, core); } /** * Creates a new {@link CloudSolrClient} using the list of Zookeeper hosts and collection name * provided. * * @param zookeeperHosts comma-separate list of Zookeeper hosts managing the Solr Cloud * configuration * @param collection name of the collection to create * @return {@code Future} used to retrieve the new {@link CloudSolrClient} instance */ public static Future<SolrClient> getClient(String zookeeperHosts, String collection) { RetryPolicy retryPolicy = new RetryPolicy().retryWhen(null) .withBackoff(10, TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS); return Failsafe.with(retryPolicy) .with(EXECUTOR_SERVICE) .onRetry((c, failure, ctx) -> LOGGER.debug( "Attempt {} failed to create Solr Cloud client for collection {} using Zookeeper hosts [{}]. Retrying again.", ctx.getExecutions(), collection, zookeeperHosts)) .onFailedAttempt(failure -> LOGGER.debug( "Attempt failed to create Solr Cloud client for collection {} using Zookeeper hosts [{}]", collection, zookeeperHosts, failure)) .onSuccess(client -> LOGGER.debug( "Successfully created Solr Cloud client for collection {}", collection)) .onFailure(failure -> LOGGER.warn( "All attempts failed to create Solr Cloud client for collection {} using Zookeeper host [{}]", collection, zookeeperHosts, failure)) .get(() -> createSolrCloudClient(zookeeperHosts, collection)); } private static ScheduledExecutorService createExecutorService() throws NumberFormatException { Integer threadPoolSize = NumberUtils.toInt(System.getProperty( "org.codice.ddf.system.threadPoolSize"), THREAD_POOL_DEFAULT_SIZE); return Executors.newScheduledThreadPool(threadPoolSize); } private static SolrClient createSolrCloudClient(String zookeeperHosts, String collection) { CloudSolrClient client = new CloudSolrClient(zookeeperHosts); client.connect(); try { uploadCoreConfiguration(collection, client); } catch (SolrFactoryException e) { LOGGER.debug("Unable to upload configuration to Solr Cloud", e); return null; } try { createCollection(collection, client); } catch (SolrFactoryException e) { LOGGER.debug("Unable to create collection on Solr Cloud", e); return null; } client.setDefaultCollection(collection); return client; } private static void createCollection(String collection, CloudSolrClient client) throws SolrFactoryException { try { CollectionAdminResponse response = new CollectionAdminRequest.List().process(client); if (response == null || response.getResponse() == null || response.getResponse() .get("collections") == null) { throw new SolrFactoryException("Failed to get a list of existing collections"); } List<String> collections = (List<String>) response.getResponse() .get("collections"); if (!collections.contains(collection)) { response = new CollectionAdminRequest.Create().setNumShards(SHARD_COUNT) .setMaxShardsPerNode(MAXIMUM_SHARDS_PER_NODE) .setReplicationFactor(REPLICATION_FACTOR) .setCollectionName(collection) .process(client); if (!response.isSuccess()) { throw new SolrFactoryException( "Failed to create collection [" + collection + "]: " + response.getErrorMessages()); } if (!isCollectionReady(client, collection)) { throw new SolrFactoryException( "Solr collection [" + collection + "] was not ready in time."); } } else { LOGGER.debug("Collection already exists: " + collection); } } catch (SolrServerException | IOException e) { throw new SolrFactoryException("Failed to create collection: " + collection, e); } } private static void uploadCoreConfiguration(String collection, CloudSolrClient client) throws SolrFactoryException { boolean configExistsInZk; try { configExistsInZk = client.getZkStateReader() .getZkClient() .exists("/configs/" + collection, true); } catch (KeeperException | InterruptedException e) { throw new SolrFactoryException( "Failed to check config status with Zookeeper for collection: " + collection, e); } if (!configExistsInZk) { ConfigurationStore configStore = ConfigurationStore.getInstance(); if (System.getProperty("solr.data.dir") != null) { configStore.setDataDirectoryPath(System.getProperty("solr.data.dir")); } ConfigurationFileProxy configProxy = new ConfigurationFileProxy(configStore); configProxy.writeSolrConfiguration(collection); Path configPath = Paths.get(configProxy.getDataDirectory() .getAbsolutePath(), collection, "conf"); try { client.uploadConfig(configPath, collection); } catch (IOException e) { throw new SolrFactoryException( "Failed to upload configurations for collection: " + collection, e); } } } private static boolean isCollectionReady(CloudSolrClient client, String collection) { RetryPolicy retryPolicy = new RetryPolicy().retryWhen(false) .withMaxRetries(30) .withDelay(1, TimeUnit.SECONDS); boolean collectionCreated = Failsafe.with(retryPolicy) .onFailure(failure -> LOGGER.debug( "All attempts failed to read Zookeeper state for collection existence (" + collection + ")", failure)) .get(() -> client.getZkStateReader() .getClusterState() .hasCollection(collection)); if (!collectionCreated) { LOGGER.debug("Timeout while waiting for collection to be created: " + collection); return false; } boolean shardsStarted = Failsafe.with(retryPolicy) .onFailure(failure -> LOGGER.debug( "All attempts failed to read Zookeeper state for collection's shard count (" + collection + ")", failure)) .get(() -> client.getZkStateReader() .getClusterState() .getSlices(collection) .size() == SHARD_COUNT); if (!shardsStarted) { LOGGER.debug("Timeout while waiting for collection shards to start: " + collection); } return shardsStarted; } }