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(); }