/*
* (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Tiry
* bdelbosc
*/
package org.nuxeo.elasticsearch.core;
import static org.nuxeo.elasticsearch.ElasticSearchConstants.ALL_FIELDS;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.NoNodeAvailableException;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.Settings.Builder;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.nuxeo.elasticsearch.api.ESClientInitializationService;
import org.nuxeo.elasticsearch.api.ElasticSearchAdmin;
import org.nuxeo.elasticsearch.config.ElasticSearchIndexConfig;
import org.nuxeo.elasticsearch.config.ElasticSearchLocalConfig;
import org.nuxeo.elasticsearch.config.ElasticSearchRemoteConfig;
import org.nuxeo.runtime.api.Framework;
import com.google.common.util.concurrent.ListenableFuture;
/**
* @since 6.0
*/
public class ElasticSearchAdminImpl implements ElasticSearchAdmin {
private static final Log log = LogFactory.getLog(ElasticSearchAdminImpl.class);
private static final String TIMEOUT_WAIT_FOR_CLUSTER = "30s";
final AtomicInteger totalCommandProcessed = new AtomicInteger(0);
private final Map<String, String> indexNames = new HashMap<>();
private final Map<String, String> repoNames = new HashMap<>();
private final Map<String, ElasticSearchIndexConfig> indexConfig;
private Node localNode;
private Client client;
private boolean indexInitDone = false;
private final ElasticSearchLocalConfig localConfig;
private final ElasticSearchRemoteConfig remoteConfig;
private final ESClientInitializationService clientInitService;
private String[] includeSourceFields;
private String[] excludeSourceFields;
private boolean embedded = true;
private List<String> repositoryInitialized = new ArrayList<>();
/**
* Init the admin service, remote configuration if not null will take precedence over local embedded configuration.
*
* @deprecated since 9.1, use {@link #ElasticSearchAdminImpl(ElasticSearchLocalConfig, ElasticSearchRemoteConfig,
* Map<String, ElasticSearchIndexConfig>, ESClientInitializationService)} instead.
*/
@Deprecated
public ElasticSearchAdminImpl(ElasticSearchLocalConfig localConfig, ElasticSearchRemoteConfig remoteConfig,
Map<String, ElasticSearchIndexConfig> indexConfig) {
this(localConfig, remoteConfig, indexConfig, null);
}
/**
* Init the admin service, remote configuration if not null will take precedence over local embedded configuration.
* The transport client initialization can be customized.
*
* @since 9.1
*/
public ElasticSearchAdminImpl(ElasticSearchLocalConfig localConfig, ElasticSearchRemoteConfig remoteConfig,
Map<String, ElasticSearchIndexConfig> indexConfig, ESClientInitializationService clientInitService) {
this.remoteConfig = remoteConfig;
this.localConfig = localConfig;
this.indexConfig = indexConfig;
this.clientInitService = clientInitService;
connect();
initializeIndexes();
}
private void connect() {
if (client != null) {
return;
}
if (remoteConfig != null) {
client = connectToRemote(remoteConfig);
embedded = false;
} else {
localNode = createEmbeddedNode(localConfig);
client = connectToEmbedded();
embedded = true;
}
checkClusterHealth();
log.info("ES Connected");
}
public void disconnect() {
if (client != null) {
client.close();
client = null;
indexInitDone = false;
log.info("ES Disconnected");
}
if (localNode != null) {
localNode.close();
localNode = null;
log.info("ES embedded Node Stopped");
}
}
private Node createEmbeddedNode(ElasticSearchLocalConfig conf) {
log.info("ES embedded Node Initializing (local in JVM)");
if (conf == null) {
throw new IllegalStateException("No embedded configuration defined");
}
if (!Framework.isTestModeSet()) {
log.warn("Elasticsearch embedded configuration is ONLY for testing"
+ " purpose. You need to create a dedicated Elasticsearch" + " cluster for production.");
}
Builder sBuilder = Settings.settingsBuilder();
sBuilder.put("http.enabled", conf.httpEnabled())
.put("network.host", conf.getNetworkHost())
.put("path.home", conf.getHomePath())
.put("path.data", conf.getDataPath())
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put("cluster.name", conf.getClusterName())
.put("node.name", conf.getNodeName())
.put("http.netty.worker_count", 4)
.put("http.cors.enabled", true)
.put("http.cors.allow-origin", "*")
.put("http.cors.allow-credentials", true)
.put("http.cors.allow-headers", "Authorization, X-Requested-With, Content-Type, Content-Length")
.put("cluster.routing.allocation.disk.threshold_enabled", false)
.put("http.port", conf.getHttpPort());
if (conf.getIndexStorageType() != null) {
sBuilder.put("index.store.type", conf.getIndexStorageType());
}
Settings settings = sBuilder.build();
log.debug("Using settings: " + settings.toDelimitedString(','));
Node ret = NodeBuilder.nodeBuilder().local(true).settings(settings).node();
assert ret != null : "Can not create an embedded ES Node";
return ret;
}
private Client connectToEmbedded() {
log.info("Connecting to embedded ES");
Client ret = localNode.start().client();
assert ret != null : "Can not connect to embedded ES Node";
return ret;
}
private Client connectToRemote(ElasticSearchRemoteConfig config) {
log.info("Connecting to remote ES cluster: " + config);
Settings settings = clientInitService.initializeSettings(config);
if (log.isDebugEnabled()) {
log.debug("Using settings: " + settings.toDelimitedString(','));
}
TransportClient ret = clientInitService.initializeClient(settings);
String[] addresses = config.getAddresses();
if (addresses == null) {
log.error("You need to provide an addressList to join a cluster");
} else {
for (String item : config.getAddresses()) {
String[] address = item.split(":");
log.debug("Add transport address: " + item);
try {
InetAddress inet = InetAddress.getByName(address[0]);
ret.addTransportAddress(new InetSocketTransportAddress(inet, Integer.parseInt(address[1])));
} catch (UnknownHostException e) {
log.error("Unable to resolve host " + address[0], e);
}
}
}
assert ret != null : "Unable to create a remote client";
return ret;
}
private void checkClusterHealth(String... indexNames) {
if (client == null) {
throw new IllegalStateException("No es client available");
}
String errorMessage = null;
try {
log.debug("Waiting for cluster yellow health status, indexes: " + Arrays.toString(indexNames));
ClusterHealthResponse ret = client.admin()
.cluster()
.prepareHealth(indexNames)
.setTimeout(TIMEOUT_WAIT_FOR_CLUSTER)
.setWaitForYellowStatus()
.get();
if (ret.isTimedOut()) {
errorMessage = "ES Cluster health status not Yellow after " + TIMEOUT_WAIT_FOR_CLUSTER + " give up: "
+ ret;
} else {
if ((indexNames.length > 0) && ret.getStatus() != ClusterHealthStatus.GREEN) {
log.warn("Es Cluster ready but not GREEN: " + ret);
} else {
log.info("ES Cluster ready: " + ret);
}
}
} catch (NoNodeAvailableException e) {
errorMessage = "Failed to connect to elasticsearch, check addressList and clusterName: " + e.getMessage();
}
if (errorMessage != null) {
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
}
private void initializeIndexes() {
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
if (conf.isDocumentIndex()) {
log.info("Associate index " + conf.getName() + " with repository: " + conf.getRepositoryName());
indexNames.put(conf.getRepositoryName(), conf.getName());
repoNames.put(conf.getName(), conf.getRepositoryName());
Set<String> set = new LinkedHashSet<>();
if (includeSourceFields != null) {
set.addAll(Arrays.asList(includeSourceFields));
}
set.addAll(Arrays.asList(conf.getIncludes()));
if (set.contains(ALL_FIELDS)) {
set.clear();
set.add(ALL_FIELDS);
}
includeSourceFields = set.toArray(new String[set.size()]);
set.clear();
if (excludeSourceFields != null) {
set.addAll(Arrays.asList(excludeSourceFields));
}
set.addAll(Arrays.asList(conf.getExcludes()));
excludeSourceFields = set.toArray(new String[set.size()]);
}
}
initIndexes(false);
}
// Admin Impl =============================================================
@Override
public void refreshRepositoryIndex(String repositoryName) {
if (log.isDebugEnabled()) {
log.debug("Refreshing index associated with repo: " + repositoryName);
}
getClient().admin().indices().prepareRefresh(getIndexNameForRepository(repositoryName)).execute().actionGet();
if (log.isDebugEnabled()) {
log.debug("Refreshing index done");
}
}
@Override
public String getIndexNameForRepository(String repositoryName) {
String ret = indexNames.get(repositoryName);
if (ret == null) {
throw new NoSuchElementException("No index defined for repository: " + repositoryName);
}
return ret;
}
@Override
public List<String> getIndexNamesForType(String type) {
List<String> indexNames = new ArrayList<>();
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
if (type.equals(conf.getType())) {
indexNames.add(conf.getName());
}
}
return indexNames;
}
@Override
public String getIndexNameForType(String type) {
List<String> indexNames = getIndexNamesForType(type);
if (indexNames.isEmpty()) {
throw new NoSuchElementException("No index defined for type: " + type);
}
return indexNames.get(0);
}
@Override
public void flushRepositoryIndex(String repositoryName) {
log.warn("Flushing index associated with repo: " + repositoryName);
getClient().admin().indices().prepareFlush(getIndexNameForRepository(repositoryName)).execute().actionGet();
log.info("Flushing index done");
}
@Override
public void refresh() {
for (String repositoryName : indexNames.keySet()) {
refreshRepositoryIndex(repositoryName);
}
}
@Override
public void flush() {
for (String repositoryName : indexNames.keySet()) {
flushRepositoryIndex(repositoryName);
}
}
@Override
public void optimizeIndex(String indexName) {
log.warn("Optimizing index: " + indexName);
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
if (conf.getName().equals(indexName)) {
getClient().admin().indices().prepareForceMerge(indexName).get();
}
}
log.info("Optimize done");
}
@Override
public void optimizeRepositoryIndex(String repositoryName) {
optimizeIndex(getIndexNameForRepository(repositoryName));
}
@Override
public void optimize() {
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
optimizeIndex(conf.getName());
}
}
@Override
public Client getClient() {
return client;
}
@Override
public void initIndexes(boolean dropIfExists) {
indexInitDone = false;
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
initIndex(conf, dropIfExists);
}
log.info("ES Service ready");
indexInitDone = true;
}
@Override
public void dropAndInitIndex(String indexName) {
log.info("Drop and init index: " + indexName);
indexInitDone = false;
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
if (conf.getName().equals(indexName)) {
initIndex(conf, true);
}
}
indexInitDone = true;
}
@Override
public void dropAndInitRepositoryIndex(String repositoryName) {
log.info("Drop and init index of repository: " + repositoryName);
indexInitDone = false;
for (ElasticSearchIndexConfig conf : indexConfig.values()) {
if (conf.isDocumentIndex() && repositoryName.equals(conf.getRepositoryName())) {
initIndex(conf, true);
}
}
indexInitDone = true;
}
@Override
public List<String> getRepositoryNames() {
return Collections.unmodifiableList(new ArrayList<>(indexNames.keySet()));
}
void initIndex(ElasticSearchIndexConfig conf, boolean dropIfExists) {
if (!conf.mustCreate()) {
return;
}
log.info(String.format("Initialize index: %s, type: %s", conf.getName(), conf.getType()));
boolean mappingExists = false;
boolean indexExists = getClient().admin()
.indices()
.prepareExists(conf.getName())
.execute()
.actionGet()
.isExists();
if (indexExists) {
if (!dropIfExists) {
log.debug("Index " + conf.getName() + " already exists");
mappingExists = getClient().admin()
.indices()
.prepareGetMappings(conf.getName())
.execute()
.actionGet()
.getMappings()
.get(conf.getName())
.containsKey(conf.getType());
} else {
if (!Framework.isTestModeSet()) {
log.warn(String.format(
"Initializing index: %s, type: %s with " + "dropIfExists flag, deleting an existing index",
conf.getName(), conf.getType()));
}
getClient().admin().indices().delete(new DeleteIndexRequest(conf.getName())).actionGet();
indexExists = false;
}
}
if (!indexExists) {
log.info(String.format("Creating index: %s", conf.getName()));
if (log.isDebugEnabled()) {
log.debug("Using settings: " + conf.getSettings());
}
getClient().admin()
.indices()
.prepareCreate(conf.getName())
.setSettings(conf.getSettings())
.execute()
.actionGet();
}
if (!mappingExists) {
log.info(String.format("Creating mapping type: %s on index: %s", conf.getType(), conf.getName()));
if (log.isDebugEnabled()) {
log.debug("Using mapping: " + conf.getMapping());
}
getClient().admin()
.indices()
.preparePutMapping(conf.getName())
.setType(conf.getType())
.setSource(conf.getMapping())
.execute()
.actionGet();
if (!dropIfExists && conf.getRepositoryName() != null) {
repositoryInitialized.add(conf.getRepositoryName());
}
}
// make sure the index is ready before returning
checkClusterHealth(conf.getName());
}
@Override
public long getPendingWorkerCount() {
// impl of scheduling is left to the ESService
throw new UnsupportedOperationException("Not implemented");
}
@Override
public long getRunningWorkerCount() {
// impl of scheduling is left to the ESService
throw new UnsupportedOperationException("Not implemented");
}
@Override
public int getTotalCommandProcessed() {
return totalCommandProcessed.get();
}
@Override
public boolean isEmbedded() {
return embedded;
}
@Override
public boolean useExternalVersion() {
if (isEmbedded()) {
return localConfig.useExternalVersion();
}
return remoteConfig.useExternalVersion();
}
@Override
public boolean isIndexingInProgress() {
// impl of scheduling is left to the ESService
throw new UnsupportedOperationException("Not implemented");
}
@Override
public ListenableFuture<Boolean> prepareWaitForIndexing() {
throw new UnsupportedOperationException("Not implemented");
}
/**
* Get the elastic search indexes for searches
*/
String[] getSearchIndexes(List<String> searchRepositories) {
if (searchRepositories.isEmpty()) {
Collection<String> values = indexNames.values();
return values.toArray(new String[values.size()]);
}
String[] ret = new String[searchRepositories.size()];
int i = 0;
for (String repo : searchRepositories) {
ret[i++] = getIndexNameForRepository(repo);
}
return ret;
}
public boolean isReady() {
return indexInitDone;
}
String[] getIncludeSourceFields() {
return includeSourceFields;
}
String[] getExcludeSourceFields() {
return excludeSourceFields;
}
Map<String, String> getRepositoryMap() {
return repoNames;
}
/**
* Get the list of repository names that have their index created.
*/
public List<String> getInitializedRepositories() {
return repositoryInitialized;
}
}