/** * This software is licensed to you under the Apache License, Version 2.0 (the * "Apache License"). * * LinkedIn's contributions are made under the Apache License. If you contribute * to the Software, the contributions will be deemed to have been made under the * Apache License, unless you expressly indicate otherwise. Please do not make any * contributions that would be inconsistent with the Apache License. * * You may obtain a copy of the Apache License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, this software * distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Apache * License for the specific language governing permissions and limitations for the * software governed under the Apache License. * * © 2012 LinkedIn Corp. All Rights Reserved. */ package com.senseidb.search.node; import com.linkedin.norbert.javacompat.network.RequestBuilder; import com.linkedin.norbert.network.ResponseIterator; import com.linkedin.norbert.network.common.ExceptionIterator; import com.linkedin.norbert.network.common.PartialIterator; import com.linkedin.norbert.network.common.TimeoutIterator; import com.senseidb.search.req.*; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.RandomStringUtils; import org.apache.log4j.Logger; import com.linkedin.norbert.NorbertException; import com.linkedin.norbert.javacompat.cluster.Node; import com.linkedin.norbert.javacompat.network.PartitionedNetworkClient; import com.linkedin.norbert.javacompat.network.RequestBuilder; import com.linkedin.norbert.network.ResponseIterator; import com.linkedin.norbert.network.Serializer; import com.linkedin.norbert.network.common.ExceptionIterator; import com.linkedin.norbert.network.common.PartialIterator; import com.linkedin.norbert.network.common.TimeoutIterator; import com.senseidb.metrics.MetricFactory; import com.senseidb.metrics.MetricsConstants; 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.senseidb.search.req.SenseiRequest; import com.senseidb.svc.api.SenseiException; import com.yammer.metrics.core.Meter; import com.yammer.metrics.core.MetricName; import com.yammer.metrics.core.Timer; /** * @author "Xiaoyang Gu<xgu@linkedin.com>" * * @param <REQUEST> * @param <RESULT> */ public abstract class AbstractConsistentHashBroker<REQUEST extends AbstractSenseiRequest, RESULT extends AbstractSenseiResult> extends AbstractSenseiBroker<REQUEST, RESULT> { private final static Logger logger = Logger.getLogger(AbstractConsistentHashBroker.class); protected final long _timeout; protected final Serializer<REQUEST, RESULT> _serializer; private final Timer _scatterTimer; private final Timer _gatherTimer; private final Timer _totalTimer; private final Meter _searchCounter; private final Meter _errorMeter; private final Meter _emptyMeter; /** * @param networkClient * @param clusterClient * @param routerFactory * @param serializer * The serializer used to serialize/deserialize request/response pairs * @param scatterGatherHandler * @throws NorbertException */ public AbstractConsistentHashBroker(PartitionedNetworkClient<String> networkClient, Serializer<REQUEST, RESULT> serializer, long timeoutMillis) throws NorbertException { super(networkClient); _serializer = serializer; _timeout = timeoutMillis; // register metrics monitoring for timers MetricName scatterMetricName = new MetricName(MetricsConstants.Domain,"timer","scatter-time","broker"); _scatterTimer = MetricFactory.newTimer(scatterMetricName, TimeUnit.MILLISECONDS, TimeUnit.SECONDS); MetricName gatherMetricName = new MetricName(MetricsConstants.Domain,"timer","gather-time","broker"); _gatherTimer = MetricFactory.newTimer(gatherMetricName, TimeUnit.MILLISECONDS,TimeUnit.SECONDS); MetricName totalMetricName = new MetricName(MetricsConstants.Domain,"timer","total-time","broker"); _totalTimer = MetricFactory.newTimer(totalMetricName, TimeUnit.MILLISECONDS,TimeUnit.SECONDS); MetricName searchCounterMetricName = new MetricName(MetricsConstants.Domain,"meter","search-count","broker"); _searchCounter = MetricFactory.newMeter(searchCounterMetricName, "requets", TimeUnit.SECONDS); MetricName errorMetricName = new MetricName(MetricsConstants.Domain,"meter","error-meter","broker"); _errorMeter = MetricFactory.newMeter(errorMetricName, "errors",TimeUnit.SECONDS); MetricName emptyMetricName = new MetricName(MetricsConstants.Domain,"meter","empty-meter","broker"); _emptyMeter = MetricFactory.newMeter(emptyMetricName, "null-hits", TimeUnit.SECONDS); } public <T> T customizeRequest(REQUEST request) { return (T) request; } protected IntSet getPartitions(Set<Node> nodes) { IntSet partitionSet = new IntOpenHashSet(); for (Node n : nodes) { partitionSet.addAll(n.getPartitionIds()); } return partitionSet; } /** * @return an empty result instance. Used when the request cannot be properly * processed or when the true result is empty. */ public abstract RESULT getEmptyResultInstance(); /** * The method that provides the search service. * * @param req * @return * @throws SenseiException */ public RESULT browse(final REQUEST req) throws SenseiException { // if (_partitions == null){ // ErrorMeter.mark(); // throw new SenseiException("Browse called before cluster is connected!"); // } _searchCounter.mark(); try { return _totalTimer.time(new Callable<RESULT>(){ @Override public RESULT call() throws Exception { return doBrowse(_networkClient, req, _partitions); } }); } catch (Exception e) { _errorMeter.mark(); throw new SenseiException(e.getMessage(), e); } } /** * Merge results on the client/broker side. It likely works differently from * the one in the search node. * * @param request * the original request object * @param resultList * the list of results from all the requested partitions. * @return one single result instance that is merged from the result list. */ public abstract RESULT mergeResults(REQUEST request, List<RESULT> resultList); protected String getRouteParam(REQUEST req) { String param = req.getRouteParam(); if (param == null) { return RandomStringUtils.random(4); } else { return param; } } protected RESULT doBrowse(PartitionedNetworkClient<String> networkClient, final REQUEST req, IntSet partitions) { final long time = System.currentTimeMillis(); final List<RESULT> resultList = new ArrayList<RESULT>(); try { resultList.addAll(_scatterTimer.time(new Callable<List<RESULT>>() { @Override public List<RESULT> call() throws Exception { return doCall(req); } })); } catch (Exception e) { _errorMeter.mark(); RESULT emptyResult = getEmptyResultInstance(); logger.error("Error running scatter/gather", e); emptyResult.addError(new SenseiError("Error gathering the results" + e.getMessage(), ErrorType.BrokerGatherError)); return emptyResult; } if (resultList.size() == 0) { logger.error("no result received at all return empty result"); RESULT emptyResult = getEmptyResultInstance(); emptyResult.addError(new SenseiError("Error gathering the results. " + "no result received at all return empty result", ErrorType.BrokerGatherError)); _emptyMeter.mark(); return emptyResult; } RESULT result = null; try { result = _gatherTimer.time(new Callable<RESULT>() { @Override public RESULT call() throws Exception { return mergeResults(req, resultList); } }); } catch (Exception e) { result = getEmptyResultInstance(); logger.error("Error gathering the results", e); result.addError(new SenseiError("Error gathering the results" + e.getMessage(), ErrorType.BrokerGatherError)); _errorMeter.mark(); } if (logger.isDebugEnabled()){ logger.debug("remote search took " + (System.currentTimeMillis() - time) + "ms"); } return result; } protected List<RESULT> doCall(final REQUEST req) throws ExecutionException { List<RESULT> resultList = new ArrayList<RESULT>(); // only instantiate if debug logging is enabled final List<StringBuilder> timingLogLines = logger.isDebugEnabled() ? new LinkedList<StringBuilder>() : null; ResponseIterator<RESULT> responseIterator = buildIterator(_networkClient.sendRequestToOneReplica(getRouteParam(req), new RequestBuilder<Integer, REQUEST>() { @Override public REQUEST apply(Node node, Set<Integer> nodePartitions) { // TODO: Cloning is yucky per http://www.artima.com/intv/bloch13.html REQUEST clone = (REQUEST) (((SenseiRequest) req).clone()); clone.setPartitions(nodePartitions); if (timingLogLines != null) { // this means debug logging was enabled, produce first portion of log lines timingLogLines.add(buildLogLineForRequest(node, clone)); } REQUEST customizedRequest = customizeRequest(clone); return customizedRequest; } }, _serializer)); while(responseIterator.hasNext()) { resultList.add(responseIterator.next()); } if (timingLogLines != null) { // this means debug logging was enabled, complete the timing log lines and log them int i = 0; for (StringBuilder logLine : timingLogLines) { // we are assuming the request builder gets called in the same order as the response // iterator is built, otherwise the loglines would be out of sync between req & res if (i < resultList.size()) { logger.debug(buildLogLineForResult(logLine, resultList.get(i++))); } } logger.debug(String.format("There are %d responses", resultList.size())); } return resultList; } protected StringBuilder buildLogLineForRequest(Node node, REQUEST clone) { return new StringBuilder() .append("Request to individual node - id:") .append(node.getId()) .append(" - url:") .append(node.getUrl()) .append(" - partitions:") .append(node.getPartitionIds()); } protected StringBuilder buildLogLineForResult(StringBuilder logLine, RESULT result) { return logLine.append(" - took ").append(result.getTime()).append("ms."); } protected ResponseIterator<RESULT> buildIterator(ResponseIterator<RESULT> responseIterator) { TimeoutIterator<RESULT> timeoutIterator = new TimeoutIterator<RESULT>(responseIterator, _timeout); if(allowPartialMerge()) { return new PartialIterator<RESULT>(new ExceptionIterator<RESULT>(timeoutIterator)); } return timeoutIterator; } public void shutdown() { logger.info("shutting down broker..."); if (_scatterTimer != null) { _scatterTimer.stop(); } if (_gatherTimer != null) { _gatherTimer.stop(); } if (_totalTimer != null) { _totalTimer.stop(); } if (_searchCounter != null) { _searchCounter.stop(); } if (_errorMeter != null) { _errorMeter.stop(); } if (_emptyMeter != null) { _emptyMeter.stop(); } } public long getTimeout() { return _timeout; } public Serializer<REQUEST, RESULT> getSerializer() { return _serializer; } /** * @return boolean representing whether or not the server can tolerate node failures or timeouts and merge the other * results. It's a tradeoff between fault tolerance and accuracy that may be acceptable for some applications */ public abstract boolean allowPartialMerge(); }