All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.compass.core.lucene.engine.transaction.support.AbstractConcurrentTransactionProcessor 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.core.lucene.engine.transaction.support;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.compass.core.Resource;
import org.compass.core.engine.SearchEngineException;
import org.compass.core.lucene.engine.LuceneSearchEngine;
import org.compass.core.lucene.engine.LuceneSearchEngineHits;
import org.compass.core.lucene.engine.LuceneSearchEngineInternalSearch;
import org.compass.core.lucene.engine.LuceneSearchEngineQuery;
import org.compass.core.lucene.engine.transaction.support.job.CreateTransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.DeleteByQueryTransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.DeleteTransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.FlushCommitTransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.TransactionJob;
import org.compass.core.lucene.engine.transaction.support.job.UpdateTransactionJob;
import org.compass.core.spi.InternalResource;
import org.compass.core.spi.ResourceKey;

/**
 * Base class support for async dirty operation processing.
 *
 * 

Each dirty operation is added to a backlog for a specific thread to proces it (obtained from * {@link org.compass.core.executor.ExecutorManager}). A {@link org.compass.core.lucene.engine.transaction.support.AbstractConcurrentTransactionProcessor.Processor} * is assigned for each thread responsible for processing dirty operations. * *

Extedning classes should implement teh required operations and provide indication as to if search/read operations * should block until all dirty operations have been processed, and if concurrent operations are even allowed. In case * concurrent operations are not allowed, all dirty operations will be perfomed in a sync manner. * *

Different settings can control how the concurrent processing is perfomed. Settings names are based on * {@link #getName()} by using {@link #getSettingName(String)}. * *

The concurrentOperations setting can be used to disable concurrent dirty operations, or enable * them. This is only applies of the concurrentOperations parameter in the constructor is true. * *

The number of processor threads can be controlled using concurrencyLevel setting. It defaults to * 5 threads. * *

Operations are hashed to their respective processor thread for procesing. Hashing can be controlled to either * be perofmed based on uid or subindex. The default is uid. * *

The size of the backlog for each processor thread can also be controlled. If the backlog is full, user operations * will block until space becaomes available (by the respective processor processing the operations). The amount of time * the operation will block can be controlled using the addTimeout setting which defaults to 10 seconds. * * @author kimchy */ public abstract class AbstractConcurrentTransactionProcessor extends AbstractSearchTransactionProcessor { private final boolean waitForSearchOperations; private final boolean concurrentOperations; private final int concurrencyLevel; private Processor[] processors; private final ResourceHashing hashing; private final int backlog; private final long addTimeout; protected AbstractConcurrentTransactionProcessor(Log logger, LuceneSearchEngine searchEngine, boolean waitForSearchOperations, boolean concurrentOperations) { super(logger, searchEngine); this.waitForSearchOperations = waitForSearchOperations; this.concurrentOperations = concurrentOperations && searchEngine.getSettings().getSettingAsBoolean(getSettingName("concurrentOperations"), true); this.concurrencyLevel = searchEngine.getSettings().getSettingAsInt(getSettingName("concurrencyLevel"), 5); this.hashing = ResourceHashing.fromName(searchEngine.getSettings().getSetting(getSettingName("hashing"), "uid")); this.backlog = searchEngine.getSettings().getSettingAsInt(getSettingName("backlog"), 100); this.addTimeout = searchEngine.getSettings().getSettingAsTimeInMillis(getSettingName("addTimeout"), 10000); } /** * Returns true if concurrent operaetions are enabled for this transaction processor. */ public boolean isConcurrentOperations() { return concurrentOperations; } public void begin() throws SearchEngineException { // nothing to do here } public void prepare() throws SearchEngineException { if (concurrentOperations) { waitForJobs(); } doPrepare(); } /** * Sub classes should implement this. Behaviour should be the same as {@link #prepare()}. */ protected abstract void doPrepare() throws SearchEngineException; public void commit(boolean onePhase) throws SearchEngineException { if (concurrentOperations) { waitForJobs(); } doCommit(onePhase); } /** * Sub classes should implement this. Behaviour should be the same as {@link #commit(boolean)}. */ protected abstract void doCommit(boolean onePhase) throws SearchEngineException; public void rollback() throws SearchEngineException { clearJobs(); doRollback(); } /** * Sub classes should implement this. Behaviour should be the same as {@link #rollback()}. */ protected abstract void doRollback() throws SearchEngineException; public void flush() throws SearchEngineException { waitForJobs(); doFlush(); } protected void doFlush() throws SearchEngineException { } public void create(InternalResource resource) throws SearchEngineException { TransactionJob job = new CreateTransactionJob(resource); if (concurrentOperations) { prepareBeforeAsyncDirtyOperation(job); getProcessor(job).addJob(job); } else { doProcessJob(job); } } public void update(InternalResource resource) throws SearchEngineException { TransactionJob job = new UpdateTransactionJob(resource); if (concurrentOperations) { prepareBeforeAsyncDirtyOperation(job); getProcessor(job).addJob(job); } else { doProcessJob(job); } } public void delete(ResourceKey resourceKey) throws SearchEngineException { TransactionJob job = new DeleteTransactionJob(resourceKey); if (concurrentOperations) { prepareBeforeAsyncDirtyOperation(job); getProcessor(job).addJob(job); } else { doProcessJob(job); } } public void delete(LuceneSearchEngineQuery query) throws SearchEngineException { // we flush everything here so we maintain order flush(); String[] calcSubIndexes = indexManager.getStore().calcSubIndexes(query.getSubIndexes(), query.getAliases()); for (String subIndex : calcSubIndexes) { TransactionJob job = new DeleteByQueryTransactionJob(query.getQuery(), subIndex); if (concurrentOperations) { prepareBeforeAsyncDirtyOperation(job); getProcessor(job).addJob(job); } else { doProcessJob(job); } } } public void flushCommit(String... aliases) throws SearchEngineException { flush(); // intersect Set calcSubIndexes = new HashSet(); if (aliases == null || aliases.length == 0) { calcSubIndexes.addAll(Arrays.asList(getDirtySubIndexes())); } else { Set dirtySubIndxes = new HashSet(Arrays.asList(getDirtySubIndexes())); Set requiredSubIndexes = new HashSet(Arrays.asList(indexManager.polyCalcSubIndexes(null, aliases, null))); for (String subIndex : indexManager.getSubIndexes()) { if (dirtySubIndxes.contains(subIndex) && requiredSubIndexes.contains(subIndex)) { calcSubIndexes.add(subIndex); } } } for (String subIndex : calcSubIndexes) { TransactionJob job = new FlushCommitTransactionJob(subIndex); if (concurrentOperations) { prepareBeforeAsyncDirtyOperation(job); getProcessor(job).addJob(job); } else { doProcessJob(job); } } flush(); } /** * Returns the current dirty sub indexes. */ protected abstract String[] getDirtySubIndexes(); /** * Sub classes should implement the actual processing of a transactional job. */ protected abstract void doProcessJob(TransactionJob job) throws SearchEngineException; /** * Called by a single thread (the calling thread) before a dirty transaction job is added to the * queue to be executed in an async manner. */ protected abstract void prepareBeforeAsyncDirtyOperation(TransactionJob job) throws SearchEngineException; public LuceneSearchEngineHits find(LuceneSearchEngineQuery query) throws SearchEngineException { if (waitForSearchOperations && concurrentOperations) { waitForJobs(); } return doFind(query); } /** * Sub classes should implement this. Behaviour should be the same as {@link #find(org.compass.core.lucene.engine.LuceneSearchEngineQuery)}. */ protected abstract LuceneSearchEngineHits doFind(LuceneSearchEngineQuery query) throws SearchEngineException; public LuceneSearchEngineInternalSearch internalSearch(String[] subIndexes, String[] aliases) throws SearchEngineException { if (waitForSearchOperations && concurrentOperations) { waitForJobs(); } return doInternalSearch(subIndexes, aliases); } /** * Base classes should implement this. Behaviour should be the same as {@link #internalSearch(String[], String[])}. */ protected abstract LuceneSearchEngineInternalSearch doInternalSearch(String[] subIndexes, String[] aliases) throws SearchEngineException; public Resource[] get(ResourceKey resourceKey) throws SearchEngineException { if (waitForSearchOperations && concurrentOperations) { waitForJobs(); } return doGet(resourceKey); } /** * Base classes should implement this. Behaviour should be the same as {@link #get(org.compass.core.spi.ResourceKey)}. */ protected abstract Resource[] doGet(ResourceKey resourceKey) throws SearchEngineException; /** * Similar to {@link #waitForJobs()} except that it clears all the remaining jobs from execution and simply * waits for clean stop of the processors. Does not throw any processing exceptions, instead logs them since * this is usually called by {@link #rollback()}. */ private void clearJobs() { if (!concurrentOperations || processors == null) { return; } InterruptedException ie = null; int lastId = -1; for (Processor processor : processors) { if (processor != null) { // we clean before stop so we won't even process any remaining jobs processor.clear(); try { processor.stop(); } catch (InterruptedException e) { lastId = processor.getId(); ie = e; } } } if (ie != null) { logger.warn("Failed to wait for processor [" + lastId + "] to stop, interrupted", ie); } SearchEngineException exception = null; for (Processor processor : processors) { if (processor != null) { try { processor.waitTillStopped(); } catch (InterruptedException e) { throw new SearchEngineException("Failed to wait for processor [" + processor.getId() + "] to be stopped / process all jobs", e); } exception = processor.getException(); } } if (exception != null) { logger.trace("EXception while waiting to clear jobs for rollback", exception); } } /** * Waits for all the current dirty operations (if there are any) to be performed. Stops all the processors * as well. * *

If there were any exceptions during the processing of dirty operation by any processor, they will be * thrown. */ private void waitForJobs() throws SearchEngineException { if (!concurrentOperations || processors == null) { return; } InterruptedException ie = null; int lastId = -1; for (Processor processor : processors) { if (processor != null) { try { processor.stop(); } catch (InterruptedException e) { lastId = processor.getId(); ie = e; } } } if (ie != null) { logger.warn("Failed to wait for processor [" + lastId + "] to stop, interrupted", ie); } SearchEngineException exception = null; for (Processor processor : processors) { if (processor != null) { try { processor.waitTillStopped(); } catch (InterruptedException e) { throw new SearchEngineException("Failed to wait for processor [" + processor.getId() + "] to be stopped / process all jobs", e); } exception = processor.getException(); } } if (exception != null) { throw exception; } } /** * Returns the processor that should handle the specific {@link org.compass.core.lucene.engine.transaction.support.job.TransactionJob}. * If the processor has not been created, it will be lazily created. If the processor has not been scheduled yet * to a thread, it will also be scheduled to one. */ private Processor getProcessor(TransactionJob job) { if (processors == null) { processors = new Processor[concurrencyLevel]; } int processorIndex = hashing.hash(job) % concurrencyLevel; Processor processor = processors[processorIndex]; if (processor == null) { processor = new Processor(processorIndex); processors[processorIndex] = processor; } try { if (processor.needsReschedule()) { processor.start(); } } catch (InterruptedException e) { throw new SearchEngineException("Failed to wait for processor [" + processor.getId() + "] to check if stopped", e); } return processor; } /** * Processor attached to a thread and responsible for processing dirty operations assigned to it. */ private class Processor implements Runnable { private final BlockingQueue jobs = new LinkedBlockingQueue(backlog); private final int id; private volatile boolean stopped = true; private volatile CountDownLatch stopLatch; private volatile CountDownLatch startLatch; private volatile SearchEngineException exception; private Processor(int id) { this.id = id; } /** * Returns the id of the processor. */ public int getId() { return id; } /** * Returns an exception that happened during an execution of the processor. * *

Note, should be called after {@link #waitTillStopped()}. */ public SearchEngineException getException() { return exception; } /** * Returns true if the processor requires rescheduling to a thread. */ public boolean needsReschedule() throws InterruptedException { if (stopped) { waitTillStopped(); } return stopped; } /** * Starts the given processor, scheduling it to a thread as well. */ public void start() { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Starting"); } startLatch = new CountDownLatch(1); stopped = false; indexManager.getExecutorManager().submit(this); } /** * Stops the given processor. Note, stopping the processor will cause it to finish executing all the remaining * jobs and then exiting. */ public void stop() throws InterruptedException { if (stopped) { return; } if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Stopping"); } stopped = true; } /** * Clears the jobs associated with the processor. */ public void clear() { jobs.clear(); } /** * Wait till stop. Note, should be called only after {@link #stop()} was called. */ public void waitTillStopped() throws InterruptedException { if (startLatch != null) { startLatch.await(); } if (stopLatch != null) { stopLatch.await(); } } /** * Adds a job to the processor. If there is an execption that occured during the procesing of a previously * submitted job, the exception will be thrown. * *

If the backlog of the processor is full, will wait till space becomes avaialbe. */ public void addJob(TransactionJob job) throws SearchEngineException { if (exception != null) { throw exception; } try { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Adding Job [" + job + "]"); } boolean offered = jobs.offer(job, addTimeout, TimeUnit.MILLISECONDS); if (!offered) { throw new SearchEngineException("Processor [" + id + "]: Failed to add job [" + job + "] after [" + addTimeout + "ms] and backlog size [" + backlog + "]"); } } catch (InterruptedException e) { throw new SearchEngineException("Processor [" + id + "]: Failed to add job [" + job + "], interrupted while adding to queue", e); } } public void run() { try { // clear the exception, since this is a new run // note, calling getException is only done after we waitTillStopped (which is only done after stop) exception = null; stopLatch = new CountDownLatch(1); startLatch.countDown(); if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Started"); } while (!stopped) { TransactionJob job; try { job = jobs.poll(100, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { if (!stopped) { logger.warn("Processor [" + id + "]: Interrupted without being stopped", e); } break; } if (job != null) { try { processJob(job); } catch (SearchEngineException e) { exception = e; break; } } } if (exception != null) { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Stopping because of an exception", exception); } } else { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Received stop, processing remaining jobs"); } try { processRemainingJobs(); } catch (SearchEngineException e) { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Failed to processes remaining jobs", e); } exception = e; } } if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Stopped"); } } catch (Exception e) { logger.warn("Processor [" + id + "]: Recevied an unexpected exception", e); } finally { stopLatch.countDown(); } } private void processRemainingJobs() throws SearchEngineException { ArrayList remainingJobs = new ArrayList(); jobs.drainTo(remainingJobs); for (TransactionJob job : remainingJobs) { processJob(job); } } private void processJob(TransactionJob job) throws SearchEngineException { if (logger.isTraceEnabled()) { logger.trace("Processor [" + id + "]: Processing Job [" + job + "]"); } doProcessJob(job); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy