/*
* Copyright 2014 NAVER Corp.
*
* 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 com.navercorp.pinpoint.web.dao.hbase;
import com.navercorp.pinpoint.common.PinpointConstants;
import com.navercorp.pinpoint.common.buffer.AutomaticBuffer;
import com.navercorp.pinpoint.common.buffer.Buffer;
import com.navercorp.pinpoint.common.hbase.HBaseTables;
import com.navercorp.pinpoint.common.hbase.HbaseOperations2;
import com.navercorp.pinpoint.common.hbase.LimitEventHandler;
import com.navercorp.pinpoint.common.hbase.RowMapper;
import com.navercorp.pinpoint.common.util.BytesUtils;
import com.navercorp.pinpoint.common.util.DateUtils;
import com.navercorp.pinpoint.common.server.util.SpanUtils;
import com.navercorp.pinpoint.common.util.TimeUtils;
import com.navercorp.pinpoint.common.util.TransactionId;
import com.navercorp.pinpoint.web.dao.ApplicationTraceIndexDao;
import com.navercorp.pinpoint.web.mapper.TraceIndexScatterMapper2;
import com.navercorp.pinpoint.web.mapper.TraceIndexScatterMapper3;
import com.navercorp.pinpoint.web.mapper.TransactionIdMapper;
import com.navercorp.pinpoint.web.scatter.ScatterData;
import com.navercorp.pinpoint.web.vo.LimitedScanResult;
import com.navercorp.pinpoint.web.vo.Range;
import com.navercorp.pinpoint.web.vo.ResponseTimeRange;
import com.navercorp.pinpoint.web.vo.SelectedScatterArea;
import com.navercorp.pinpoint.web.vo.scatter.Dot;
import com.sematext.hbase.wd.AbstractRowKeyDistributor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.filter.BinaryPrefixComparator;
import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.filter.FilterList.Operator;
import org.apache.hadoop.hbase.filter.QualifierFilter;
import org.apache.hadoop.hbase.util.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
/**
* @author emeroad
* @author netspider
*/
@Repository
public class HbaseApplicationTraceIndexDao implements ApplicationTraceIndexDao {
private static final int APPLICATION_TRACE_INDEX_NUM_PARTITIONS = 32;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private HbaseOperations2 hbaseOperations2;
@Autowired
@Qualifier("transactionIdMapper")
private RowMapper<List<TransactionId>> traceIndexMapper;
@Autowired
@Qualifier("traceIndexScatterMapper")
private RowMapper<List<Dot>> traceIndexScatterMapper;
@Autowired
@Qualifier("applicationTraceIndexDistributor")
private AbstractRowKeyDistributor traceIdRowKeyDistributor;
private int scanCacheSize = 256;
public void setScanCacheSize(int scanCacheSize) {
this.scanCacheSize = scanCacheSize;
}
@Override
public LimitedScanResult<List<TransactionId>> scanTraceIndex(final String applicationName, Range range, int limit, boolean scanBackward) {
if (applicationName == null) {
throw new NullPointerException("applicationName must not be null");
}
if (range == null) {
throw new NullPointerException("range must not be null");
}
if (limit < 0) {
throw new IllegalArgumentException("negative limit:" + limit);
}
logger.debug("scanTraceIndex");
Scan scan = createScan(applicationName, range, scanBackward);
final LimitedScanResult<List<TransactionId>> limitedScanResult = new LimitedScanResult<>();
LastRowAccessor lastRowAccessor = new LastRowAccessor();
List<List<TransactionId>> traceIndexList = hbaseOperations2.findParallel(HBaseTables.APPLICATION_TRACE_INDEX,
scan, traceIdRowKeyDistributor, limit, traceIndexMapper, lastRowAccessor, APPLICATION_TRACE_INDEX_NUM_PARTITIONS);
List<TransactionId> transactionIdSum = new ArrayList<>(128);
for(List<TransactionId> transactionId: traceIndexList) {
transactionIdSum.addAll(transactionId);
}
limitedScanResult.setScanData(transactionIdSum);
if (transactionIdSum.size() >= limit) {
Long lastRowTimestamp = lastRowAccessor.getLastRowTimestamp();
limitedScanResult.setLimitedTime(lastRowTimestamp);
if (logger.isDebugEnabled()) {
logger.debug("lastRowTimestamp lastTime:{}", DateUtils.longToDateStr(lastRowTimestamp));
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("scanner start lastTime:{}", DateUtils.longToDateStr(range.getFrom()));
}
limitedScanResult.setLimitedTime(range.getFrom());
}
return limitedScanResult;
}
@Override
public LimitedScanResult<List<TransactionId>> scanTraceIndex(final String applicationName, SelectedScatterArea area, int limit) {
if (applicationName == null) {
throw new NullPointerException("applicationName must not be null");
}
if (area == null) {
throw new NullPointerException("area must not be null");
}
if (limit < 0) {
throw new IllegalArgumentException("negative limit:" + limit);
}
logger.debug("scanTraceIndex");
Scan scan = createScan(applicationName, area.getTimeRange());
final LimitedScanResult<List<TransactionId>> limitedScanResult = new LimitedScanResult<>();
LastRowAccessor lastRowAccessor = new LastRowAccessor();
List<List<TransactionId>> traceIndexList = hbaseOperations2.findParallel(HBaseTables.APPLICATION_TRACE_INDEX,
scan, traceIdRowKeyDistributor, limit, traceIndexMapper, lastRowAccessor, APPLICATION_TRACE_INDEX_NUM_PARTITIONS);
List<TransactionId> transactionIdSum = new ArrayList<>(128);
for(List<TransactionId> transactionId: traceIndexList) {
transactionIdSum.addAll(transactionId);
}
limitedScanResult.setScanData(transactionIdSum);
if (transactionIdSum.size() >= limit) {
Long lastRowTimestamp = lastRowAccessor.getLastRowTimestamp();
limitedScanResult.setLimitedTime(lastRowTimestamp);
if (logger.isDebugEnabled()) {
logger.debug("lastRowTimestamp lastTime:{}", DateUtils.longToDateStr(lastRowTimestamp));
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("scanner start lastTime:{}", DateUtils.longToDateStr(area.getTimeRange().getFrom()));
}
limitedScanResult.setLimitedTime(area.getTimeRange().getFrom());
}
return limitedScanResult;
}
private class LastRowAccessor implements LimitEventHandler {
private Long lastRowTimestamp = -1L;
private TransactionId lastTransactionId = null;
private int lastTransactionElapsed = -1;
@Override
public void handleLastResult(Result lastResult) {
if (lastResult == null) {
return;
}
Cell[] rawCells = lastResult.rawCells();
Cell last = rawCells[rawCells.length - 1];
byte[] row = CellUtil.cloneRow(last);
byte[] originalRow = traceIdRowKeyDistributor.getOriginalKey(row);
long reverseStartTime = BytesUtils.bytesToLong(originalRow, PinpointConstants.APPLICATION_NAME_MAX_LEN);
this.lastRowTimestamp = TimeUtils.recoveryTimeMillis(reverseStartTime);
byte[] qualifier = CellUtil.cloneQualifier(last);
this.lastTransactionId = TransactionIdMapper.parseVarTransactionId(qualifier, 0, qualifier.length);
this.lastTransactionElapsed = BytesUtils.bytesToInt(qualifier, 0);
if (logger.isDebugEnabled()) {
logger.debug("lastRowTimestamp={}, lastTransactionId={}, lastTransactionElapsed={}", DateUtils.longToDateStr(lastRowTimestamp), lastTransactionId, lastTransactionElapsed);
}
}
private Long getLastRowTimestamp() {
return lastRowTimestamp;
}
public TransactionId getLastTransactionId() {
return lastTransactionId;
}
public int getLastTransactionElapsed() {
return lastTransactionElapsed;
}
}
private Scan createScan(String applicationName, Range range) {
return createScan(applicationName, range, true);
}
private Scan createScan(String applicationName, Range range, boolean scanBackward) {
Scan scan = new Scan();
scan.setCaching(this.scanCacheSize);
byte[] bApplicationName = Bytes.toBytes(applicationName);
byte[] traceIndexStartKey = SpanUtils.getTraceIndexRowKey(bApplicationName, range.getFrom());
byte[] traceIndexEndKey = SpanUtils.getTraceIndexRowKey(bApplicationName, range.getTo());
if (scanBackward) {
// start key is replaced by end key because key has been reversed
scan.setStartRow(traceIndexEndKey);
scan.setStopRow(traceIndexStartKey);
} else {
scan.setReversed(true);
scan.setStartRow(traceIndexStartKey);
scan.setStopRow(traceIndexEndKey);
}
scan.addFamily(HBaseTables.APPLICATION_TRACE_INDEX_CF_TRACE);
scan.setId("ApplicationTraceIndexScan");
// toString() method of Scan converts a message to json format so it is slow for the first time.
logger.trace("create scan:{}", scan);
return scan;
}
/**
*
*/
@Override
public List<Dot> scanTraceScatter(String applicationName, SelectedScatterArea area, TransactionId offsetTransactionId, int offsetTransactionElapsed, int limit) {
if (applicationName == null) {
throw new NullPointerException("applicationName must not be null");
}
if (area == null) {
throw new NullPointerException("range must not be null");
}
if (limit < 0) {
throw new IllegalArgumentException("negative limit:" + limit);
}
logger.debug("scanTraceScatter");
Scan scan = createScan(applicationName, area.getTimeRange());
// method 1
// not used yet. instead, use another row mapper (testing)
// scan.setFilter(makeResponseTimeFilter(area, offsetTransactionId, offsetTransactionElapsed));
// method 2
ResponseTimeRange responseTimeRange = area.getResponseTimeRange();
TraceIndexScatterMapper2 mapper = new TraceIndexScatterMapper2(responseTimeRange.getFrom(), responseTimeRange.getTo());
List<List<Dot>> dotListList = hbaseOperations2.findParallel(HBaseTables.APPLICATION_TRACE_INDEX, scan, traceIdRowKeyDistributor, limit, mapper, APPLICATION_TRACE_INDEX_NUM_PARTITIONS);
List<Dot> result = new ArrayList<>();
for(List<Dot> dotList : dotListList) {
result.addAll(dotList);
}
return result;
}
@Override
public ScatterData scanTraceScatterData(String applicationName, Range range, int xGroupUnit, int yGroupUnit, int limit, boolean scanBackward) {
if (applicationName == null) {
throw new NullPointerException("applicationName must not be null");
}
if (range == null) {
throw new NullPointerException("range must not be null");
}
if (limit < 0) {
throw new IllegalArgumentException("negative limit:" + limit);
}
logger.debug("scanTraceScatterDataMadeOfDotGroup");
Scan scan = createScan(applicationName, range, scanBackward);
TraceIndexScatterMapper3 mapper = new TraceIndexScatterMapper3(range.getFrom(), range.getTo(), xGroupUnit, yGroupUnit);
List<ScatterData> dotGroupList = hbaseOperations2.findParallel(HBaseTables.APPLICATION_TRACE_INDEX, scan, traceIdRowKeyDistributor, limit, mapper, APPLICATION_TRACE_INDEX_NUM_PARTITIONS);
if (CollectionUtils.isEmpty(dotGroupList)) {
return new ScatterData(range.getFrom(), range.getTo(), xGroupUnit, yGroupUnit);
} else {
ScatterData firstScatterData = dotGroupList.get(0);
for (int i = 1; i < dotGroupList.size(); i++) {
firstScatterData.merge(dotGroupList.get(i));
}
return firstScatterData;
}
}
/**
* make the hbase filter for selecting values of y-axis(response time) in order to select transactions in scatter chart.
* 4 bytes for elapsed time should be attached for the prefix of column qualifier for to use this filter.
*
* @param area
* @param offsetTransactionId
* @param offsetTransactionElapsed
* @return
*/
private Filter makeResponseTimeFilter(final SelectedScatterArea area, final TransactionId offsetTransactionId, int offsetTransactionElapsed) {
// filter by response time
ResponseTimeRange responseTimeRange = area.getResponseTimeRange();
byte[] responseFrom = Bytes.toBytes(responseTimeRange.getFrom());
byte[] responseTo = Bytes.toBytes(responseTimeRange.getTo());
FilterList filterList = new FilterList(Operator.MUST_PASS_ALL);
filterList.addFilter(new QualifierFilter(CompareOp.GREATER_OR_EQUAL, new BinaryPrefixComparator(responseFrom)));
filterList.addFilter(new QualifierFilter(CompareOp.LESS_OR_EQUAL, new BinaryPrefixComparator(responseTo)));
// add offset
if (offsetTransactionId != null) {
final Buffer buffer = new AutomaticBuffer(32);
buffer.putInt(offsetTransactionElapsed);
buffer.putPrefixedString(offsetTransactionId.getAgentId());
buffer.putSVLong(offsetTransactionId.getAgentStartTime());
buffer.putVLong(offsetTransactionId.getTransactionSequence());
byte[] qualifierOffset = buffer.getBuffer();
filterList.addFilter(new QualifierFilter(CompareOp.GREATER, new BinaryPrefixComparator(qualifierOffset)));
}
return filterList;
}
}