// This file is part of OpenTSDB.
// Copyright (C) 2013 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 <http://www.gnu.org/licenses/>.
package net.opentsdb.tools;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.opentsdb.core.TSDB;
import net.opentsdb.meta.TSMeta;
import net.opentsdb.tree.Tree;
import net.opentsdb.tree.TreeBuilder;
import net.opentsdb.uid.NoSuchUniqueId;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.utils.JSON;
import org.hbase.async.Bytes;
import org.hbase.async.HBaseException;
import org.hbase.async.KeyValue;
import org.hbase.async.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
/**
* Helper tool class used to generate or synchronize a tree using TSMeta objects
* stored in the UID table. Also can be used to delete a tree. This class should
* be used only by the CLI tools.
*/
final class TreeSync extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(TreeSync.class);
/** Charset used to convert Strings to byte arrays and back. */
private static final Charset CHARSET;
static {
final Class<UniqueId> uidclass = UniqueId.class;
try {
// Those are all implementation details so they're not part of the
// interface. We access them anyway using reflection. I think this
// is better than marking those public and adding a javadoc comment
// "THIS IS INTERNAL DO NOT USE". If only Java had C++'s "friend" or
// a less stupid notion of a package.
Field f;
f = uidclass.getDeclaredField("CHARSET");
f.setAccessible(true);
CHARSET = (Charset) f.get(null);
} catch (Exception e) {
throw new RuntimeException("static initializer failed", e);
}
}
/** TSDB to use for storage access */
final TSDB tsdb;
/** The ID to start the sync with for this thread */
final long start_id;
/** The end of the ID block to work on */
final long end_id;
/** Diagnostic ID for this thread */
final int thread_id;
/**
* Default constructor, stores the TSDB to use
* @param tsdb The TSDB to use for access
* @param start_id The starting ID of the block we'll work on
* @param quotient The total number of IDs in our block
* @param thread_id The ID of this thread (starts at 0)
*/
public TreeSync(final TSDB tsdb, final long start_id, final double quotient,
final int thread_id) {
this.tsdb = tsdb;
this.start_id = start_id;
this.end_id = start_id + (long) quotient + 1; // teensy bit of overlap
this.thread_id = thread_id;
}
/**
* Performs a tree synchronization using a table scanner across the UID table
* @return 0 if completed successfully, something else if an error occurred
*/
public void run() {
final Scanner scanner = getScanner();
// start the process by loading all of the trees in the system
final List<Tree> trees;
try {
trees = Tree.fetchAllTrees(tsdb).joinUninterruptibly();
LOG.info("[" + thread_id + "] Complete");
} catch (Exception e) {
LOG.error("[" + thread_id + "] Unexpected Exception", e);
throw new RuntimeException("[" + thread_id + "] Unexpected exception", e);
}
if (trees == null) {
LOG.warn("No tree definitions were found");
return;
} else {
boolean has_enabled_tree = false;
for (Tree tree : trees) {
if (tree.getEnabled()) {
has_enabled_tree = true;
break;
}
}
if (!has_enabled_tree) {
LOG.warn("No enabled trees were found");
return;
}
LOG.info("Found [" + trees.size() + "] trees");
}
// setup an array for storing the tree processing calls so we can block
// until each call has completed
final ArrayList<Deferred<Boolean>> tree_calls =
new ArrayList<Deferred<Boolean>>();
final Deferred<Boolean> completed = new Deferred<Boolean>();
/**
* Scanner callback that loops through the UID table recursively until
* the scanner returns a null row set.
*/
final class TsuidScanner implements Callback<Deferred<Boolean>,
ArrayList<ArrayList<KeyValue>>> {
/**
* Fetches the next set of rows from the scanner, adding this class as a
* callback
* @return A meaningless deferred used to wait on until processing has
* completed
*/
public Deferred<Boolean> scan() {
return scanner.nextRows().addCallbackDeferring(this);
}
@Override
public Deferred<Boolean> call(ArrayList<ArrayList<KeyValue>> rows)
throws Exception {
if (rows == null) {
completed.callback(true);
return null;
}
for (final ArrayList<KeyValue> row : rows) {
// convert to a string one time
final String tsuid = UniqueId.uidToString(row.get(0).key());
/**
* A throttling callback used to wait for the current TSMeta to
* complete processing through the trees before continuing on with
* the next set.
*/
final class TreeBuilderBufferCB implements Callback<Boolean,
ArrayList<ArrayList<Boolean>>> {
@Override
public Boolean call(ArrayList<ArrayList<Boolean>> builder_calls)
throws Exception {
//LOG.debug("Processed [" + builder_calls.size() + "] tree_calls");
return true;
}
}
/**
* Executed after parsing a TSMeta object and loading all of the
* associated UIDMetas. Once the meta has been loaded, this callback
* runs it through each of the configured TreeBuilder objects and
* stores the resulting deferred in an array. Once processing of all
* of the rules has completed, we group the deferreds and call
* BufferCB() to wait for their completion.
*/
final class ParseCB implements Callback<Deferred<Boolean>, TSMeta> {
final ArrayList<Deferred<ArrayList<Boolean>>> builder_calls =
new ArrayList<Deferred<ArrayList<Boolean>>>();
@Override
public Deferred<Boolean> call(TSMeta meta) throws Exception {
if (meta != null) {
LOG.debug("Processing TSMeta: " + meta + " w value: " +
JSON.serializeToString(meta));
// copy the trees into a tree builder object and iterate through
// each builder. We need to do this as a builder is not thread
// safe and cannot be used asynchronously.
final ArrayList<TreeBuilder> tree_builders =
new ArrayList<TreeBuilder>(trees.size());
for (Tree tree : trees) {
if (!tree.getEnabled()) {
continue;
}
final TreeBuilder builder = new TreeBuilder(tsdb, tree);
tree_builders.add(builder);
}
for (TreeBuilder builder : tree_builders) {
builder_calls.add(builder.processTimeseriesMeta(meta));
}
return Deferred.group(builder_calls)
.addCallback(new TreeBuilderBufferCB());
} else {
return Deferred.fromResult(false);
}
}
}
/**
* An error handler used to catch issues when loading the TSMeta such
* as a missing UID name. In these situations we want to log that the
* TSMeta had an issue and continue on.
*/
final class ErrBack implements Callback<Deferred<Boolean>, Exception> {
@Override
public Deferred<Boolean> call(Exception e) throws Exception {
if (e.getClass().equals(IllegalStateException.class)) {
LOG.error("Invalid data when processing TSUID [" + tsuid + "]", e);
} else if (e.getClass().equals(IllegalArgumentException.class)) {
LOG.error("Invalid data when processing TSUID [" + tsuid + "]", e);
} else if (e.getClass().equals(NoSuchUniqueId.class)) {
LOG.warn("Timeseries [" + tsuid +
"] includes a non-existant UID: " + e.getMessage());
} else {
LOG.error("[" + thread_id + "] Exception while processing TSUID [" +
tsuid + "]", e);
}
return Deferred.fromResult(false);
}
}
// matched a TSMeta column, so request a parsing and loading of
// associated UIDMeta objects, then pass it off to callbacks for
// parsing through the trees.
final Deferred<Boolean> process_tsmeta =
TSMeta.parseFromColumn(tsdb, row.get(0), true)
.addCallbackDeferring(new ParseCB());
process_tsmeta.addErrback(new ErrBack());
tree_calls.add(process_tsmeta);
}
/**
* Another buffer callback that waits for the current set of TSMetas to
* complete their tree calls before we fetch another set of rows from
* the scanner. This necessary to avoid OOM issues.
*/
final class ContinueCB implements Callback<Deferred<Boolean>,
ArrayList<Boolean>> {
@Override
public Deferred<Boolean> call(ArrayList<Boolean> tsuids)
throws Exception {
LOG.debug("Processed [" + tsuids.size() + "] tree_calls, continuing");
tree_calls.clear();
return scan();
}
}
// request the next set of rows from the scanner, but wait until the
// current set of TSMetas has been processed so we don't slaughter our
// host
Deferred.group(tree_calls).addCallback(new ContinueCB());
return Deferred.fromResult(null);
}
}
/**
* Used to capture unhandled exceptions from the scanner callbacks and
* exit the thread properly
*/
final class ErrBack implements Callback<Deferred<Boolean>, Exception> {
@Override
public Deferred<Boolean> call(Exception e) throws Exception {
LOG.error("Unexpected exception", e);
completed.callback(false);
return Deferred.fromResult(false);
}
}
final TsuidScanner tree_scanner = new TsuidScanner();
tree_scanner.scan().addErrback(new ErrBack());
try {
completed.joinUninterruptibly();
LOG.info("[" + thread_id + "] Complete");
} catch (Exception e) {
LOG.error("[" + thread_id + "] Scanner Exception", e);
throw new RuntimeException("[" + thread_id + "] Scanner exception", e);
}
return;
}
/**
* Attempts to delete all data generated by the given tree, and optionally,
* the tree definition itself.
* @param tree_id The tree with data to delete
* @param delete_definition Whether or not the tree definition itself should
* be removed from the system
* @return 0 if completed successfully, something else if an error occurred
*/
public int purgeTree(final int tree_id, final boolean delete_definition)
throws Exception {
if (delete_definition) {
LOG.info("Deleting tree branches and definition for: " + tree_id);
} else {
LOG.info("Deleting tree branches for: " + tree_id);
}
Tree.deleteTree(tsdb, tree_id, delete_definition).joinUninterruptibly();
LOG.info("Completed tree deletion for: " + tree_id);
return 0;
}
/**
* Returns a scanner set to scan the range configured for this thread
* @return A scanner on the "name" CF configured for the specified range
* @throws HBaseException if something goes boom
*/
private Scanner getScanner() throws HBaseException {
final short metric_width = TSDB.metrics_width();
final byte[] start_row =
Arrays.copyOfRange(Bytes.fromLong(start_id), 8 - metric_width, 8);
final byte[] end_row =
Arrays.copyOfRange(Bytes.fromLong(end_id), 8 - metric_width, 8);
LOG.debug("[" + thread_id + "] Start row: " + UniqueId.uidToString(start_row));
LOG.debug("[" + thread_id + "] End row: " + UniqueId.uidToString(end_row));
final Scanner scanner = tsdb.getClient().newScanner(tsdb.metaTable());
scanner.setStartKey(start_row);
scanner.setStopKey(end_row);
scanner.setFamily("name".getBytes(CHARSET));
scanner.setQualifier("ts_meta".getBytes(CHARSET));
return scanner;
}
}