com.ngdata.hbaseindexer.master.IndexerMaster.java Source code

Java tutorial

Introduction

Here is the source code for com.ngdata.hbaseindexer.master.IndexerMaster.java

Source

/*
 * Copyright 2013 NGDATA nv
 *
 * 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.
 */
package com.ngdata.hbaseindexer.master;

import static com.ngdata.hbaseindexer.model.api.IndexerModelEventType.INDEXER_ADDED;
import static com.ngdata.hbaseindexer.model.api.IndexerModelEventType.INDEXER_UPDATED;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.ngdata.hbaseindexer.ConfKeys;
import com.ngdata.hbaseindexer.SolrConnectionParams;
import com.ngdata.hbaseindexer.model.api.BatchBuildInfo;
import com.ngdata.hbaseindexer.model.api.IndexerDefinition;
import com.ngdata.hbaseindexer.model.api.IndexerDefinition.BatchIndexingState;
import com.ngdata.hbaseindexer.model.api.IndexerDefinition.IncrementalIndexingState;
import com.ngdata.hbaseindexer.model.api.IndexerDefinitionBuilder;
import com.ngdata.hbaseindexer.model.api.IndexerLifecycleListener;
import com.ngdata.hbaseindexer.model.api.IndexerModelEvent;
import com.ngdata.hbaseindexer.model.api.IndexerModelListener;
import com.ngdata.hbaseindexer.model.api.IndexerNotFoundException;
import com.ngdata.hbaseindexer.model.api.WriteableIndexerModel;
import com.ngdata.hbaseindexer.mr.HBaseMapReduceIndexerTool;
import com.ngdata.hbaseindexer.mr.JobProcessCallback;
import com.ngdata.hbaseindexer.util.solr.SolrConnectionParamUtil;
import com.ngdata.hbaseindexer.util.zookeeper.LeaderElection;
import com.ngdata.hbaseindexer.util.zookeeper.LeaderElectionCallback;
import com.ngdata.hbaseindexer.util.zookeeper.LeaderElectionSetupException;
import com.ngdata.sep.SepModel;
import com.ngdata.sep.util.io.Closer;
import com.ngdata.sep.util.zookeeper.ZooKeeperItf;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configurable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.RunningJob;
import org.apache.zookeeper.KeeperException;

/**
 * The indexer master is active on only one hbase-indexer node and is responsible for things such as launching
 * batch indexing jobs, assigning or removing SEP subscriptions, and the like.
 */
public class IndexerMaster {
    private final ZooKeeperItf zk;

    private final WriteableIndexerModel indexerModel;

    private final Configuration mapReduceConf;

    private final Configuration hbaseConf;

    private final String zkConnectString;

    // TODO
    //    private final SolrClientConfig solrClientConfig;

    private LeaderElection leaderElection;

    private IndexerModelListener listener = new MyListener();

    private final List<IndexerLifecycleListener> lifecycleListeners = Lists.newArrayList();

    private EventWorker eventWorker = new EventWorker();

    private JobClient jobClient;

    private final Log log = LogFactory.getLog(getClass());

    private SepModel sepModel;

    /**
     * Total number of IndexerModel events processed (useful in test cases).
     */
    private final AtomicInteger eventCount = new AtomicInteger();

    public IndexerMaster(ZooKeeperItf zk, WriteableIndexerModel indexerModel, Configuration mapReduceConf,
            Configuration hbaseConf, String zkConnectString, SepModel sepModel) {

        this.zk = zk;
        this.indexerModel = indexerModel;
        this.mapReduceConf = mapReduceConf;
        this.hbaseConf = hbaseConf;
        this.zkConnectString = zkConnectString;
        this.sepModel = sepModel;

        registerLifecycleListeners();
    }

    private void registerLifecycleListeners() {
        String listenerConf = hbaseConf.get("hbaseindexer.lifecycle.listeners", "");
        Iterable<String> classNames = Splitter.on(",").trimResults().omitEmptyStrings().split(listenerConf);
        for (String className : classNames) {
            try {
                Class<IndexerLifecycleListener> listenerClass = (Class<IndexerLifecycleListener>) getClass()
                        .getClassLoader().loadClass(className);
                registerLifecycleListener(listenerClass.newInstance());
            } catch (Exception e) {
                log.error("Could not add an instance of " + className + " to the indexerMaster lifecycle listeners."
                        + e);
            }
        }
    }

