org.compass.needle.terracotta.transaction.processor.TerracottaTransactionProcessorFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.compass.needle.terracotta.transaction.processor.TerracottaTransactionProcessorFactory.java

Source

/*
 * Copyright 2004-2009 the original author or authors.
 *
 * 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 org.compass.needle.terracotta.transaction.processor;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
import org.compass.core.CompassException;
import org.compass.core.config.CompassConfigurable;
import org.compass.core.config.CompassSettings;
import org.compass.core.config.SearchEngineFactoryAware;
import org.compass.core.engine.SearchEngineFactory;
import org.compass.core.lucene.engine.LuceneSearchEngine;
import org.compass.core.lucene.engine.LuceneSearchEngineFactory;
import org.compass.core.lucene.engine.transaction.TransactionProcessor;
import org.compass.core.lucene.engine.transaction.TransactionProcessorFactory;
import org.compass.core.lucene.engine.transaction.support.job.TransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.TransactionJobs;
import org.compass.core.transaction.context.TransactionContextCallback;
import org.compass.core.util.StringUtils;

/**
 * The terracotta transaction processor factory allows to add {@link org.compass.core.lucene.engine.transaction.support.job.TransactionJobs TransactionJobs}
 * to a shared work queue (partitioned by sub index) to be processed later by worker processors.
 *
 * <p>By default, the transaction processor factory acts as a worker processor as well. In order to disable it (and make
 * it only a producer node) set the {@link TerracottaTransactionProcessorEnvironment#PROCESS} to <code>false</code>.
 *
 * <p>By default, each worker processor node will try and processes jobs from all sub indexes (you can start as many
 * as you like). In order to pin down the worker processor to work only on specific sub indexes, set then using
 * {@link TerracottaTransactionProcessorEnvironment#SUB_INDEXES} setting.
 *
 * <p>The processor itself, once it identifies that there is a transactional job to be processed, will try and get
 * more transactional jobs in a non blocking manner for better utilization of an already opened IndexWriter. By default
 * it will try and get 5 more, and it can be controlled using {@link TerracottaTransactionProcessorEnvironment#NON_BLOCKING_BATCH_JOBS_SIZE}.
 *
 * <p>Transactions visibility (once a transaction commits, how long till the other nodes, including the one that committed
 * will see the result) can be controlld using {@link org.compass.core.lucene.LuceneEnvironment.SearchEngineIndex#CACHE_INTERVAL_INVALIDATION}.
 * Note, by default, refreshing to a new index happens in the background and does not affect the search nodes.
 *
 * <p>When working with several machines, the index should probably be shared between all nodes. The terracotta based
 * directory store can be used to share the index as well.
 *
 * @author kimchy
 */
