net.opentsdb.core.SaltScanner.orig Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opentsdb Show documentation
Show all versions of opentsdb Show documentation
OpenTSDB is a distributed, scalable Time Series Database (TSDB)
written on top of HBase. OpenTSDB was written to address a common need:
store, index and serve metrics collected from computer systems (network
gear, operating systems, applications) at a large scale, and make this
data easily accessible and graphable.
// This file is part of OpenTSDB.
// Copyright (C) 2015 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see .
package net.opentsdb.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import net.opentsdb.meta.Annotation;
import net.opentsdb.query.filter.TagVFilter;
import net.opentsdb.rollup.RollupQuery;
import net.opentsdb.rollup.RollupSpan;
import net.opentsdb.stats.QueryStats;
import net.opentsdb.stats.QueryStats.QueryStat;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.utils.DateTime;
import net.opentsdb.utils.JSON;
import org.hbase.async.Bytes.ByteMap;
import org.hbase.async.Bytes;
import org.hbase.async.DeleteRequest;
import org.hbase.async.KeyValue;
import org.hbase.async.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
/**
* A class that handles coordinating the various scanners created for each
* salt bucket when salting is enabled. Each scanner stores it's results in
* local maps and once everyone has reported in, then the maps are parsed and
* combined into a proper set of spans to return to the {@link TsdbQuery} class.
*
* Note that if one or more of the scanners throws an exception, then that
* exception will be returned to the caller in the deferred. Unfortunately we
* don't have a good way to cancel a scan in progress so the first scanner with
* an error will store it, then we wait for all of the other scanners to
* complete.
*
* Concurrency is important in this class as the scanners are executing
* asynchronously and can modify variables at any time.
*/
public class SaltScanner {
private static final Logger LOG = LoggerFactory.getLogger(SaltScanner.class);
/** This is a map that the caller must supply. We'll fill it with data.
* WARNING: The salted row comparator should be applied to this map. */
private final TreeMap spans;
/** The list of pre-configured scanners. One scanner should be created per
* salt bucket. */
private final List scanners;
/** Stores the compacted columns from each scanner as it completes. After all
* scanners are done, we process this into the span map above. */
private final Map> kv_map =
new ConcurrentHashMap>();
/** Stores annotations from each scanner as it completes */
private final Map> annotation_map =
Collections.synchronizedMap(
new TreeMap>(new RowKey.SaltCmp()));
/** A deferred to call with the spans on completion */
private final Deferred> results =
new Deferred>();
/** The metric this scanner set is dealing with. If a row comes in with a
* different metric we toss an exception. This shouldn't happen though. */
private final byte[] metric;
/** The TSDB to which we belong */
private final TSDB tsdb;
/** A stats object associated with the sub query used for storing stats
* about scanner operations. */
private final QueryStats query_stats;
/** Index of the sub query in the main query list */
private final int query_index;
/** A counter used to determine how many scanners are still running */
private AtomicInteger completed_tasks = new AtomicInteger();
/** When the scanning started. We store the scan latency once all scanners
* are done.*/
private long start_time; // milliseconds.
/** Whether or not to delete the queried data */
private final boolean delete;
/** A rollup query configuration if scanning for rolled up data. */
private final RollupQuery rollup_query;
/** A list of filters to iterate over when processing rows */
private final List filters;
/** A holder for storing the first exception thrown by a scanner if something
* goes pear shaped. Make sure to synchronize on this object when checking
* for null or assigning from a scanner's callback. */
private volatile Exception exception;
/**
* Default ctor that performs some validation. Call {@link scan} after
* construction to actually start fetching data.
* @param tsdb The TSDB to which we belong
* @param metric The metric we're expecting to fetch
* @param scanners A list of HBase scanners, one for each bucket
* @param spans The span map to store results in
* @param filters A list of filters for processing
* @throws IllegalArgumentException if any required data was missing or
* we had invalid parameters.
*/
public SaltScanner(final TSDB tsdb, final byte[] metric,
final List scanners,
final TreeMap spans,
final List filters) {
this(tsdb, metric, scanners, spans, filters, false, null, null, 0);
}
/**
* Default ctor that performs some validation. Call {@link scan} after
* construction to actually start fetching data.
* @param tsdb The TSDB to which we belong
* @param metric The metric we're expecting to fetch
* @param scanners A list of HBase scanners, one for each bucket
* @param spans The span map to store results in
* @param delete Whether or not to delete the queried data
* @param rollup_query An optional rollup query config. May be null.
* @param filters A list of filters for processing
* @param query_stats A stats object for tracking timing
* @param query_index The index of the sub query in the main query list
* @throws IllegalArgumentException if any required data was missing or
* we had invalid parameters.
*/
public SaltScanner(final TSDB tsdb, final byte[] metric,
final List scanners,
final TreeMap spans,
final List filters,
final boolean delete,
final RollupQuery rollup_query,
final QueryStats query_stats,
final int query_index) {
if (Const.SALT_WIDTH() < 1) {
throw new IllegalArgumentException(
"Salting is disabled. Use the regular scanner");
}
if (tsdb == null) {
throw new IllegalArgumentException("The TSDB argument was null.");
}
if (spans == null) {
throw new IllegalArgumentException("Span map cannot be null.");
}
if (!spans.isEmpty()) {
throw new IllegalArgumentException("The span map should be empty.");
}
if (scanners == null || scanners.isEmpty()) {
throw new IllegalArgumentException("Missing or empty scanners list. "
+ "Please provide a list of scanners for each salt.");
}
if (scanners.size() != Const.SALT_BUCKETS()) {
throw new IllegalArgumentException("Not enough or too many scanners " +
scanners.size() + " when the salt bucket count is " +
Const.SALT_BUCKETS());
}
if (metric == null) {
throw new IllegalArgumentException("The metric array was null.");
}
if (metric.length != TSDB.metrics_width()) {
throw new IllegalArgumentException("The metric was too short. It must be "
+ TSDB.metrics_width() + "bytes wide.");
}
this.scanners = scanners;
this.spans = spans;
this.metric = metric;
this.tsdb = tsdb;
this.filters = filters;
this.delete = delete;
this.rollup_query = rollup_query;
this.query_stats = query_stats;
this.query_index = query_index;
}
/**
* Starts all of the scanners asynchronously and returns the data fetched
* once all of the scanners have completed. Note that the result may be an
* exception if one or more of the scanners encountered an exception. The
* first error will be returned, others will be logged.
* @return A deferred to wait on for results.
*/
public Deferred> scan() {
start_time = System.currentTimeMillis();
int i = 0;
for (final Scanner scanner: scanners) {
new ScannerCB(scanner, i++).scan();
}
return results;
}
/**
* Called once all of the scanners have reported back in to record our
* latency and merge the results into the spans map. If there was an exception
* stored then we'll return that instead.
*/
private void mergeAndReturnResults() {
final long hbase_time = System.currentTimeMillis();
TsdbQuery.scanlatency.add((int)(hbase_time - start_time));
long rows = 0;
if (exception != null) {
LOG.error("After all of the scanners finished, at "
+ "least one threw an exception", exception);
results.callback(exception);
return;
}
// Merge sorted spans together
final long merge_start = DateTime.nanoTime();
for (final List kvs : kv_map.values()) {
if (kvs == null || kvs.isEmpty()) {
LOG.warn("Found a key value list that was null or empty");
continue;
}
for (final KeyValue kv : kvs) {
if (kv == null) {
LOG.warn("Found a key value item that was null");
continue;
}
if (kv.key() == null) {
LOG.warn("A key for a kv was null");
continue;
}
Span datapoints = spans.get(kv.key());
if (datapoints == null) {
datapoints = RollupQuery.isValidQuery(rollup_query) ?
new RollupSpan(tsdb, this.rollup_query) : new Span(tsdb);
spans.put(kv.key(), datapoints);
}
if (annotation_map.containsKey(kv.key())) {
for (final Annotation note: annotation_map.get(kv.key())) {
datapoints.getAnnotations().add(note);
}
annotation_map.remove(kv.key());
}
try {
datapoints.addRow(kv);
rows++;
} catch (RuntimeException e) {
LOG.error("Exception adding row to span", e);
throw e;
}
}
}
kv_map.clear();
for (final byte[] key : annotation_map.keySet()) {
Span datapoints = spans.get(key);
if (datapoints == null) {
datapoints = new Span(tsdb);
spans.put(key, datapoints);
}
for (final Annotation note: annotation_map.get(key)) {
datapoints.getAnnotations().add(note);
}
}
if (query_stats != null) {
query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME,
(DateTime.nanoTime() - merge_start));
}
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning completed in " + (hbase_time - start_time) + " ms, " +
rows + " rows, and stored in " + spans.size() + " spans");
LOG.debug("It took " + (System.currentTimeMillis() - hbase_time) + " ms, "
+ " to merge and sort the rows into a tree map");
}
results.callback(spans);
}
/**
* Scanner callback executed recursively each time we get a set of data
* from storage. This is responsible for determining what columns are
* returned and issuing requests to load leaf objects.
* When the scanner returns a null set of rows, the method initiates the
* final callback.
*/
final class ScannerCB implements Callback