    @VisibleForTesting
    public void registerLifecycleListener(IndexerLifecycleListener indexerLifecycleListener) {
        if (indexerLifecycleListener instanceof Configurable) {
            ((Configurable) indexerLifecycleListener).setConf(hbaseConf);
        }

        lifecycleListeners.add(indexerLifecycleListener);
    }

    @PostConstruct
    public void start() throws LeaderElectionSetupException, IOException, InterruptedException, KeeperException {
        leaderElection = new LeaderElection(zk, "Indexer Master", hbaseConf.get(ConfKeys.ZK_ROOT_NODE) + "/masters",
                new MyLeaderElectionCallback());
    }

    @PreDestroy
    public void stop() {
        try {
            if (leaderElection != null)
                leaderElection.stop();
        } catch (InterruptedException e) {
            log.info("Interrupted while shutting down leader election.");
        }

        Closer.close(jobClient);
    }

    public int getEventCount() {
        return eventCount.intValue();
    }

    private synchronized JobClient getJobClient() throws IOException {
        if (jobClient == null) {
            jobClient = new JobClient(new JobConf(mapReduceConf));
        }
        return jobClient;
    }

    private class MyLeaderElectionCallback implements LeaderElectionCallback {
        @Override
        public void activateAsLeader() throws Exception {
            log.info("Starting up as indexer master.");

            // Start these processes, but it is not until we have registered our model listener
            // that these will receive work.
            eventWorker.start();

            Collection<IndexerDefinition> indexers = indexerModel.getIndexers(listener);

            // Rather than performing any work that might to be done for the indexers here,
            // we push out fake events. This way there's only one place where these actions
            // need to be performed.
            for (IndexerDefinition index : indexers) {
                eventWorker.putEvent(new IndexerModelEvent(INDEXER_UPDATED, index.getName()));
            }

            log.info("Startup as indexer master successful.");
        }

        @Override
        public void deactivateAsLeader() throws Exception {
            log.info("Shutting down as indexer master.");

            indexerModel.unregisterListener(listener);

            // Argument false for shutdown: we do not interrupt the event worker thread: if there
            // was something running there that is blocked until the ZK connection comes back up
            // we want it to finish (e.g. a lock taken that should be released again)
            eventWorker.shutdown(false);

            log.info("Shutdown as indexer master successful.");
        }
    }

    private boolean needsSubscriptionIdAssigned(IndexerDefinition indexer) {
        return !indexer.getLifecycleState().isDeleteState()
                && indexer.getIncrementalIndexingState() != IncrementalIndexingState.DO_NOT_SUBSCRIBE
                && indexer.getSubscriptionId() == null;
    }

    private boolean needsSubscriptionIdUnassigned(IndexerDefinition indexer) {
        return indexer.getIncrementalIndexingState() == IncrementalIndexingState.DO_NOT_SUBSCRIBE
                && indexer.getSubscriptionId() != null;
    }

    private boolean needsBatchBuildStart(IndexerDefinition indexer) {
        return !indexer.getLifecycleState().isDeleteState()
                && indexer.getBatchIndexingState() == BatchIndexingState.BUILD_REQUESTED
                && indexer.getActiveBatchBuildInfo() == null;
    }