public class TerracottaTransactionProcessorFactory
        implements TransactionProcessorFactory, CompassConfigurable, SearchEngineFactoryAware {

    private static transient final Log logger = LogFactory.getLog(TerracottaTransactionProcessorFactory.class);

    private final TerracottaHolder holder = new TerracottaHolder();

    private transient LuceneSearchEngineFactory searchEngineFactory;

    private transient CompassSettings settings;

    private final transient Map<String, TerracottaProcessor> currentProcessors = new ConcurrentHashMap<String, TerracottaProcessor>();

    private int batchJobsSize;

    private long batchJobTimeout;

    private int nonBlockingBatchSize;

    public void setSearchEngineFactory(SearchEngineFactory searchEngineFactory) {
        this.searchEngineFactory = (LuceneSearchEngineFactory) searchEngineFactory;
    }

    public void configure(CompassSettings settings) throws CompassException {
        this.settings = settings;
        batchJobsSize = settings.getSettingAsInt(TerracottaTransactionProcessorEnvironment.BATCH_JOBS_SIZE, 5);
        batchJobTimeout = settings
                .getSettingAsTimeInMillis(TerracottaTransactionProcessorEnvironment.BATCH_JOBS_SIZE, 100);
        if (logger.isDebugEnabled()) {
            logger.debug("Terracotta Transaction Processor blocking batch size is [" + batchJobsSize
                    + "] with timeout of [" + batchJobTimeout + "ms]");
        }
        nonBlockingBatchSize = settings
                .getSettingAsInt(TerracottaTransactionProcessorEnvironment.NON_BLOCKING_BATCH_JOBS_SIZE, 5);
        if (logger.isDebugEnabled()) {
            logger.debug(
                    "Terracotta Transaction Processor non blocking batch size is [" + nonBlockingBatchSize + "]");
        }
        holder.getInitializationLock().lock();
        try {
            for (String subIndex : searchEngineFactory.getIndexManager().getSubIndexes()) {
                BlockingQueue<TransactionJobs> subIndexJobs = holder.getJobsPerSubIndex().get(subIndex);
                if (subIndexJobs == null) {
                    subIndexJobs = new LinkedBlockingQueue<TransactionJobs>();
                    holder.getJobsPerSubIndex().put(subIndex, subIndexJobs);
                }
                Lock processorLock = holder.getProcessorLocks().get(subIndex);
                if (processorLock == null) {
                    processorLock = new ReentrantLock();
                    holder.getProcessorLocks().put(subIndex, processorLock);
                }
            }
        } finally {
            holder.getInitializationLock().unlock();
        }
        if (settings.getSettingAsBoolean(TerracottaTransactionProcessorEnvironment.PROCESS, true)) {
            String[] subIndexesSetting = StringUtils.commaDelimitedListToStringArray(
                    settings.getSetting(TerracottaTransactionProcessorEnvironment.SUB_INDEXES));
            if (subIndexesSetting.length == 0) {
                subIndexesSetting = null;
            }
            String[] aliasesSetting = StringUtils.commaDelimitedListToStringArray(
                    settings.getSetting(TerracottaTransactionProcessorEnvironment.ALIASES));
            if (aliasesSetting.length == 0) {
                aliasesSetting = null;
            }
            String[] subIndexes = searchEngineFactory.getIndexManager().calcSubIndexes(subIndexesSetting,
                    aliasesSetting, null);
            logger.info("Terracotta Transaction Processor Worker started. Sub indexes to process: "
                    + Arrays.toString(subIndexes));
            for (String subIndex : subIndexes) {
                TerracottaProcessor processor = new TerracottaProcessor(subIndex,
                        holder.getJobsPerSubIndex().get(subIndex));
                searchEngineFactory.getExecutorManager().submit(processor);
                currentProcessors.put(subIndex, processor);
            }
        } else {
            logger.info(
                    "Terracotta transaction processor will only submit transactions to be processed (none worker mode)");
        }
    }

    public TransactionProcessor create(LuceneSearchEngine searchEngine) {
        return new TerracottaTransactionProcessor(searchEngine, this);
    }

    public void close() {
        for (TerracottaProcessor processor : currentProcessors.values()) {
            processor.stop();
        }
    }

    /**
     * The terracotta transaction processor is not thread safe.
     */
    public boolean isThreadSafe() {
        return false;
    }

    public Map<String, TransactionJobs> add(TransactionJobs jobs) {
        Map<String, TransactionJobs> subIndexesJobs = jobs.buildJobsPerSubIndex();
        for (Map.Entry<String, TransactionJobs> entry : subIndexesJobs.entrySet()) {
            holder.getJobsPerSubIndex().get(entry.getKey()).add(entry.getValue());
        }
        return subIndexesJobs;
    }

    public void remove(Map<String, TransactionJobs> subIndexesJobs) {
        for (Map.Entry<String, TransactionJobs> entry : subIndexesJobs.entrySet()) {
            holder.getJobsPerSubIndex().get(entry.getKey()).remove(entry.getValue());
        }
    }

    private class TerracottaProcessor implements Runnable {

        private final BlockingQueue<TransactionJobs> jobsToProcess;

        private final String subIndex;

        private volatile boolean running = true;

        private TerracottaProcessor(String subIndex, BlockingQueue<TransactionJobs> jobsToProcess) {
            this.subIndex = subIndex;
            this.jobsToProcess = jobsToProcess;
        }

        public String getSubIndex() {
            return subIndex;
        }

        public void stop() {
            running = false;
        }

        private String message(String message) {
            return "Processor [" + subIndex + "]: " + message;
        }

        public void run() {
            while (running) {
                // each node locks and waits for jobs. This means that there are never
                // two processors for the same sub index waiting for (and potentially taking) jobs.
                // This also allows for several JVMs to run and be able to share the load for a specific
                // sub index (if they are handling more than one sub index)
                Lock processLock = holder.getProcessorLocks().get(subIndex);
                boolean locked = false; // create a shared lock for terracotta to process
                try {
                    locked = processLock.tryLock(1000, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    // we geto interupted, bail out
                    running = false;
                }
                if (!locked) {
                    continue;
                }

                try {
                    TransactionJobs jobs = null;
                    try {
                        jobs = jobsToProcess.poll(1000, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        // we geto interupted, bail out
                        running = false;
                    }
                    if (jobs == null) {
                        continue;
                    }
                    final List<TransactionJobs> jobsList = new ArrayList<TransactionJobs>();
                    jobsList.add(jobs);

                    for (int i = 0; i < batchJobsSize; i++) {
                        try {
                            jobs = jobsToProcess.poll(batchJobTimeout, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException e) {
                            // we geto interupted, bail out in the next run
                            running = false;
                        }
                        if (jobs == null) {
                            break;
                        }
                        if (logger.isTraceEnabled()) {
                            logger.trace("Batching additional Jobs [" + System.identityHashCode(jobs) + "]");
                        }
                        jobsList.add(jobs);
                    }

                    jobsToProcess.drainTo(jobsList, nonBlockingBatchSize);
                    if (logger.isDebugEnabled()) {
                        int totalJobs = 0;
                        for (TransactionJobs x : jobsList) {
                            totalJobs += x.getJobs().size();
                        }
                        logger.debug(message(
                                "procesing [" + jobsList.size() + "] transactions with [" + totalJobs + "] jobs"));
                    }

                    final TransactionJobs finalJobs = jobs;
                    searchEngineFactory.getTransactionContext().execute(new TransactionContextCallback<Boolean>() {
                        public Boolean doInTransaction() throws CompassException {
                            IndexWriter writer;
                            try {
                                writer = searchEngineFactory.getLuceneIndexManager().getIndexWritersManager()
                                        .openIndexWriter(settings, subIndex);
                                searchEngineFactory.getLuceneIndexManager().getIndexWritersManager()
                                        .trackOpenIndexWriter(subIndex, writer);
                            } catch (LockObtainFailedException e) {
                                // we failed to get a lock, probably another one running and getting it, which is bad!
                                logger.error(message(
                                        "Another instance is running on the sub index, make sure it does not. Should not happen really..."));
                                return false;
                            } catch (IOException e) {
                                logger.error(message("Failed to open index writer, dismissing jobs [" + finalJobs
                                        + "]. Should not happen really..."), e);
                                return false;
                            }
                            try {
                                for (TransactionJobs xJobs : jobsList) {
                                    for (TransactionJob job : xJobs.getJobs()) {
                                        job.execute(writer, searchEngineFactory);
                                    }
                                }
                                writer.commit();
                            } catch (Exception e) {
                                logger.error(message("Failed to process jobs [" + finalJobs + "]"), e);
                                try {
                                    writer.rollback();
                                } catch (IOException e1) {
                                    logger.warn(
                                            message("Failed to rollback transaction on jobs [" + finalJobs + "]"),
                                            e);
                                }
                            } finally {
                                try {
                                    writer.close();
                                } catch (IOException e) {
                                    logger.warn(message("Failed to close writer, ignoring"), e);
                                } finally {
                                    searchEngineFactory.getLuceneIndexManager().getIndexWritersManager()
                                            .trackCloseIndexWriter(subIndex, writer);
                                }
                            }
                            return null;
                        }
                    });
                } finally {
                    processLock.unlock();
                }
            }
        }
    }
}