/* * (C) Copyright 2014 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; import static org.nuxeo.elasticsearch.ElasticSearchConstants.ES_ENABLED_PROPERTY; import static org.nuxeo.elasticsearch.ElasticSearchConstants.INDEXING_QUEUE_ID; import static org.nuxeo.elasticsearch.ElasticSearchConstants.REINDEX_ON_STARTUP_PROPERTY; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import javax.transaction.Transaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.elasticsearch.client.Client; import org.elasticsearch.index.query.QueryBuilder; import org.nuxeo.ecm.automation.jaxrs.io.documents.JsonESDocumentWriter; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.SortInfo; import org.nuxeo.ecm.core.repository.RepositoryService; import org.nuxeo.ecm.core.work.api.Work; import org.nuxeo.ecm.core.work.api.WorkManager; import org.nuxeo.elasticsearch.api.ESClientInitializationService; import org.nuxeo.elasticsearch.api.ElasticSearchAdmin; import org.nuxeo.elasticsearch.api.ElasticSearchIndexing; import org.nuxeo.elasticsearch.api.ElasticSearchService; import org.nuxeo.elasticsearch.api.EsResult; import org.nuxeo.elasticsearch.api.EsScrollResult; import org.nuxeo.elasticsearch.commands.IndexingCommand; import org.nuxeo.elasticsearch.config.ESClientInitializationDescriptor; import org.nuxeo.elasticsearch.config.ElasticSearchDocWriterDescriptor; import org.nuxeo.elasticsearch.config.ElasticSearchIndexConfig; import org.nuxeo.elasticsearch.config.ElasticSearchLocalConfig; import org.nuxeo.elasticsearch.config.ElasticSearchRemoteConfig; import org.nuxeo.elasticsearch.core.ElasticSearchAdminImpl; import org.nuxeo.elasticsearch.core.ElasticSearchIndexingImpl; import org.nuxeo.elasticsearch.core.ElasticSearchServiceImpl; import org.nuxeo.elasticsearch.query.NxQueryBuilder; import org.nuxeo.elasticsearch.work.IndexingWorker; import org.nuxeo.elasticsearch.work.ScrollingIndexingWorker; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.ComponentContext; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.transaction.TransactionHelper; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; /** * Component used to configure and manage ElasticSearch integration */ public class ElasticSearchComponent extends DefaultComponent implements ElasticSearchAdmin, ElasticSearchIndexing, ElasticSearchService { private static final Log log = LogFactory.getLog(ElasticSearchComponent.class); private static final String EP_REMOTE = "elasticSearchRemote"; private static final String EP_LOCAL = "elasticSearchLocal"; private static final String EP_INDEX = "elasticSearchIndex"; private static final String EP_DOC_WRITER = "elasticSearchDocWriter"; private static final String EP_CLIENT_INIT = "elasticSearchClientInitialization"; private static final long REINDEX_TIMEOUT = 20; // Indexing commands that where received before the index initialization private final List<IndexingCommand> stackedCommands = Collections.synchronizedList(new ArrayList<>()); private final Map<String, ElasticSearchIndexConfig> indexConfig = new HashMap<>(); private ElasticSearchLocalConfig localConfig; private ElasticSearchRemoteConfig remoteConfig; private ElasticSearchAdminImpl esa; private ElasticSearchIndexingImpl esi; private ElasticSearchServiceImpl ess; protected JsonESDocumentWriter jsonESDocumentWriter; protected ESClientInitializationService clientInitService; private ListeningExecutorService waiterExecutorService; private final AtomicInteger runIndexingWorkerCount = new AtomicInteger(0); // Nuxeo Component impl ======================================é============= @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { switch (extensionPoint) { case EP_LOCAL: ElasticSearchLocalConfig localContrib = (ElasticSearchLocalConfig) contribution; if (localContrib.isEnabled()) { localConfig = localContrib; remoteConfig = null; log.info("Registering local embedded configuration: " + localConfig + ", loaded from " + contributor.getName()); } else if (localConfig != null) { log.info("Disabling previous local embedded configuration, deactivated by " + contributor.getName()); localConfig = null; } break; case EP_REMOTE: ElasticSearchRemoteConfig remoteContribution = (ElasticSearchRemoteConfig) contribution; if (remoteContribution.isEnabled()) { remoteConfig = remoteContribution; localConfig = null; log.info( "Registering remote configuration: " + remoteConfig + ", loaded from " + contributor.getName()); } else if (remoteConfig != null) { log.info("Disabling previous remote configuration, deactivated by " + contributor.getName()); remoteConfig = null; } break; case EP_INDEX: ElasticSearchIndexConfig idx = (ElasticSearchIndexConfig) contribution; ElasticSearchIndexConfig previous = indexConfig.get(idx.getName()); if (idx.isEnabled()) { idx.merge(previous); indexConfig.put(idx.getName(), idx); log.info("Registering index configuration: " + idx + ", loaded from " + contributor.getName()); } else if (previous != null) { log.info("Disabling index configuration: " + previous + ", deactivated by " + contributor.getName()); indexConfig.remove(idx.getName()); } break; case EP_DOC_WRITER: ElasticSearchDocWriterDescriptor writerDescriptor = (ElasticSearchDocWriterDescriptor) contribution; try { jsonESDocumentWriter = writerDescriptor.getKlass().newInstance(); } catch (IllegalAccessException | InstantiationException e) { log.error("Can not instantiate jsonESDocumentWriter from " + writerDescriptor.getKlass()); throw new RuntimeException(e); } break; case EP_CLIENT_INIT: ESClientInitializationDescriptor clientInitDescriptor = (ESClientInitializationDescriptor) contribution; try { clientInitService = clientInitDescriptor.getKlass().newInstance(); clientInitService.setUsername(clientInitDescriptor.getUsername()); clientInitService.setPassword(clientInitDescriptor.getPassword()); } catch (IllegalAccessException | InstantiationException e) { log.error( "Can not instantiate ES Client initialization service from " + clientInitDescriptor.getKlass()); throw new RuntimeException(e); } break; default: throw new IllegalStateException("Invalid EP: " + extensionPoint); } } @Override public void applicationStarted(ComponentContext context) { if (!isElasticsearchEnabled()) { log.info("Elasticsearch service is disabled"); return; } esa = new ElasticSearchAdminImpl(localConfig, remoteConfig, indexConfig, clientInitService); esi = new ElasticSearchIndexingImpl(esa, jsonESDocumentWriter); ess = new ElasticSearchServiceImpl(esa); initListenerThreadPool(); processStackedCommands(); reindexOnStartup(); } @Override public void applicationStopped(ComponentContext context, Instant deadline) { try { shutdownListenerThreadPool(); } finally { try { esa.disconnect(); } finally { esa = null; esi = null; ess = null; } } } private void reindexOnStartup() { boolean reindexOnStartup = Boolean.parseBoolean(Framework.getProperty(REINDEX_ON_STARTUP_PROPERTY, "false")); if (!reindexOnStartup) { return; } for (String repositoryName : esa.getInitializedRepositories()) { log.warn(String.format("Indexing repository: %s on startup", repositoryName)); runReindexingWorker(repositoryName, "SELECT ecm:uuid FROM Document"); try { prepareWaitForIndexing().get(REINDEX_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { log.error(e.getMessage(), e); } catch (TimeoutException e) { log.warn(String.format("Indexation of repository %s not finished after %d s, continuing in background", repositoryName, REINDEX_TIMEOUT)); } } } protected boolean isElasticsearchEnabled() { return Boolean.parseBoolean(Framework.getProperty(ES_ENABLED_PROPERTY, "true")); } @Override public int getApplicationStartedOrder() { RepositoryService component = (RepositoryService) Framework.getRuntime().getComponent( "org.nuxeo.ecm.core.repository.RepositoryServiceComponent"); return component.getApplicationStartedOrder() / 2; } void processStackedCommands() { if (!stackedCommands.isEmpty()) { log.info(String.format("Processing %d indexing commands stacked during startup", stackedCommands.size())); runIndexingWorker(stackedCommands); stackedCommands.clear(); log.debug("Done"); } } // Es Admin ================================================================ @Override public Client getClient() { return esa.getClient(); } @Override public void initIndexes(boolean dropIfExists) { esa.initIndexes(dropIfExists); } @Override public void dropAndInitIndex(String indexName) { esa.dropAndInitIndex(indexName); } @Override public void dropAndInitRepositoryIndex(String repositoryName) { esa.dropAndInitRepositoryIndex(repositoryName); } @Override public List<String> getRepositoryNames() { return esa.getRepositoryNames(); } @Override public String getIndexNameForRepository(String repositoryName) { return esa.getIndexNameForRepository(repositoryName); } @Override public List<String> getIndexNamesForType(String type) { return esa.getIndexNamesForType(type); } @Override public String getIndexNameForType(String type) { return esa.getIndexNameForType(type); } @SuppressWarnings("deprecation") @Override public long getPendingWorkerCount() { WorkManager wm = Framework.getLocalService(WorkManager.class); // api is deprecated for completed work return wm.getQueueSize(INDEXING_QUEUE_ID, Work.State.SCHEDULED); } @SuppressWarnings("deprecation") @Override public long getRunningWorkerCount() { WorkManager wm = Framework.getLocalService(WorkManager.class); // api is deprecated for completed work return runIndexingWorkerCount.get() + wm.getQueueSize(INDEXING_QUEUE_ID, Work.State.RUNNING); } @Override public int getTotalCommandProcessed() { return esa.getTotalCommandProcessed(); } @Override public boolean isEmbedded() { return esa.isEmbedded(); } @Override public boolean useExternalVersion() { return esa.useExternalVersion(); } @Override public boolean isIndexingInProgress() { return (runIndexingWorkerCount.get() > 0) || (getPendingWorkerCount() > 0) || (getRunningWorkerCount() > 0); } @Override public ListenableFuture<Boolean> prepareWaitForIndexing() { return waiterExecutorService.submit(new Callable<Boolean>() { @Override public Boolean call() throws Exception { WorkManager wm = Framework.getLocalService(WorkManager.class); boolean completed = false; do { completed = wm.awaitCompletion(INDEXING_QUEUE_ID, 300, TimeUnit.SECONDS); } while (!completed); return true; } }); } private static class NamedThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { return new Thread(r, "waitForEsIndexing"); } } protected void initListenerThreadPool() { waiterExecutorService = MoreExecutors.listeningDecorator( Executors.newCachedThreadPool(new NamedThreadFactory())); } protected void shutdownListenerThreadPool() { try { waiterExecutorService.shutdown(); } finally { waiterExecutorService = null; } } @Override public void refresh() { esa.refresh(); } @Override public void refreshRepositoryIndex(String repositoryName) { esa.refreshRepositoryIndex(repositoryName); } @Override public void flush() { esa.flush(); } @Override public void flushRepositoryIndex(String repositoryName) { esa.flushRepositoryIndex(repositoryName); } @Override public void optimize() { esa.optimize(); } @Override public void optimizeRepositoryIndex(String repositoryName) { esa.optimizeRepositoryIndex(repositoryName); } @Override public void optimizeIndex(String indexName) { esa.optimizeIndex(indexName); } // ES Indexing ============================================================= @Override public void indexNonRecursive(IndexingCommand cmd) { indexNonRecursive(Collections.singletonList(cmd)); } @Override public void indexNonRecursive(List<IndexingCommand> cmds) { if (!isReady()) { stackCommands(cmds); return; } if (log.isDebugEnabled()) { log.debug("Process indexing commands: " + Arrays.toString(cmds.toArray())); } esi.indexNonRecursive(cmds); } protected void stackCommands(List<IndexingCommand> cmds) { if (log.isDebugEnabled()) { log.debug("Delaying indexing commands: Waiting for Index to be initialized." + Arrays.toString(cmds.toArray())); } stackedCommands.addAll(cmds); } @Override public void runIndexingWorker(List<IndexingCommand> cmds) { if (!isReady()) { stackCommands(cmds); return; } runIndexingWorkerCount.incrementAndGet(); try { dispatchWork(cmds); } finally { runIndexingWorkerCount.decrementAndGet(); } } /** * Dispatch jobs between sync and async worker */ protected void dispatchWork(List<IndexingCommand> cmds) { Map<String, List<IndexingCommand>> syncCommands = new HashMap<>(); Map<String, List<IndexingCommand>> asyncCommands = new HashMap<>(); for (IndexingCommand cmd : cmds) { if (cmd.isSync()) { List<IndexingCommand> syncCmds = syncCommands.get(cmd.getRepositoryName()); if (syncCmds == null) { syncCmds = new ArrayList<>(); } syncCmds.add(cmd); syncCommands.put(cmd.getRepositoryName(), syncCmds); } else { List<IndexingCommand> asyncCmds = asyncCommands.get(cmd.getRepositoryName()); if (asyncCmds == null) { asyncCmds = new ArrayList<>(); } asyncCmds.add(cmd); asyncCommands.put(cmd.getRepositoryName(), asyncCmds); } } runIndexingSyncWorker(syncCommands); scheduleIndexingAsyncWorker(asyncCommands); } protected void scheduleIndexingAsyncWorker(Map<String, List<IndexingCommand>> asyncCommands) { if (asyncCommands.isEmpty()) { return; } WorkManager wm = Framework.getLocalService(WorkManager.class); for (String repositoryName : asyncCommands.keySet()) { IndexingWorker idxWork = new IndexingWorker(repositoryName, asyncCommands.get(repositoryName)); // we are in afterCompletion don't wait for a commit wm.schedule(idxWork, false); } } protected void runIndexingSyncWorker(Map<String, List<IndexingCommand>> syncCommands) { if (syncCommands.isEmpty()) { return; } Transaction transaction = TransactionHelper.suspendTransaction(); try { for (String repositoryName : syncCommands.keySet()) { IndexingWorker idxWork = new IndexingWorker(repositoryName, syncCommands.get(repositoryName)); idxWork.run(); } } finally { if (transaction != null) { TransactionHelper.resumeTransaction(transaction); } } } @Override public void runReindexingWorker(String repositoryName, String nxql) { if (nxql == null || nxql.isEmpty()) { throw new IllegalArgumentException("Expecting an NXQL query"); } ScrollingIndexingWorker worker = new ScrollingIndexingWorker(repositoryName, nxql); WorkManager wm = Framework.getLocalService(WorkManager.class); wm.schedule(worker); } // ES Search =============================================================== @Override public DocumentModelList query(NxQueryBuilder queryBuilder) { return ess.query(queryBuilder); } @Override public EsResult queryAndAggregate(NxQueryBuilder queryBuilder) { return ess.queryAndAggregate(queryBuilder); } @Override public EsScrollResult scroll(NxQueryBuilder queryBuilder, long keepAlive) { return ess.scroll(queryBuilder, keepAlive); } @Override public EsScrollResult scroll(EsScrollResult scrollResult) { return ess.scroll(scrollResult); } @Override public void clearScroll(EsScrollResult scrollResult) { ess.clearScroll(scrollResult); } @Deprecated @Override public DocumentModelList query(CoreSession session, String nxql, int limit, int offset, SortInfo... sortInfos) { NxQueryBuilder query = new NxQueryBuilder(session).nxql(nxql).limit(limit).offset(offset).addSort(sortInfos); return query(query); } @Deprecated @Override public DocumentModelList query(CoreSession session, QueryBuilder queryBuilder, int limit, int offset, SortInfo... sortInfos) { NxQueryBuilder query = new NxQueryBuilder(session).esQuery(queryBuilder) .limit(limit) .offset(offset) .addSort(sortInfos); return query(query); } // misc ==================================================================== private boolean isReady() { return (esa != null) && esa.isReady(); } }