org.compass.needle.terracotta.transaction.processor.TerracottaTransactionProcessorFactory Maven / Gradle / Ivy
/*
* 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.
*
* 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 false
.
*
*
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.
*
*
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}.
*
*
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.
*
*
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 currentProcessors = new ConcurrentHashMap();
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 subIndexJobs = holder.getJobsPerSubIndex().get(subIndex);
if (subIndexJobs == null) {
subIndexJobs = new LinkedBlockingQueue();
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 add(TransactionJobs jobs) {
Map subIndexesJobs = jobs.buildJobsPerSubIndex();
for (Map.Entry entry : subIndexesJobs.entrySet()) {
holder.getJobsPerSubIndex().get(entry.getKey()).add(entry.getValue());
}
return subIndexesJobs;
}
public void remove(Map subIndexesJobs) {
for (Map.Entry entry : subIndexesJobs.entrySet()) {
holder.getJobsPerSubIndex().get(entry.getKey()).remove(entry.getValue());
}
}
private class TerracottaProcessor implements Runnable {
private final BlockingQueue jobsToProcess;
private final String subIndex;
private volatile boolean running = true;
private TerracottaProcessor(String subIndex, BlockingQueue 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 jobsList = new ArrayList();
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() {
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();
}
}
}
}
}