    private void assignSubscription(String indexerName) {
        try {
            String lock = indexerModel.lockIndexer(indexerName);
            try {
                // Read current situation of record and assure it is still actual
                IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);
                if (needsSubscriptionIdAssigned(indexer)) {
                    // We assume we are the only process which creates subscriptions which begin with the
                    // prefix "Indexer:". This way we are sure there are no naming conflicts or conflicts
                    // due to concurrent operations (e.g. someone deleting this subscription right after we
                    // created it).
                    String subscriptionId = subscriptionId(indexer.getName());
                    sepModel.addSubscription(subscriptionId);
                    indexer = new IndexerDefinitionBuilder().startFrom(indexer).subscriptionId(subscriptionId)
                            .build();
                    indexerModel.updateIndexerInternal(indexer);
                    log.info("Assigned subscription ID '" + subscriptionId + "' to indexer '" + indexerName + "'");
                }
            } finally {
                indexerModel.unlockIndexer(lock);
            }
        } catch (Throwable t) {
            log.error("Error trying to assign a subscription to index " + indexerName, t);
        }
    }

    private void unassignSubscription(String indexerName) {
        try {
            String lock = indexerModel.lockIndexer(indexerName);
            try {
                // Read current situation of record and assure it is still actual
                IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);
                if (needsSubscriptionIdUnassigned(indexer)) {
                    sepModel.removeSubscription(indexer.getSubscriptionId());
                    log.info("Deleted queue subscription for indexer " + indexerName);
                    indexer = new IndexerDefinitionBuilder().startFrom(indexer).subscriptionId(null).build();
                    indexerModel.updateIndexerInternal(indexer);
                }
            } finally {
                indexerModel.unlockIndexer(lock);
            }
        } catch (Throwable t) {
            log.error("Error trying to delete the subscription for indexer " + indexerName, t);
        }
    }

    private String subscriptionId(String indexerName) {
        return "Indexer_" + indexerName;
    }

    private void startFullIndexBuild(final String indexerName) {
        try {
            String lock = indexerModel.lockIndexer(indexerName);
            try {
                // Read current situation of record and assure it is still actual
                final IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);
                IndexerDefinitionBuilder updatedIndexer = new IndexerDefinitionBuilder().startFrom(indexer);
                final String[] batchArguments = createBatchArguments(indexer);
                if (needsBatchBuildStart(indexer)) {
                    final ListeningExecutorService executor = MoreExecutors
                            .listeningDecorator(Executors.newSingleThreadExecutor());
                    ListenableFuture<Integer> future = executor.submit(new Callable<Integer>() {
                        @Override
                        public Integer call() throws Exception {
                            HBaseMapReduceIndexerTool tool = new HBaseMapReduceIndexerTool();
                            tool.setConf(hbaseConf);
                            return tool.run(batchArguments,
                                    new IndexerDefinitionUpdaterJobProgressCallback(indexerName));
                        }
                    });

                    Futures.addCallback(future, new FutureCallback<Integer>() {
                        @Override
                        public void onSuccess(Integer exitCode) {
                            markBatchBuildCompleted(indexerName, exitCode == 0);
                            executor.shutdownNow();
                        }

                        @Override
                        public void onFailure(Throwable throwable) {
                            log.error("batch index build failed", throwable);
                            markBatchBuildCompleted(indexerName, false);
                            executor.shutdownNow();
                        }
                    });

                    BatchBuildInfo jobInfo = new BatchBuildInfo(System.currentTimeMillis(), null, null,
                            batchArguments);
                    updatedIndexer.activeBatchBuildInfo(jobInfo).batchIndexingState(BatchIndexingState.BUILDING)
                            .batchIndexCliArguments(null).build();

                    indexerModel.updateIndexerInternal(updatedIndexer.build());

                    log.info("Started batch index build for index " + indexerName);

                }
            } finally {
                indexerModel.unlockIndexer(lock);
            }
        } catch (Throwable t) {
            log.error("Error trying to start index build job for index " + indexerName, t);
        }
    }

    private void markBatchBuildCompleted(String indexerName, boolean success) {
        try {
            // Lock internal bypasses the index-in-delete-state check, which does not matter (and might cause
            // failure) in our case.
            String lock = indexerModel.lockIndexerInternal(indexerName, false);
            try {
                // Read current situation of record and assure it is still actual
                IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);

                BatchBuildInfo activeJobInfo = indexer.getActiveBatchBuildInfo();

                if (activeJobInfo == null) {
                    // This might happen if we got some older update event on the indexer right after we
                    // marked this job as finished.
                    log.warn(
                            "Unexpected situation: indexer batch build completed but indexer does not have an active"
                                    + " build job. Index: " + indexer.getName() + ". Ignoring this event.");
                    return;
                }

                BatchBuildInfo batchBuildInfo = new BatchBuildInfo(activeJobInfo);
                batchBuildInfo = batchBuildInfo.finishedSuccessfully(success);

                indexer = new IndexerDefinitionBuilder().startFrom(indexer).lastBatchBuildInfo(batchBuildInfo)
                        .activeBatchBuildInfo(null).batchIndexingState(BatchIndexingState.INACTIVE).build();

                indexerModel.updateIndexerInternal(indexer);

                log.info("Marked indexer batch build as finished for indexer " + indexerName);
            } finally {
                indexerModel.unlockIndexer(lock, true);
            }
        } catch (Throwable t) {
            log.error("Error trying to mark index batch build as finished for indexer " + indexerName, t);
        }
    }

    private String[] createBatchArguments(IndexerDefinition indexer) {
        String[] batchIndexArguments = indexer.getBatchIndexCliArgumentsOrDefault();
        List<String> args = Lists.newArrayList();

        String mode = Optional.fromNullable(indexer.getConnectionParams().get(SolrConnectionParams.MODE))
                .or("cloud").toLowerCase();

        if ("cloud".equals(mode)) { // cloud mode is the default
            args.add("--zk-host");
            args.add(indexer.getConnectionParams().get(SolrConnectionParams.ZOOKEEPER));
        } else {
            for (String shard : SolrConnectionParamUtil.getShards(indexer.getConnectionParams())) {
                args.add("--shard-url");
                args.add(shard);
            }
        }
        args.add("--hbase-indexer-zk");
        args.add(zkConnectString);

        args.add("--hbase-indexer-name");
        args.add(indexer.getName());

        args.add("--reducers");
        args.add("0");

        // additional arguments that were configured on the index (e.g. HBase scan parameters)
        args.addAll(Lists.newArrayList(batchIndexArguments));

        return args.toArray(new String[args.size()]);
    }

    private void prepareDeleteIndex(String indexerName) {
        // We do not have to take a lock on the indexer, since once in delete state the indexer cannot
        // be modified anymore by ordinary users.
        boolean canBeDeleted = false;
        try {
            // Read current situation of record and assure it is still actual
            IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);
            if (indexer.getLifecycleState() == IndexerDefinition.LifecycleState.DELETE_REQUESTED) {
                canBeDeleted = true;

                String queueSubscriptionId = indexer.getSubscriptionId();
                if (queueSubscriptionId != null) {
                    sepModel.removeSubscription(indexer.getSubscriptionId());
                    // We leave the subscription ID in the indexer definition FYI
                }

                if (indexer.getActiveBatchBuildInfo() != null) {
                    JobClient jobClient = getJobClient();
                    Set<String> jobs = indexer.getActiveBatchBuildInfo().getMapReduceJobTrackingUrls().keySet();
                    for (String jobId : jobs) {
                        RunningJob job = jobClient.getJob(jobId);
                        if (job != null) {
                            job.killJob();
                            log.info("Kill indexer build job for indexer " + indexerName + ", job ID =  " + jobId);
                        }
                        canBeDeleted = false;
                    }
                }

                if (!canBeDeleted) {
                    indexer = new IndexerDefinitionBuilder().startFrom(indexer)
                            .lifecycleState(IndexerDefinition.LifecycleState.DELETING).build();
                    indexerModel.updateIndexerInternal(indexer);
                }
            } else if (indexer.getLifecycleState() == IndexerDefinition.LifecycleState.DELETING) {
                // Check if the build job is already finished, if so, allow delete
                if (indexer.getActiveBatchBuildInfo() == null) {
                    canBeDeleted = true;
                }
            }
        } catch (Throwable t) {
            log.error("Error preparing deletion of indexer " + indexerName, t);
        }

        if (canBeDeleted) {
            deleteIndexer(indexerName);
        }
    }

    private void deleteIndexer(String indexerName) {
        // delete model
        boolean failedToDeleteIndexer = false;
        try {
            indexerModel.deleteIndexerInternal(indexerName);
        } catch (Throwable t) {
            log.error("Failed to delete indexer " + indexerName, t);
            failedToDeleteIndexer = true;
        }

        if (failedToDeleteIndexer) {
            try {
                IndexerDefinition indexer = indexerModel.getFreshIndexer(indexerName);
                indexer = new IndexerDefinitionBuilder().startFrom(indexer)
                        .lifecycleState(IndexerDefinition.LifecycleState.DELETE_FAILED).build();
                indexerModel.updateIndexerInternal(indexer);
            } catch (Throwable t) {
                log.error("Failed to set indexer state to " + IndexerDefinition.LifecycleState.DELETE_FAILED, t);
            }
        }
    }

    private class MyListener implements IndexerModelListener {
        @Override
        public void process(IndexerModelEvent event) {
            try {
                // Let the events be processed by another thread. Especially important since
                // we take ZkLock's in the event handlers (see ZkLock javadoc).
                eventWorker.putEvent(event);
            } catch (InterruptedException e) {
                log.info("IndexerMaster.IndexerModelListener interrupted.");
            }
        }
    }

    private class EventWorker implements Runnable {

        private BlockingQueue<IndexerModelEvent> eventQueue = new LinkedBlockingQueue<IndexerModelEvent>();

        private boolean stop;

        private Thread thread;

        public synchronized void shutdown(boolean interrupt) throws InterruptedException {
            stop = true;
            eventQueue.clear();

            if (!thread.isAlive()) {
                return;
            }

            if (interrupt)
                thread.interrupt();
            thread.join();
            thread = null;
        }

        public synchronized void start() throws InterruptedException {
            if (thread != null) {
                log.warn("EventWorker start was requested, but old thread was still there. Stopping it now.");
                thread.interrupt();
                thread.join();
            }
            eventQueue.clear();
            stop = false;
            thread = new Thread(this, "IndexerMasterEventWorker");
            thread.start();
        }

        public void putEvent(IndexerModelEvent event) throws InterruptedException {
            if (stop) {
                throw new RuntimeException("This EventWorker is stopped, no events should be added.");
            }
            eventQueue.put(event);
        }

        @Override
        public void run() {
            long startedAt = System.currentTimeMillis();

            while (!stop && !Thread.interrupted()) {
                try {
                    IndexerModelEvent event = null;
                    while (!stop && event == null) {
                        event = eventQueue.poll(1000, TimeUnit.MILLISECONDS);
                    }

                    if (stop || event == null || Thread.interrupted()) {
                        return;
                    }

                    // Warn if the queue is getting large, but do not do this just after we started, because
                    // on initial startup a fake update event is added for every defined index, which would lead
                    // to this message always being printed on startup when more than 10 indexes are defined.
                    int queueSize = eventQueue.size();
                    if (queueSize >= 10 && (System.currentTimeMillis() - startedAt > 5000)) {
                        log.warn("EventWorker queue getting large, size = " + queueSize);
                    }

                    if (event.getType() == INDEXER_ADDED || event.getType() == INDEXER_UPDATED) {
                        IndexerDefinition indexer = null;
                        try {
                            indexer = indexerModel.getIndexer(event.getIndexerName());
                        } catch (IndexerNotFoundException e) {
                            // ignore, indexer has meanwhile been deleted, we will get another event for this
                        }

                        if (indexer != null) {
                            if (indexer.getLifecycleState() == IndexerDefinition.LifecycleState.DELETE_REQUESTED
                                    || indexer.getLifecycleState() == IndexerDefinition.LifecycleState.DELETING) {
                                prepareDeleteIndex(indexer.getName());
                                for (IndexerLifecycleListener lifecycleListener : lifecycleListeners) {
                                    lifecycleListener.onDelete(indexer);
                                }

                                // in case of delete, we do not need to handle any other cases
                            } else {
                                if (needsSubscriptionIdAssigned(indexer)) {
                                    assignSubscription(indexer.getName());
                                    for (IndexerLifecycleListener lifecycleListener : lifecycleListeners) {
                                        lifecycleListener.onSubscribe(indexer);
                                    }
                                }

                                if (needsSubscriptionIdUnassigned(indexer)) {
                                    unassignSubscription(indexer.getName());
                                    for (IndexerLifecycleListener lifecycleListener : lifecycleListeners) {
                                        lifecycleListener.onUnsubscribe(indexer);
                                    }
                                }

                                if (needsBatchBuildStart(indexer)) {
                                    startFullIndexBuild(indexer.getName());
                                    for (IndexerLifecycleListener lifecycleListener : lifecycleListeners) {
                                        lifecycleListener.onBatchBuild(indexer);
                                    }
                                }
                            }
                        }
                    }
                    eventCount.incrementAndGet();
                } catch (InterruptedException e) {
                    return;
                } catch (Throwable t) {
                    log.error("Error processing indexer model event in IndexerMaster.", t);
                }
            }
        }
    }

    /**
     * Updates the index definition with information about MR jobs that are involved in the batch build (tracking urls mainly).
     */
    private final class IndexerDefinitionUpdaterJobProgressCallback implements JobProcessCallback {

        private final String indexerName;

        private IndexerDefinitionUpdaterJobProgressCallback(String indexerName) {
            this.indexerName = indexerName;
        }

        @Override
        public void jobStarted(String jobId, String trackingUrl) {
            try {
                // Lock internal bypasses the index-in-delete-state check, which does not matter (and might cause
                // failure) in our case.
                String lock = indexerModel.lockIndexerInternal(indexerName, false);
                try {
                    IndexerDefinition definition = indexerModel.getFreshIndexer(indexerName);
                    BatchBuildInfo batchBuildInfo = new BatchBuildInfo(definition.getActiveBatchBuildInfo())
                            .withJob(jobId, trackingUrl);
                    IndexerDefinition updatedDefinition = new IndexerDefinitionBuilder().startFrom(definition)
                            .activeBatchBuildInfo(batchBuildInfo).build();
                    indexerModel.updateIndexerInternal(updatedDefinition);

                    log.info("Updated indexer batch build state for indexer " + indexerName);
                } finally {
                    indexerModel.unlockIndexer(lock, true);
                }
            } catch (Exception e) {
                log.error("failed to update indexer batch build state for indexer " + indexerName);
            }
        }
    }
}