package com.senseidb.svc.impl;
import com.senseidb.metrics.MetricFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.log4j.Logger;
import org.apache.lucene.util.NamedThreadFactory;
import org.jboss.netty.util.internal.ConcurrentHashMap;
import proj.zoie.api.IndexReaderFactory;
import proj.zoie.api.ZoieIndexReader;
import com.browseengine.bobo.api.BoboIndexReader;
import com.linkedin.norbert.network.Serializer;
import com.senseidb.metrics.MetricsConstants;
import com.senseidb.search.node.SenseiCore;
import com.senseidb.search.node.SenseiQueryBuilderFactory;
import com.senseidb.search.req.AbstractSenseiRequest;
import com.senseidb.search.req.AbstractSenseiResult;
import com.senseidb.search.req.ErrorType;
import com.senseidb.search.req.SenseiError;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.Timer;
public abstract class AbstractSenseiCoreService<Req extends AbstractSenseiRequest,Res extends AbstractSenseiResult>{
private final static Logger logger = Logger.getLogger(AbstractSenseiCoreService.class);
private final Timer _getReaderTimer;
private final Timer _searchTimer;
private final Timer _mergeTimer;
private final Meter _searchCounter;
protected long _timeout = 8000;
protected final SenseiCore _core;
private final NamedThreadFactory threadFactory = new NamedThreadFactory("parallel-searcher");
private final ExecutorService _executorService = Executors.newCachedThreadPool(threadFactory);
private final Map<Integer,Timer> partitionTimerMetricMap = new HashMap<Integer,Timer>();
public AbstractSenseiCoreService(SenseiCore core){
_core = core;
_getReaderTimer = registerTimer("getreader-time");
_searchTimer = registerTimer("search-time");
_mergeTimer = registerTimer("merge-time");
// TODO: requets is a mis-spell. Can we fix it?
_searchCounter = registerMeter("search-count", "requets");
}
private Timer buildTimer(int partition) {
MetricName partitionSearchMetricName = new MetricName(MetricsConstants.Domain,"timer","partition-time-"+partition,"partition");
return MetricFactory.newTimer(partitionSearchMetricName,TimeUnit.MILLISECONDS,TimeUnit.SECONDS);
}
private Timer getTimer(int partition) {
Timer timer = partitionTimerMetricMap.get(partition);
if(timer == null) {
partitionTimerMetricMap.put(partition, buildTimer(partition));
return getTimer(partition);
}
return timer;
}
public final Res execute(final Req senseiReq){
_searchCounter.mark();
Set<Integer> partitions = senseiReq==null ? null : senseiReq.getPartitions();
if (partitions==null){
partitions = new HashSet<Integer>();
int[] containsPart = _core.getPartitions();
if (containsPart!=null){
for (int part : containsPart){
partitions.add(part);
}
}
}
Res finalResult;
if (partitions != null && partitions.size() > 0)
{
if (logger.isDebugEnabled()){
logger.debug("serving partitions: " + partitions.toString());
}
//we need to release index readers from all partitions only after the merge step
final Map<IndexReaderFactory<ZoieIndexReader<BoboIndexReader>>, List<ZoieIndexReader<BoboIndexReader>>> indexReaderCache = new ConcurrentHashMap<IndexReaderFactory<ZoieIndexReader<BoboIndexReader>>, List<ZoieIndexReader<BoboIndexReader>>>();
try {
final ArrayList<Res> resultList = new ArrayList<Res>(partitions.size());
Future<Res>[] futures = new Future[partitions.size()-1];
int i = 0;
for (final int partition : partitions)
{
final long start = System.currentTimeMillis();
final IndexReaderFactory<ZoieIndexReader<BoboIndexReader>> readerFactory = _core.getIndexReaderFactory(partition);
if (i < partitions.size() - 1) // Search simultaneously.
{
try
{
futures[i] = (Future<Res>)_executorService.submit(new Callable<Res>()
{
public Res call() throws Exception
{
Timer timer = getTimer(partition);
Res res = timer.time(new Callable<Res>(){
@Override
public Res call() throws Exception {
return handleRequest(senseiReq, readerFactory, _core.getQueryBuilderFactory(), indexReaderCache);
}
});
long end = System.currentTimeMillis();
res.setTime(end - start);
logger.info("searching partition: " + partition + " browse took: " + res.getTime());
return res;
}
});
} catch (Exception e)
{
senseiReq.addError(new SenseiError(e.getMessage(), ErrorType.BoboExecutionError));
logger.error(e.getMessage(), e);
}
}
else // Reuse current thread.
{
try
{
Timer timer = getTimer(partition);
Res res = timer.time(new Callable<Res>(){
@Override
public Res call() throws Exception {
return handleRequest(senseiReq, readerFactory, _core.getQueryBuilderFactory(), indexReaderCache);
}
});
resultList.add(res);
long end = System.currentTimeMillis();
res.setTime(end - start);
logger.info("searching partition: " + partition + " browse took: " + res.getTime());
} catch (Exception e)
{
logger.error(e.getMessage(), e);
senseiReq.addError(new SenseiError(e.getMessage(), ErrorType.BoboExecutionError));
resultList.add(getEmptyResultInstance(e));
}
}
++i;
}
for (i=0; i<futures.length; ++i)
{
try
{
Res res = futures[i].get(_timeout, TimeUnit.MILLISECONDS);
resultList.add(res);
}
catch(Exception e)
{
logger.error(e.getMessage(), e);
if (e instanceof TimeoutException) {
senseiReq.addError(new SenseiError(e.getMessage(), ErrorType.ExecutionTimeout));
} else {
senseiReq.addError(new SenseiError(e.getMessage(), ErrorType.BoboExecutionError));
}
resultList.add(getEmptyResultInstance(e));
}
}
try{
finalResult = _mergeTimer.time(new Callable<Res>(){
public Res call() throws Exception{
return mergePartitionedResults(senseiReq, resultList);
}
});
}
catch(Exception e){
logger.error(e.getMessage(),e);
finalResult = getEmptyResultInstance(null);
finalResult.addError(new SenseiError(e.getMessage(), ErrorType.MergePartitionError));
}
} finally {
returnIndexReaders(indexReaderCache);
}
}
else
{
if (logger.isInfoEnabled()){
logger.info("no partitions specified");
}
finalResult = getEmptyResultInstance(null);
finalResult.addError(new SenseiError("no partitions specified", ErrorType.PartitionCallError));
}
if (logger.isInfoEnabled()){
logger.info("searching partitions: " + String.valueOf(partitions) + "; route by: " + senseiReq.getRouteParam() + "; took: " + finalResult.getTime());
}
return finalResult;
}
private void returnIndexReaders(Map<IndexReaderFactory<ZoieIndexReader<BoboIndexReader>>, List<ZoieIndexReader<BoboIndexReader>>> indexReaderCache) {
for (IndexReaderFactory<ZoieIndexReader<BoboIndexReader>> indexReaderFactory : indexReaderCache.keySet()) {
indexReaderFactory.returnIndexReaders(indexReaderCache.get(indexReaderFactory));
}
}
private final Res handleRequest(final Req senseiReq, final IndexReaderFactory<ZoieIndexReader<BoboIndexReader>> readerFactory,
final SenseiQueryBuilderFactory queryBuilderFactory,
Map<IndexReaderFactory<ZoieIndexReader<BoboIndexReader>>, List<ZoieIndexReader<BoboIndexReader>>> indexReadersToCleanUp) throws Exception {
List<ZoieIndexReader<BoboIndexReader>> readerList = null;
readerList = _getReaderTimer.time(new Callable<List<ZoieIndexReader<BoboIndexReader>>>() {
public List<ZoieIndexReader<BoboIndexReader>> call() throws Exception {
if (readerFactory == null)
return Collections.EMPTY_LIST;
return readerFactory.getIndexReaders();
}
});
if (logger.isDebugEnabled()) {
logger.debug("obtained readerList of size: " + readerList == null ? 0 : readerList.size());
}
if (readerFactory != null && readerList != null) {
indexReadersToCleanUp.put(readerFactory, readerList);
}
final List<BoboIndexReader> boboReaders = ZoieIndexReader.extractDecoratedReaders(readerList);
return _searchTimer.time(new Callable<Res>() {
public Res call() throws Exception {
return handlePartitionedRequest(senseiReq, boboReaders, queryBuilderFactory);
}
});
}
protected final Timer registerTimer(String name)
{
return MetricFactory.newTimer(new MetricName(MetricsConstants.Domain, "timer", name, getMetricScope()),
TimeUnit.MILLISECONDS,
TimeUnit.SECONDS);
}
protected final Meter registerMeter(String name, String eventType)
{
return MetricFactory.newMeter(new MetricName(MetricsConstants.Domain, "meter", name, getMetricScope()), eventType, TimeUnit.SECONDS);
}
public abstract Res handlePartitionedRequest(Req r,final List<BoboIndexReader> readerList,SenseiQueryBuilderFactory queryBuilderFactory) throws Exception;
public abstract Res mergePartitionedResults(Req r,List<Res> reqList);
public abstract Res getEmptyResultInstance(Throwable error);
public abstract Serializer<Req, Res> getSerializer();
/**
* Returns the name of the metric scope. It's used for creating {@link MetricName} that get registered through
* {@link MetricFactory}.
*/
protected abstract String getMetricScope();
}