diff --git a/tools/docker/Dockerfile b/Dockerfile similarity index 88% rename from tools/docker/Dockerfile rename to Dockerfile index c9410133e1..35dd48583c 100644 --- a/tools/docker/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM java:openjdk-8-alpine MAINTAINER jonathan.creasy@gmail.com -ENV VERSION 2.3.0-RC1 +ENV VERSION 2.5.0-SNAPSHOT ENV WORKDIR /usr/share/opentsdb ENV LOGDIR /var/log/opentsdb ENV DATADIR /data/opentsdb @@ -32,10 +32,10 @@ ENV TSDB_PORT 4244 WORKDIR $WORKDIR -ADD libs $WORKDIR/libs -ADD logback.xml $WORKDIR +ADD third_party/*/*.jar $WORKDIR/libs/ +ADD src/logback.xml $WORKDIR ADD tsdb-$VERSION.jar $WORKDIR -ADD opentsdb.conf $ETCDIR/opentsdb.conf +ADD src/opentsdb.conf $ETCDIR/opentsdb.conf VOLUME ["/etc/openstsdb"] VOLUME ["/data/opentsdb"] diff --git a/Makefile.am b/Makefile.am index d3ce9287e7..0885b4265c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -32,6 +32,8 @@ dist_noinst_DATA = pom.xml.in build-aux/rpm/opentsdb.conf \ build-aux/rpm/logback.xml build-aux/rpm/init.d/opentsdb \ build-aux/rpm/systemd/opentsdb@.service tsdb_SRC := \ + src/core/AbstractSpanGroup.java \ + src/core/AbstractQuery.java \ src/core/AggregationIterator.java \ src/core/Aggregator.java \ src/core/Aggregators.java \ @@ -48,6 +50,7 @@ tsdb_SRC := \ src/core/DownsamplingSpecification.java \ src/core/FillingDownsampler.java \ src/core/FillPolicy.java \ + src/core/GroupCallback.java \ src/core/Histogram.java \ src/core/HistogramAggregation.java \ src/core/HistogramAggregationIterator.java \ @@ -78,14 +81,18 @@ tsdb_SRC := \ src/core/RequestBuilder.java \ src/core/RowKey.java \ src/core/RowSeq.java \ + src/core/RpcResponder.java \ src/core/iRowSeq.java \ src/core/SaltScanner.java \ src/core/SeekableView.java \ + src/core/SeekableViewChain.java \ src/core/SimpleHistogram.java \ src/core/SimpleHistogramDataPointAdapter.java \ src/core/SimpleHistogramDecoder.java \ src/core/Span.java \ src/core/SpanGroup.java \ + src/core/SplitRollupQuery.java \ + src/core/SplitRollupSpanGroup.java \ src/core/TSDB.java \ src/core/Tags.java \ src/core/TsdbQuery.java \ @@ -120,6 +127,7 @@ tsdb_SRC := \ src/query/expression/ExpressionReader.java \ src/query/expression/Expressions.java \ src/query/expression/ExpressionTree.java \ + src/query/expression/FirstDifference.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ src/query/expression/IntersectionIterator.java \ @@ -316,12 +324,17 @@ test_SRC := \ test/core/TestRateSpan.java \ test/core/TestRowKey.java \ test/core/TestRowSeq.java \ + test/core/TestRpcResponsder.java \ test/core/TestSaltScanner.java \ + test/core/TestSeekableViewChain.java \ test/core/TestSpan.java \ test/core/TestSpanGroup.java \ + test/core/TestSplitRollupQuery.java \ + test/core/TestSplitRollupSpanGroup.java \ test/core/TestTags.java \ test/core/TestTSDB.java \ test/core/TestTSDBAddPoint.java \ + test/core/TestTSDBTableAvailability.java \ test/core/TestTsdbQueryDownsample.java \ test/core/TestTsdbQueryDownsampleSalted.java \ test/core/TestTsdbQuery.java \ @@ -376,6 +389,7 @@ test_SRC := \ test/query/pojo/TestTimeSpan.java \ test/rollup/TestRollupConfig.java \ test/rollup/TestRollupInterval.java \ + test/rollup/TestRollupQuery.java \ test/rollup/TestRollupSeq.java \ test/rollup/TestRollupUtils.java \ test/search/TestSearchPlugin.java \ @@ -414,6 +428,7 @@ test_SRC := \ test/tsd/TestRTPublisher.java \ test/tsd/TestSearchRpc.java \ test/tsd/TestStatsRpc.java \ + test/tsd/TestStatusRpc.java \ test/tsd/TestSuggestRpc.java \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ diff --git a/build-aux/deb/logback.xml b/build-aux/deb/logback.xml index 9c32b2ecbe..e3e04945a4 100644 --- a/build-aux/deb/logback.xml +++ b/build-aux/deb/logback.xml @@ -64,6 +64,9 @@ + + + diff --git a/build-aux/rpm/logback.xml b/build-aux/rpm/logback.xml index 4fae3c5655..c1bb905908 100644 --- a/build-aux/rpm/logback.xml +++ b/build-aux/rpm/logback.xml @@ -64,6 +64,9 @@ + + + diff --git a/configure.ac b/configure.ac index 659a5cd976..fe726e760a 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see . # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.0RC2], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.5.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) diff --git a/screwdriver.yaml b/screwdriver.yaml index 11a4352285..1357d02594 100644 --- a/screwdriver.yaml +++ b/screwdriver.yaml @@ -1,7 +1,11 @@ shared: - image: maven + image: maven:3-adoptopenjdk-8 jobs: + pr: + steps: + - run_arbitrary_script: apt-get update && apt-get install autoconf make python -y && ./build.sh pom.xml && mvn clean test --quiet main: + requires: [~pr, ~commit] steps: - - run_arbitrary_script: apt-get update && apt-get install autoconf make -y && ./build.sh pom.xml && mvn clean test --quiet + - run_arbitrary_script: apt-get update && apt-get install autoconf make python -y && ./build.sh pom.xml && mvn clean test --quiet diff --git a/src/core/AbstractQuery.java b/src/core/AbstractQuery.java new file mode 100644 index 0000000000..5a37c50f4a --- /dev/null +++ b/src/core/AbstractQuery.java @@ -0,0 +1,66 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2012 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 org.hbase.async.HBaseException; + +public abstract class AbstractQuery implements Query { + /** + * Runs this query. + * + * @return The data points matched by this query. + *

+ * Each element in the non-{@code null} but possibly empty array returned + * corresponds to one time series for which some data points have been + * matched by the query. + * @throws HBaseException if there was a problem communicating with HBase to + * perform the search. + */ + @Override + public DataPoints[] run() throws HBaseException { + try { + return runAsync().joinUninterruptibly(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + /** + * Runs this query. + * + * @return The data points matched by this query and applied with percentile calculation + *

+ * Each element in the non-{@code null} but possibly empty array returned + * corresponds to one time series for which some data points have been + * matched by the query. + * @throws HBaseException if there was a problem communicating with HBase to + * perform the search. + * @throws IllegalStateException if the query is not a histogram query + */ + @Override + public DataPoints[] runHistogram() throws HBaseException { + if (!isHistogramQuery()) { + throw new RuntimeException("Should never be here"); + } + + try { + return runHistogramAsync().joinUninterruptibly(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } +} diff --git a/src/core/AbstractSpanGroup.java b/src/core/AbstractSpanGroup.java new file mode 100644 index 0000000000..f9605a6b05 --- /dev/null +++ b/src/core/AbstractSpanGroup.java @@ -0,0 +1,70 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2012 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.*; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupQuery; + +/** + * Groups multiple spans together and offers a dynamic "view" on them. + *

+ * This is used for queries to the TSDB, where we might group multiple + * {@link Span}s that are for the same time series but different tags + * together. We need to "hide" data points that are outside of the + * time period of the query and do on-the-fly aggregation of the data + * points coming from the different Spans, using an {@link Aggregator}. + * Since not all the Spans will have their data points at exactly the + * same time, we also do on-the-fly linear interpolation. If needed, + * this view can also return the rate of change instead of the actual + * data points. + *

+ * This is one of the rare (if not the only) implementations of + * {@link DataPoints} for which {@link #getTags} can potentially return + * an empty map. + *

+ * The implementation can also dynamically downsample the data when a + * sampling interval a downsampling function (in the form of an + * {@link Aggregator}) are given. This is done by using a special + * iterator when using the {@link Span.DownsamplingIterator}. + */ +abstract class AbstractSpanGroup implements DataPoints { + /** + * Finds the {@code i}th data point of this group in {@code O(n)}. + * Where {@code n} is the number of data points in this group. + */ + protected DataPoint getDataPoint(int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("negative index: " + i); + } + final int saved_i = i; + final SeekableView it = iterator(); + DataPoint dp = null; + while (it.hasNext() && i >= 0) { + dp = it.next(); + i--; + } + if (i != -1 || dp == null) { + throw new IndexOutOfBoundsException("index " + saved_i + + " too large (it's >= " + size() + ") for " + this); + } + return dp; + } +} diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index a5d0f541fa..7e2210c721 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -783,7 +783,7 @@ public double nextDoubleValue() { r = Double.MAX_VALUE; break; case MIN: - r = Double.MIN_VALUE; + r = -Double.MAX_VALUE; break; case PREV: r = y0; diff --git a/src/core/ByteBufferList.java b/src/core/ByteBufferList.java index 443750e082..9f744bfefc 100644 --- a/src/core/ByteBufferList.java +++ b/src/core/ByteBufferList.java @@ -58,6 +58,20 @@ public void add(final byte[] buf, final int offset, final int len) { total_length += len; } + /** + * Removes the last added segment from the segments array and returns it to the caller + * + * @return byte array representing the most recently added segment or null if no segments exist + */ + public BufferSegment removeLastSegment() { + if (segments.isEmpty()) { + return null; + } + BufferSegment seg = segments.remove(segments.size() - 1); + total_length -= seg.len; + return seg; + } + /** * Get the most recently added segment. * @@ -73,7 +87,7 @@ public byte[] getLastSegment() { /** * Get the number of segments that have added to this buffer list. - * + * * @return the segment count */ public int segmentCount() { @@ -82,7 +96,7 @@ public int segmentCount() { /** * Get the accumulated bytes as a single byte array (may be a zero-byte array if empty). - * + * * @param padding the number of additional bytes to include at the end * @return the accumulated bytes */ diff --git a/src/core/ColumnDatapointIterator.java b/src/core/ColumnDatapointIterator.java index 8df8a20626..0d1858678b 100644 --- a/src/core/ColumnDatapointIterator.java +++ b/src/core/ColumnDatapointIterator.java @@ -120,6 +120,19 @@ public void writeToBuffers(ByteBufferList compQualifier, ByteBufferList compValu compValue.add(value, value_offset, current_val_length); } + /** + * Write a new qualifier and its associated value to the compacted qualifier buffer and compacted values buffer respectively. + * + * @param newQualifier - the new qualifier + * @param newVal - the new value + * @param compactedQual - the qualifiers buffer + * @param compactedVal - the values buffer + */ + public void writeToBuffers(byte[] newQualifier, byte[] newVal, ByteBufferList compactedQual, ByteBufferList compactedVal) { + compactedQual.add(newQualifier, 0, newQualifier.length); + compactedVal.add(newVal, 0, newVal.length); + } + public void writeToBuffersFromOffset(ByteBufferList compQualifier, ByteBufferList compValue, Pair offsets, Pair offsetLengths) { compQualifier.add(qualifier, offsets.getKey(), offsetLengths.getKey()); compValue.add(value, offsets.getValue(), offsetLengths.getValue()); @@ -186,6 +199,20 @@ private boolean update() { return true; } + /** + * @return the flags mask from the qualifier + */ + public short getFlagsFromQualifier() { + return Internal.getFlagsFromQualifier(qualifier, qualifier_offset); + } + + /** + * @return the column timestamp + */ + public long getColumnTimestamp() { + return column_timestamp; + } + // order in ascending order by timestamp, descending order by row timestamp (so we find the // entry we are going to keep first, and don't have to copy over it) @Override diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 857d60fbd2..859197bbdd 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -558,7 +558,7 @@ private void defaultMergeDataPoints(ByteBufferList compacted_qual, final byte[] discardedVal = col.getCopyOfCurrentValue(); if (!Arrays.equals(existingVal, discardedVal)) { duplicates_different.incrementAndGet(); - if (!tsdb.config.fix_duplicates()) { + if (!tsdb.config.fix_duplicates() && !tsdb.config.sum_duplicates()) { throw new IllegalDataException("Duplicate timestamp for key=" + Arrays.toString(row.get(0).key()) + ", ms_offset=" + ts + ", older=" + Arrays.toString(existingVal) + ", newer=" + Arrays.toString(discardedVal) @@ -570,6 +570,53 @@ private void defaultMergeDataPoints(ByteBufferList compacted_qual, } else { duplicates_same.incrementAndGet(); } + + if (tsdb.config.sum_duplicates()) { + short current_flags = Internal.getFlagsFromQualifier(compacted_qual.getLastSegment()); + short discarded_flags = col.getFlagsFromQualifier(); + + boolean is_current_long = ((current_flags & Const.FLAG_FLOAT) == 0x0); + boolean is_discarded_long = ((discarded_flags & Const.FLAG_FLOAT) == 0x0); + + /* the current value flags determine the type of the output (long or double) */ + + double current_val = 0.0; + double discarded_val = 0.0; + + if (is_current_long) { + current_val = Internal.extractIntegerValue(existingVal, 0, (byte)current_flags); + } else { + current_val = Internal.extractFloatingPointValue(existingVal, 0, (byte) current_flags); + } + + if (is_discarded_long) { + discarded_val = Internal.extractIntegerValue(discardedVal, 0, (byte)discarded_flags); + } else { + discarded_val = Internal.extractFloatingPointValue(discardedVal, 0, (byte) current_flags); + } + + current_val += discarded_val; + byte[] new_val = null; + current_flags = 0; + + if (is_current_long) { + new_val = Bytes.fromLong((long) current_val); + current_flags = (short) (new_val.length - 1); + } else { + new_val = Bytes.fromLong(Double.doubleToRawLongBits(current_val)); + current_flags = Const.FLAG_FLOAT | 0x7; + } + + final byte[] new_qualifier = Internal.buildQualifier(col.getColumnTimestamp(), current_flags); + + /* now remove the last value & qualifier (existing one) and write the updated value and qualifier */ + + prevTs = ts; + compacted_val.removeLastSegment(); + compacted_qual.removeLastSegment(); + col.writeToBuffers(new_qualifier, new_val, compacted_qual, compacted_val); + ms_in_row |= col.isMilliseconds(); + s_in_row |= !col.isMilliseconds(); } else { prevTs = ts; col.writeToBuffers(compacted_qual, compacted_val); diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 173ef7e65b..97765a8d70 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -213,10 +213,7 @@ public double nextDoubleValue() { specification.getFunction() == Aggregators.COUNT) { double count = 0; while (values_in_interval.hasNextValue()) { - count += values_in_interval.nextValueCount(); - // WARNING: consume and move next or we'll be stuck in an infinite - // loop here. - values_in_interval.nextDoubleValue(); + count += values_in_interval.nextDoubleValue(); } value = count; } else { diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 273f0ed18b..5edf23509b 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -244,10 +244,7 @@ public double nextDoubleValue() { specification.getFunction() == Aggregators.COUNT) { double count = 0; while (values_in_interval.hasNextValue()) { - count += values_in_interval.nextValueCount(); - // WARNING: consume and move next or we'll be stuck in an infinite - // loop here. - values_in_interval.nextDoubleValue(); + count += values_in_interval.nextDoubleValue(); } value = count; } else { diff --git a/src/core/GroupCallback.java b/src/core/GroupCallback.java new file mode 100644 index 0000000000..da835dbd63 --- /dev/null +++ b/src/core/GroupCallback.java @@ -0,0 +1,30 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2012 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 com.stumbleupon.async.Callback; + +import java.util.ArrayList; + +class GroupCallback implements Callback> { + /** + * We're only waiting for all callbacks to complete, ignoring their return values. + * + * @param ignored The return values of the individual callbacks - ignored + * @return null + */ + @Override + public Object call(ArrayList ignored) { + return null; + } +} diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 0456e47be2..94795d74de 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -18,6 +18,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -45,6 +46,11 @@ final class IncomingDataPoints implements WritableDataPoints { */ static final Histogram putlatency = new Histogram(16000, (short) 2, 100); + /** + * Keep track of the number of UIDs that came back null with auto_metric disabled. + */ + static final AtomicLong auto_metric_rejection_count = new AtomicLong(); + /** The {@code TSDB} instance we belong to. */ private final TSDB tsdb; @@ -137,9 +143,14 @@ static byte[] rowKeyTemplate(final TSDB tsdb, final String metric, short pos = (short) Const.SALT_WIDTH(); - copyInRowKey(row, pos, - (tsdb.config.auto_metric() ? tsdb.metrics.getOrCreateId(metric) - : tsdb.metrics.getId(metric))); + byte[] metric_id = (tsdb.config.auto_metric() ? tsdb.metrics.getOrCreateId(metric) + : tsdb.metrics.getId(metric)); + + if(!tsdb.config.auto_metric() && metric_id == null) { + auto_metric_rejection_count.incrementAndGet(); + } + + copyInRowKey(row, pos, metric_id); pos += metric_width; pos += Const.TIMESTAMP_BYTES; @@ -151,60 +162,6 @@ static byte[] rowKeyTemplate(final TSDB tsdb, final String metric, return row; } - /** - * Returns a partially initialized row key for this metric and these tags. The - * only thing left to fill in is the base timestamp. - * - * @since 2.0 - */ - static Deferred rowKeyTemplateAsync(final TSDB tsdb, - final String metric, final Map tags) { - final short metric_width = tsdb.metrics.width(); - final short tag_name_width = tsdb.tag_names.width(); - final short tag_value_width = tsdb.tag_values.width(); - final short num_tags = (short) tags.size(); - - int row_size = (Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES - + tag_name_width * num_tags + tag_value_width * num_tags); - final byte[] row = new byte[row_size]; - - // Lookup or create the metric ID. - final Deferred metric_id; - if (tsdb.config.auto_metric()) { - metric_id = tsdb.metrics.getOrCreateIdAsync(metric, metric, tags); - } else { - metric_id = tsdb.metrics.getIdAsync(metric); - } - - // Copy the metric ID at the beginning of the row key. - class CopyMetricInRowKeyCB implements Callback { - public byte[] call(final byte[] metricid) { - copyInRowKey(row, (short) Const.SALT_WIDTH(), metricid); - return row; - } - } - - // Copy the tag IDs in the row key. - class CopyTagsInRowKeyCB implements - Callback, ArrayList> { - public Deferred call(final ArrayList tags) { - short pos = (short) (Const.SALT_WIDTH() + metric_width); - pos += Const.TIMESTAMP_BYTES; - for (final byte[] tag : tags) { - copyInRowKey(row, pos, tag); - pos += tag.length; - } - // Once we've resolved all the tags, schedule the copy of the metric - // ID and return the row key we produced. - return metric_id.addCallback(new CopyMetricInRowKeyCB()); - } - } - - // Kick off the resolution of all tags. - return Tags.resolveOrCreateAllAsync(tsdb, metric, tags) - .addCallbackDeferring(new CopyTagsInRowKeyCB()); - } - public void setSeries(final String metric, final Map tags) { checkMetricAndTags(metric, tags); try { @@ -355,12 +312,18 @@ public Deferred call(final Boolean allowed) throws Exception { point.setDurable(!batch_import); return tsdb.client.append(point);/* .addBoth(cb) */ } else { - final PutRequest point = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.table, row, TSDB.FAMILY, - qualifier, value, timestamp); + boolean isLong = ((flags & Const.FLAG_FLOAT) == 0x0); + if (isLong && tsdb.config.use_hbase_counters()) { + AtomicIncrementRequest counterRequest = new AtomicIncrementRequest(tsdb.table, row, TSDB.FAMILY, qualifier, Bytes.getLong(value)); + return tsdb.client.atomicIncrement(counterRequest, !batch_import); + } else { + final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, + qualifier, value); point.setDurable(!batch_import); return tsdb.client.put(point)/* .addBoth(cb) */; } } + } @Override public String toString() { return "IncomingDataPoints.addPointInternal Write Callback"; @@ -391,6 +354,7 @@ private long baseTime() { public Deferred addPoint(final long timestamp, final long value) { final byte[] v; + if (Byte.MIN_VALUE <= value && value <= Byte.MAX_VALUE) { v = new byte[] { (byte) value }; } else if (Short.MIN_VALUE <= value && value <= Short.MAX_VALUE) { diff --git a/src/core/Internal.java b/src/core/Internal.java index 3d1b82171e..146a272c78 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -241,30 +241,51 @@ public static ArrayList extractDataPoints(final ArrayList row, final byte[] qual = kv.qualifier(); final int len = qual.length; final byte[] val = kv.value(); - - if (len % 2 != 0) { - // skip a non data point column - continue; - } else if (len == 2) { // Single-value cell. - // Maybe we need to fix the flags in the qualifier. - final byte[] actual_val = fixFloatingPointValue(qual[1], val); - final byte q = fixQualifierFlags(qual[1], actual_val.length); - final byte[] actual_qual; - - if (q != qual[1]) { // We need to fix the qualifier. - actual_qual = new byte[] { qual[0], q }; // So make a copy. - } else { - actual_qual = qual; // Otherwise use the one we already have. + + // when enable_appends set to true, should get qualifier and value from the HBase Column Value + if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + int idx = 0; + int q_length = 0; + int v_length = 0; + while (idx < kv.value().length) { + q_length = Internal.getQualifierLength(kv.value(), idx); + v_length = Internal.getValueLengthFromQualifier(kv.value(), idx); + final byte[] q = new byte[q_length]; + final byte[] v = new byte[v_length]; + System.arraycopy(kv.value(),idx,q,0,q_length); + System.arraycopy(kv.value(),idx + q_length,v, 0, v_length); + idx += q_length + v_length; + + final Cell cell = new Cell(q, v); + cells.add(cell); } - - final Cell cell = new Cell(actual_qual, actual_val); - cells.add(cell); - continue; - } else if (len == 4 && inMilliseconds(qual[0])) { - // since ms support is new, there's nothing to fix - final Cell cell = new Cell(qual, val); - cells.add(cell); continue; + } else { + + if (len % 2 != 0) { + // skip a non data point column + continue; + } else if (len == 2) { // Single-value cell. + // Maybe we need to fix the flags in the qualifier. + final byte[] actual_val = fixFloatingPointValue(qual[1], val); + final byte q = fixQualifierFlags(qual[1], actual_val.length); + final byte[] actual_qual; + + if (q != qual[1]) { // We need to fix the qualifier. + actual_qual = new byte[]{qual[0], q}; // So make a copy. + } else { + actual_qual = qual; // Otherwise use the one we already have. + } + + final Cell cell = new Cell(actual_qual, actual_val); + cells.add(cell); + continue; + } else if (len == 4 && inMilliseconds(qual[0])) { + // since ms support is new, there's nothing to fix + final Cell cell = new Cell(qual, val); + cells.add(cell); + continue; + } } // Now break it down into Cells. diff --git a/src/core/MultiGetQuery.java b/src/core/MultiGetQuery.java index d12f4afd7e..830a68b5e7 100644 --- a/src/core/MultiGetQuery.java +++ b/src/core/MultiGetQuery.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.SortedMap; import java.util.TreeMap; import java.util.AbstractMap.SimpleEntry; import java.util.concurrent.ConcurrentHashMap; @@ -89,11 +90,11 @@ public class MultiGetQuery { private final Map>>> histMap = Maps.newConcurrentMap(); - private final Deferred> results = - new Deferred>(); + private final Deferred> results = + new Deferred>(); - private final Deferred> histogramResults = - new Deferred>(); + private final Deferred> histogramResults = + new Deferred>(); private final ArrayList> multi_get_tasks; private final ArrayList multi_get_indexs; @@ -608,7 +609,7 @@ void close(final boolean ok) { * Initiate the get requests and return the tree map of results. * @return A non-null tree map of results (may be empty) */ - public Deferred> fetch() { + public Deferred> fetch() { if(tags.isEmpty()) { return Deferred.fromResult(null); } @@ -620,7 +621,7 @@ public Deferred> fetch() { * Initiate the get requests and return the tree map of results. * @return A non-null tree map of results (may be empty) */ - public Deferred> fetchHistogram() { + public Deferred> fetchHistogram() { startFetch(); return histogramResults; } diff --git a/src/core/Query.java b/src/core/Query.java index 3d95834004..5cec6c7c80 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -171,7 +171,23 @@ public void setTimeSeries(final List tsuids, */ public Deferred configureFromQuery(final TSQuery query, final int index); - + + /** + * Prepares a query against HBase by setting up group bys and resolving + * strings to UIDs asynchronously. This replaces calls to all of the setters + * like the {@link setTimeSeries}, {@link setStartTime}, etc. + * Make sure to wait on the deferred return before calling {@link runAsync}. + * @param query The main query to fetch the start and end time from + * @param index The index of which sub query we're executing + * @param force_raw If true, always get the data from the raw table; disables rollups + * @throws IllegalArgumentException if the query was missing sub queries or + * the index was out of bounds. + * @throws NoSuchUniqueName if the name of a metric, or a tag name/value + * does not exist. (Bubbles up through the deferred) + * @since 2.4 + */ + Deferred configureFromQuery(final TSQuery query, final int index, boolean force_raw); + /** * Downsamples the results by specifying a fixed interval between points. *

@@ -181,7 +197,7 @@ public Deferred configureFromQuery(final TSQuery query, * way we get this one data point is by aggregating all the data points of * that interval together using an {@link Aggregator}. This enables you * to compute things like the 5-minute average or 10 minute 99th percentile. - * @param interval Number of seconds wanted between each data point. + * @param interval Number of milliseconds wanted between each data point. * @param downsampler Aggregation function to use to group data points * within an interval. */ @@ -262,7 +278,19 @@ public Deferred configureFromQuery(final TSQuery query, * @return */ public boolean isHistogramQuery(); - + + /** + * @return Whether or not this is a rollup query + * @since 2.4 + */ + public boolean isRollupQuery(); + + /** + * @return whether this query needs to be split. + * @since 2.4 + */ + public boolean needsSplitting(); + /** * Set the percentile calculation parameters for this query if this is * a histogram query diff --git a/src/core/RpcResponder.java b/src/core/RpcResponder.java new file mode 100644 index 0000000000..97e7b22bb8 --- /dev/null +++ b/src/core/RpcResponder.java @@ -0,0 +1,110 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2017 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 com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import net.opentsdb.utils.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * This class is responsible for building result of requests and + * respond to clients asynchronously. + * + * It can reduce requests that stacking in AsyncHBase, especially put requests. + * When a HBase's RPC has completed, the "AsyncHBase I/O worker" just decodes + * the response, and then do callback by this class asynchronously. We should + * take up workers as short as possible time so that workers can remove RPCs + * from in-flight state more quickly. + * + */ +public class RpcResponder { + + private static final Logger LOG = LoggerFactory.getLogger(RpcResponder.class); + + public static final String TSD_RESPONSE_ASYNC_KEY = "tsd.core.response.async"; + public static final boolean TSD_RESPONSE_ASYNC_DEFAULT = true; + + public static final String TSD_RESPONSE_WORKER_NUM_KEY = + "tsd.core.response.worker.num"; + public static final int TSD_RESPONSE_WORKER_NUM_DEFAULT = 10; + + private final boolean async; + private ExecutorService responders; + private volatile boolean running = true; + + RpcResponder(final Config config) { + async = config.getBoolean(TSD_RESPONSE_ASYNC_KEY, + TSD_RESPONSE_ASYNC_DEFAULT); + + if (async) { + int threads = config.getInt(TSD_RESPONSE_WORKER_NUM_KEY, + TSD_RESPONSE_WORKER_NUM_DEFAULT); + responders = Executors.newFixedThreadPool(threads, + new ThreadFactoryBuilder() + .setNameFormat("OpenTSDB Responder #%d") + .setDaemon(true) + .setUncaughtExceptionHandler(new ExceptionHandler()) + .build()); + } + + LOG.info("RpcResponder mode: {}", async ? "async" : "sync"); + } + + public void response(Runnable run) { + if (async) { + if (running) { + responders.execute(run); + } else { + throw new IllegalStateException("RpcResponder is closing or closed."); + } + } else { + run.run(); + } + } + + public void close() { + if (running) { + running = false; + responders.shutdown(); + } + + boolean completed; + try { + completed = responders.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + completed = false; + } + + if (!completed) { + LOG.warn( + "There are still some results that are not returned to the clients."); + } + } + + public boolean isAsync() { + return async; + } + + private class ExceptionHandler implements Thread.UncaughtExceptionHandler { + @Override + public void uncaughtException(Thread t, Throwable e) { + LOG.error("Run into an uncaught exception in thread: " + t.getName(), e); + } + } +} diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index aaac0eb0b2..dc0cc0de15 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -12,17 +12,19 @@ // see . package net.opentsdb.core; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; import java.util.TreeMap; +import java.util.AbstractMap.SimpleEntry; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import net.opentsdb.meta.Annotation; @@ -70,9 +72,9 @@ public class SaltScanner { /** 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; + private final SortedMap spans; - private final TreeMap histSpans; + private final SortedMap histSpans; /** The list of pre-configured scanners. One scanner should be created per * salt bucket. */ @@ -93,11 +95,11 @@ public class SaltScanner { List>>>(); /** A deferred to call with the spans on completion */ - private final Deferred> results = - new Deferred>(); + private final Deferred> results = + new Deferred>(); - private final Deferred> histogramResults = - new Deferred>(); + private final Deferred> histogramResults = + 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. */ @@ -124,7 +126,7 @@ public class SaltScanner { private final long max_bytes; /** A latch used to determine how many scanners are still running */ - private final CountDownLatch countdown; + private final AtomicInteger countdown; /** When the scanning started. We store the scan latency once all scanners * are done.*/ @@ -226,8 +228,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, } this.scanners = scanners; - this.spans = spans; - this.histSpans = histogramSpans; + this.spans = spans != null ? Collections.synchronizedSortedMap(spans) : null; + this.histSpans = histogramSpans != null ? Collections.synchronizedSortedMap(histogramSpans) : null; this.metric = metric; this.tsdb = tsdb; this.filters = filters; @@ -235,7 +237,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.rollup_query = rollup_query; this.query_stats = query_stats; this.query_index = query_index; - countdown = new CountDownLatch(scanners.size()); + countdown = new AtomicInteger(scanners.size()); if (rollup_query != null && RollupQuery.isValidQuery(rollup_query)) { is_rollup = true; if (rollup_query.getRollupAgg() == Aggregators.AVG) { @@ -264,7 +266,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * first error will be returned, others will be logged. * @return A deferred to wait on for results. */ - public Deferred> scan() { + public Deferred> scan() { start_time = System.currentTimeMillis(); int i = 0; for (final Scanner scanner: scanners) { @@ -273,7 +275,7 @@ public Deferred> scan() { return results; } - public Deferred> scanHistogram() { + public Deferred> scanHistogram() { start_time = DateTime.currentTimeMillis(); int index = 0; @@ -462,7 +464,7 @@ final class ScannerCB implements Callback>> { private final Scanner scanner; private final int index; - private final List kvs = new ArrayList(); + private final List kvs = Collections.synchronizedList(new ArrayList()); private final ByteMap> annotations = new ByteMap>(); private final Set skips = Collections.newSetFromMap( @@ -489,7 +491,8 @@ final class ScannerCB implements Callback> rows) @@ -552,7 +555,24 @@ public Object call(final ArrayList> rows) final List> lookups = filters != null && !filters.isEmpty() ? new ArrayList>(rows.size()) : null; - + + // fail the query when the timeout exceeded + if (this.query_timeout > 0 && fetch_time > (this.query_timeout * 1000000)) { + try { + close(false); + handleException( + new QueryException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, + "Sorry, your query timed out. Time limit: " + + this.query_timeout + " ms, fetch time: " + + (double)(fetch_time)/1000000 + " ms. Please try filtering " + + "using more tags or decrease your time range.")); + return false; + } catch (Exception e) { + LOG.error("Sorry, Scanner is closed: " + scanner, e); + return false; + } + } + // validation checking before processing the next set of results. It's // kinda funky but we want to allow queries to sneak through that were // just a *tad* over the limits so that's why we don't check at the @@ -909,7 +929,7 @@ void close(final boolean ok) { if (ok && exception == null) { validateAndTriggerCallback(kvs, annotations, histograms); } else { - countdown.countDown(); + countdown.decrementAndGet(); } } } @@ -924,10 +944,9 @@ private void validateAndTriggerCallback( final Map> annotations, final List>> histograms) { - countdown.countDown(); - final long count = countdown.getCount(); + int scannersRunning = countdown.decrementAndGet(); if (kvs.size() > 0) { - kv_map.put((int) count, kvs); + kv_map.put(scannersRunning, kvs); } for (final byte[] key : annotations.keySet()) { @@ -939,10 +958,10 @@ private void validateAndTriggerCallback( } if (histograms.size() > 0) { - histMap.put((int) count, histograms); + histMap.put(scannersRunning, histograms); } - - if (countdown.getCount() <= 0) { + + if (scannersRunning <= 0) { try { mergeAndReturnResults(); } catch (final Exception ex) { @@ -960,7 +979,7 @@ private void validateAndTriggerCallback( */ private void handleException(final Exception e) { // make sure only one scanner can set the exception - countdown.countDown(); + countdown.decrementAndGet(); if (exception == null) { synchronized (this) { if (exception == null) { diff --git a/src/core/SeekableViewChain.java b/src/core/SeekableViewChain.java new file mode 100644 index 0000000000..63c0454240 --- /dev/null +++ b/src/core/SeekableViewChain.java @@ -0,0 +1,97 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2014 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.List; +import java.util.NoSuchElementException; + +public class SeekableViewChain implements SeekableView { + + private final List iterators; + private int currentIterator; + + SeekableViewChain(List iterators) { + this.iterators = iterators; + } + + /** + * Returns {@code true} if this view has more elements. + */ + @Override + public boolean hasNext() { + SeekableView iterator = getCurrentIterator(); + return iterator != null && iterator.hasNext(); + } + + /** + * Returns a view on the next data point. + * No new object gets created, the referenced returned is always the same + * and must not be stored since its internal data structure will change the + * next time {@code next()} is called. + * + * @throws NoSuchElementException if there were no more elements to iterate + * on (in which case {@link #hasNext} would have returned {@code false}. + */ + @Override + public DataPoint next() { + SeekableView iterator = getCurrentIterator(); + if (iterator == null || !iterator.hasNext()) { + throw new NoSuchElementException("No elements left in iterator"); + } + + DataPoint next = iterator.next(); + + if (!iterator.hasNext()) { + currentIterator++; + } + + return next; + } + + /** + * Unsupported operation. + * + * @throws UnsupportedOperationException always. + */ + @Override + public void remove() { + throw new UnsupportedOperationException("Removing items is not supported"); + } + + /** + * Advances the iterator to the given point in time. + *

+ * This allows the iterator to skip all the data points that are strictly + * before the given timestamp. + * + * @param timestamp A strictly positive 32 bit UNIX timestamp (in seconds). + * @throws IllegalArgumentException if the timestamp is zero, or negative, + * or doesn't fit on 32 bits (think "unsigned int" -- yay Java!). + */ + @Override + public void seek(long timestamp) { + for (final SeekableView it : iterators) { + it.seek(timestamp); + } + } + + private SeekableView getCurrentIterator() { + while (currentIterator < iterators.size()) { + if (iterators.get(currentIterator).hasNext()) { + return iterators.get(currentIterator); + } + currentIterator++; + } + return null; + } +} diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 94bed154d0..07beaaa5d5 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -12,14 +12,7 @@ // see . package net.opentsdb.core; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; @@ -52,7 +45,7 @@ * {@link Aggregator}) are given. This is done by using a special * iterator when using the {@link Span.DownsamplingIterator}. */ -final class SpanGroup implements DataPoints { +final class SpanGroup extends AbstractSpanGroup { /** Annotations */ private final ArrayList annotations; @@ -109,7 +102,10 @@ final class SpanGroup implements DataPoints { /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; - + + /** The group we belong to */ + private byte[] group; + /** * Ctor. * @param tsdb The TSDB we belong to. @@ -231,7 +227,7 @@ final class SpanGroup implements DataPoints { final long query_end, final int query_index) { this(tsdb, start_time, end_time, spans, rate, rate_options, aggregator, - downsampler, query_start, query_end, query_index, null); + downsampler, query_start, query_end, query_index, null, new byte[0]); } /** @@ -265,7 +261,8 @@ final class SpanGroup implements DataPoints { final long query_start, final long query_end, final int query_index, - final RollupQuery rollup_query) { + final RollupQuery rollup_query, + byte[] group) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; @@ -285,6 +282,7 @@ final class SpanGroup implements DataPoints { this.query_index = query_index; this.rollup_query = rollup_query; this.tsdb = tsdb; + this.group = group; } /** @@ -531,32 +529,22 @@ public SeekableView iterator() { rate, rate_options, rollup_query); } - /** - * Finds the {@code i}th data point of this group in {@code O(n)}. - * Where {@code n} is the number of data points in this group. - */ - private DataPoint getDataPoint(int i) { - if (i < 0) { - throw new IndexOutOfBoundsException("negative index: " + i); - } - final int saved_i = i; - final SeekableView it = iterator(); - DataPoint dp = null; - while (it.hasNext() && i >= 0) { - dp = it.next(); - i--; - } - if (i != -1 || dp == null) { - throw new IndexOutOfBoundsException("index " + saved_i - + " too large (it's >= " + size() + ") for " + this); - } - return dp; - } - public long timestamp(final int i) { return getDataPoint(i).timestamp(); } + /** + * Returns the group the spans in here belong to. + * + * Returns null if the NONE aggregator was requested in the query + * Returns an empty array if there were no group bys and they're all in the same group + * Returns the group otherwise + * @return The group + */ + public byte[] group() { + return group; + } + public boolean isInteger(final int i) { return getDataPoint(i).isInteger(); } @@ -585,7 +573,8 @@ private String toStringSharedAttributes() { + ", aggregator=" + aggregator + ", downsampler=" + downsampler + ", query_start=" + query_start - + ", query_end" + query_end + + ", query_end=" + query_end + + ", group=" + Arrays.toString(group) + ')'; } @@ -602,6 +591,10 @@ public boolean isPercentile() { public float getPercentile() { throw new UnsupportedOperationException("getPercentile not supported"); } + + public List getSpans() { + return spans; + } /** * Resolves the set of tag keys to their string names. diff --git a/src/core/SplitRollupQuery.java b/src/core/SplitRollupQuery.java new file mode 100644 index 0000000000..7121c99762 --- /dev/null +++ b/src/core/SplitRollupQuery.java @@ -0,0 +1,480 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2012 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 com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.uid.NoSuchUniqueName; +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.HBaseException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +public class SplitRollupQuery extends AbstractQuery { + + private TSDB tsdb; + + private TsdbQuery rollupQuery; + private TsdbQuery rawQuery; + + private Deferred rollupResolution; + private Deferred rawResolution; + + SplitRollupQuery(final TSDB tsdb, TsdbQuery rollupQuery, Deferred rollupResolution) { + if (rollupQuery == null) { + throw new IllegalArgumentException("Rollup query cannot be null"); + } + + this.tsdb = tsdb; + + this.rollupQuery = rollupQuery; + this.rollupResolution = rollupResolution; + } + + /** + * Returns the start time of the graph. + * + * @return A strictly positive integer. + * @throws IllegalStateException if {@link #setStartTime(long)} was never + * called on this instance before. + */ + @Override + public long getStartTime() { + return rollupQuery != null ? rollupQuery.getStartTime() : rawQuery.getStartTime(); + } + + /** + * Sets the start time of the graph. Converts the timestamp to milliseconds if necessary. + * + * @param timestamp The start time, all the data points returned will have a + * timestamp greater than or equal to this one. + * @throws IllegalArgumentException if timestamp is less than or equal to 0, + * or if it can't fit on 32 bits. + * @throws IllegalArgumentException if + * {@code timestamp >= }{@link #getEndTime getEndTime}. + */ + @Override + public void setStartTime(long timestamp) { + if ((timestamp & Const.SECOND_MASK) == 0) { timestamp *= 1000L; } + + if (rollupQuery == null) { + rawQuery.setStartTime(timestamp); + return; + } + + if (rollupQuery.getEndTime() <= timestamp) { + rollupQuery = null; + rawQuery.setStartTime(timestamp); + return; + } + + rollupQuery.setStartTime(timestamp); + } + + /** + * Returns the end time of the graph. + *

+ * If {@link #setEndTime} was never called before, this method will + * automatically execute + * {@link #setEndTime setEndTime}{@code (System.currentTimeMillis() / 1000)} + * to set the end time. + * + * @return A strictly positive integer. + */ + @Override + public long getEndTime() { + return rawQuery != null ? rawQuery.getEndTime() : rollupQuery.getEndTime(); + } + + /** + * Sets the end time of the graph. Converts the timestamp to milliseconds if necessary. + * + * @param timestamp The end time, all the data points returned will have a + * timestamp less than or equal to this one. + * @throws IllegalArgumentException if timestamp is less than or equal to 0, + * or if it can't fit on 32 bits. + * @throws IllegalArgumentException if + * {@code timestamp <= }{@link #getStartTime getStartTime}. + */ + @Override + public void setEndTime(long timestamp) { + if ((timestamp & Const.SECOND_MASK) == 0) { timestamp *= 1000L; } + + rawQuery.setEndTime(timestamp); + } + + /** + * Returns whether or not the data queried will be deleted. + * + * @return A boolean + * @since 2.4 + */ + @Override + public boolean getDelete() { + return rawQuery.getDelete(); + } + + /** + * Sets whether or not the data queried will be deleted. + * + * @param delete True if data should be deleted, false otherwise. + * @since 2.4 + */ + @Override + public void setDelete(boolean delete) { + if (rollupQuery != null) { + rollupQuery.setDelete(delete); + } + rawQuery.setDelete(delete); + } + + /** + * Sets the time series to the query. + * + * @param metric The metric to retrieve from the TSDB. + * @param tags The set of tags of interest. + * @param function The aggregation function to use. + * @param rate If true, the rate of the series will be used instead of the + * actual values. + * @param rate_options If included specifies additional options that are used + * when calculating and graph rate values + * @throws NoSuchUniqueName if the name of a metric, or a tag name/value + * does not exist. + * @since 2.4 + */ + @Override + public void setTimeSeries(String metric, + Map tags, + Aggregator function, + boolean rate, + RateOptions rate_options) throws NoSuchUniqueName { + if (rollupQuery != null) { + rollupQuery.setTimeSeries(metric, tags, function, rate, rate_options); + } + rawQuery.setTimeSeries(metric, tags, function, rate, rate_options); + } + + /** + * Sets the time series to the query. + * + * @param metric The metric to retrieve from the TSDB. + * @param tags The set of tags of interest. + * @param function The aggregation function to use. + * @param rate If true, the rate of the series will be used instead of the + * actual values. + * @throws NoSuchUniqueName if the name of a metric, or a tag name/value + * does not exist. + */ + @Override + public void setTimeSeries(String metric, Map tags, Aggregator function, boolean rate) throws NoSuchUniqueName { + if (rollupQuery != null) { + rollupQuery.setTimeSeries(metric, tags, function, rate); + } + rawQuery.setTimeSeries(metric, tags, function, rate); + } + + /** + * Sets up a query for the given timeseries UIDs. For now, all TSUIDs in the + * group must share a common metric. This is to avoid issues where the scanner + * may have to traverse the entire data table if one TSUID has a metric of + * 000001 and another has a metric of FFFFFF. After modifying the query code + * to run asynchronously and use different scanners, we can allow different + * TSUIDs. + * Note: This method will not check to determine if the TSUIDs are + * valid, since that wastes time and we *assume* that the user provides TSUIDs + * that are up to date. + * + * @param tsuids A list of one or more TSUIDs to scan for + * @param function The aggregation function to use on results + * @param rate Whether or not the results should be converted to a rate + * @throws IllegalArgumentException if the tsuid list is null, empty or the + * TSUIDs do not share a common metric + * @since 2.4 + */ + @Override + public void setTimeSeries(List tsuids, Aggregator function, boolean rate) { + if (rollupQuery != null) { + rollupQuery.setTimeSeries(tsuids, function, rate); + } + rawQuery.setTimeSeries(tsuids, function, rate); + } + + /** + * Sets up a query for the given timeseries UIDs. For now, all TSUIDs in the + * group must share a common metric. This is to avoid issues where the scanner + * may have to traverse the entire data table if one TSUID has a metric of + * 000001 and another has a metric of FFFFFF. After modifying the query code + * to run asynchronously and use different scanners, we can allow different + * TSUIDs. + * Note: This method will not check to determine if the TSUIDs are + * valid, since that wastes time and we *assume* that the user provides TSUIDs + * that are up to date. + * + * @param tsuids A list of one or more TSUIDs to scan for + * @param function The aggregation function to use on results + * @param rate Whether or not the results should be converted to a rate + * @param rate_options If included specifies additional options that are used + * when calculating and graph rate values + * @throws IllegalArgumentException if the tsuid list is null, empty or the + * TSUIDs do not share a common metric + * @since 2.4 + */ + @Override + public void setTimeSeries(List tsuids, Aggregator function, boolean rate, RateOptions rate_options) { + if (rollupQuery != null) { + rollupQuery.setTimeSeries(tsuids, function, rate, rate_options); + } + rawQuery.setTimeSeries(tsuids, function, rate, rate_options); + } + + /** + * Prepares a query against HBase by setting up group bys and resolving + * strings to UIDs asynchronously. This replaces calls to all of the setters + * like the {@link setTimeSeries}, {@link setStartTime}, etc. + * Make sure to wait on the deferred return before calling {@link runAsync}. + * + * @param query The main query to fetch the start and end time from + * @param index The index of which sub query we're executing + * @return A deferred to wait on for UID resolution. The result doesn't have + * any meaning and can be discarded. + * @throws IllegalArgumentException if the query was missing sub queries or + * the index was out of bounds. + * @throws NoSuchUniqueName if the name of a metric, or a tag name/value + * does not exist. (Bubbles up through the deferred) + * @since 2.4 + */ + @Override + public Deferred configureFromQuery(TSQuery query, int index) { + return configureFromQuery(query, index, false); + } + + @Override + public Deferred configureFromQuery(TSQuery query, int index, boolean force_raw) { + if (force_raw) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + if (!rollupQuery.needsSplitting()) { + return rollupResolution; + } + + rawQuery = new TsdbQuery(tsdb); + rawResolution = rollupQuery.split(query, index, rawQuery); + + if (rollupQuery.getRollupQuery().getLastRollupTimestampSeconds() * 1000L < rollupQuery.getStartTime()) { + // We're looking at a query that would normally hit a rollup table, but the table doesn't + // have data guaranteed to be available for the requested time period or any part of it + // (i.e. the last guaranteed rollup point is before the query actually starts) + // So we won't bother running it. + rollupQuery = null; + } + + return Deferred.group(rollupResolution, rawResolution).addCallback(new GroupCallback()); + } + + /** + * Downsamples the results by specifying a fixed interval between points. + *

+ * Technically, downsampling means reducing the sampling interval. Here + * the idea is similar. Instead of returning every single data point that + * matched the query, we want one data point per fixed time interval. The + * way we get this one data point is by aggregating all the data points of + * that interval together using an {@link Aggregator}. This enables you + * to compute things like the 5-minute average or 10 minute 99th percentile. + * + * @param interval Number of seconds wanted between each data point. + * @param downsampler Aggregation function to use to group data points + */ + @Override + public void downsample(long interval, Aggregator downsampler) { + if (rollupQuery != null) { + rollupQuery.downsample(interval, downsampler); + } + rawQuery.downsample(interval, downsampler); + } + + /** + * Sets an optional downsampling function on this query + * + * @param interval The interval, in milliseconds to rollup data points + * @param downsampler An aggregation function to use when rolling up data points + * @param fill_policy Policy specifying whether to interpolate or to fill + * missing intervals with special values. + * @throws NullPointerException if the aggregation function is null + * @throws IllegalArgumentException if the interval is not greater than 0 + * @since 2.4 + */ + @Override + public void downsample(long interval, Aggregator downsampler, FillPolicy fill_policy) { + if (rollupQuery != null) { + rollupQuery.downsample(interval, downsampler, fill_policy); + } + rawQuery.downsample(interval, downsampler, fill_policy); + } + + /** + * Executes the query asynchronously + * + * @return The data points matched by this query. + *

+ * Each element in the non-{@code null} but possibly empty array returned + * corresponds to one time series for which some data points have been + * matched by the query. + * @throws HBaseException if there was a problem communicating with HBase to + * perform the search. + * @since 1.2 + */ + @Override + public Deferred runAsync() throws HBaseException { + Deferred rollupResults = Deferred.fromResult(new DataPoints[0]); + if (rollupQuery != null) { + rollupResults = rollupQuery.runAsync(); + } + Deferred rawResults = rawQuery.runAsync(); + + return Deferred.groupInOrder(Arrays.asList(rollupResults, rawResults)).addCallback(new RunCB()); + } + + /** + * Runs this query asynchronously. + * + * @return The data points matched by this query and applied with percentile calculation + *

+ * Each element in the non-{@code null} but possibly empty array returned + * corresponds to one time series for which some data points have been + * matched by the query. + * @throws HBaseException if there was a problem communicating with HBase to + * perform the search. + * @throws IllegalStateException if the query is not a histogram query + */ + @Override + public Deferred runHistogramAsync() throws HBaseException { + Deferred rollupResults = Deferred.fromResult(new DataPoints[0]); + if (rollupQuery != null) { + rollupResults = rollupQuery.runHistogramAsync(); + } Deferred rawResults = rawQuery.runHistogramAsync(); + + return Deferred.groupInOrder(Arrays.asList(rollupResults, rawResults)).addCallback(new RunCB()); + } + + /** + * Returns an index for this sub-query in the original set of queries. + * + * @return A zero based index. + * @since 2.4 + */ + @Override + public int getQueryIdx() { + return rawQuery.getQueryIdx(); + } + + /** + * Check this is a histogram query or not + * + * @return + */ + @Override + public boolean isHistogramQuery() { + return rawQuery.isHistogramQuery(); + } + + /** + * Check this is a rollup query or not + * + * @return Whether or not this is a rollup query + * @since 2.4 + */ + @Override + public boolean isRollupQuery() { + return rollupQuery != null && RollupQuery.isValidQuery(rollupQuery.getRollupQuery()); + } + + /** + * @since 2.4 + */ + @Override + public boolean needsSplitting() { + // No further splitting supported + return false; + } + + /** + * Set the percentile calculation parameters for this query if this is + * a histogram query + * + * @param percentiles + */ + @Override + public void setPercentiles(List percentiles) { + if (rollupQuery != null) { + rollupQuery.setPercentiles(percentiles); + } + rawQuery.setPercentiles(percentiles); + } + + private class RunCB implements Callback> { + + private ByteMap makeSpanGroupMap(DataPoints[] dataPointsArray) { + ByteMap map = new ByteMap<>(); + + for (DataPoints points : dataPointsArray) { + if (!(points instanceof SpanGroup)) { + throw new IllegalArgumentException("Only SpanGroups implemented"); + } + SpanGroup spanGroup = (SpanGroup) points; + map.put(spanGroup.group(), spanGroup); + } + + return map; + } + + private DataPoints[] merge(DataPoints[] rollup, DataPoints[] raw) { + ByteMap rollupResults = makeSpanGroupMap(rollup); + ByteMap rawResults = makeSpanGroupMap(raw); + + TreeSet allGroups = new TreeSet<>(Bytes.MEMCMP); + allGroups.addAll(rollupResults.keySet()); + allGroups.addAll(rawResults.keySet()); + + List results = new ArrayList<>(allGroups.size()); + + for (byte[] group : allGroups) { + SpanGroup rawGroup = rawResults.get(group); + SpanGroup rollupGroup = rollupResults.get(group); + results.add(new SplitRollupSpanGroup(rollupGroup, rawGroup)); + } + + return results.toArray(new DataPoints[0]); + } + + /** + * After both queries have run, merge their results + * + * @param dataPointArrays The results from both queries + * @return The merged data points + */ + @Override + public DataPoints[] call(ArrayList dataPointArrays) { + DataPoints[] rollupResults = dataPointArrays.get(0); + DataPoints[] rawResults = dataPointArrays.get(1); + + return merge(rollupResults, rawResults); + } + } +} diff --git a/src/core/SplitRollupSpanGroup.java b/src/core/SplitRollupSpanGroup.java new file mode 100644 index 0000000000..dd6ab87830 --- /dev/null +++ b/src/core/SplitRollupSpanGroup.java @@ -0,0 +1,417 @@ +package net.opentsdb.core; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import net.opentsdb.meta.Annotation; +import org.hbase.async.Bytes; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SplitRollupSpanGroup extends AbstractSpanGroup { + private final List spanGroups = new ArrayList<>(); + + public SplitRollupSpanGroup(SpanGroup... groups) { + for (SpanGroup group : groups) { + if (group != null) { + spanGroups.add(group); + } + } + + if (spanGroups.isEmpty()) { + throw new IllegalArgumentException("At least one SpanGroup must be non-null"); + } + } + + /** + * Returns the name of the series. + */ + @Override + public String metricName() { + return spanGroups.get(0).metricName(); + } + + /** + * Returns the name of the series. + * + * @since 1.2 + */ + @Override + public Deferred metricNameAsync() { + return spanGroups.get(0).metricNameAsync(); + } + + /** + * @return the metric UID + * @since 2.3 + */ + @Override + public byte[] metricUID() { + return spanGroups.get(0).metricUID(); + } + + /** + * Returns the tags associated with these data points. + * + * @return A non-{@code null} map of tag names (keys), tag values (values). + */ + @Override + public Map getTags() { + Map tags = new HashMap<>(); + + for (SpanGroup group : spanGroups) { + tags.putAll(group.getTags()); + } + + return tags; + } + + /** + * Returns the tags associated with these data points. + * + * @return A non-{@code null} map of tag names (keys), tag values (values). + * @since 1.2 + */ + @Override + public Deferred> getTagsAsync() { + class GetTagsCB implements Callback, ArrayList>> { + @Override + public Map call(ArrayList> resolvedTags) throws Exception { + Map tags = new HashMap<>(); + for (Map groupTags : resolvedTags) { + tags.putAll(groupTags); + } + return tags; + } + } + + List>> deferreds = new ArrayList<>(spanGroups.size()); + + for (SpanGroup group : spanGroups) { + deferreds.add(group.getTagsAsync()); + } + + return Deferred.groupInOrder(deferreds).addCallback(new GetTagsCB()); + } + + /** + * Returns a map of tag pairs as UIDs. + * When used on a span or row, it returns the tag set. When used on a span + * group it will return only the tag pairs that are common across all + * time series in the group. + * + * @return A potentially empty map of tagk to tagv pairs as UIDs + * @since 2.2 + */ + @Override + public Bytes.ByteMap getTagUids() { + Bytes.ByteMap tagUids = new Bytes.ByteMap<>(); + + for (SpanGroup group : spanGroups) { + tagUids.putAll(group.getTagUids()); + } + + return tagUids; + } + + /** + * Returns the tags associated with some but not all of the data points. + *

+ * When this instance represents the aggregation of multiple time series + * (same metric but different tags), {@link #getTags} returns the tags that + * are common to all data points (intersection set) whereas this method + * returns all the tags names that are not common to all data points (union + * set minus the intersection set, also called the symmetric difference). + *

+ * If this instance does not represent an aggregation of multiple time + * series, the list returned is empty. + * + * @return A non-{@code null} list of tag names. + */ + @Override + public List getAggregatedTags() { + List aggregatedTags = new ArrayList<>(); + + for (SpanGroup group : spanGroups) { + aggregatedTags.addAll(group.getAggregatedTags()); + } + + return aggregatedTags; + } + + /** + * Returns the tags associated with some but not all of the data points. + *

+ * When this instance represents the aggregation of multiple time series + * (same metric but different tags), {@link #getTags} returns the tags that + * are common to all data points (intersection set) whereas this method + * returns all the tags names that are not common to all data points (union + * set minus the intersection set, also called the symmetric difference). + *

+ * If this instance does not represent an aggregation of multiple time + * series, the list returned is empty. + * + * @return A non-{@code null} list of tag names. + * @since 1.2 + */ + @Override + public Deferred> getAggregatedTagsAsync() { + class GetAggregatedTagsCB implements Callback, ArrayList>> { + @Override + public List call(ArrayList> resolvedTags) throws Exception { + List aggregatedTags = new ArrayList<>(); + for (List groupTags : resolvedTags) { + aggregatedTags.addAll(groupTags); + } + return aggregatedTags; + } + } + + List>> deferreds = new ArrayList<>(spanGroups.size()); + for (SpanGroup group : spanGroups) { + deferreds.add(group.getAggregatedTagsAsync()); + } + + return Deferred.groupInOrder(deferreds).addCallback(new GetAggregatedTagsCB()); + } + + /** + * Returns the tagk UIDs associated with some but not all of the data points. + * + * @return a non-{@code null} list of tagk UIDs. + * @since 2.3 + */ + @Override + public List getAggregatedTagUids() { + List aggTagUids = new ArrayList<>(); + + for (SpanGroup group : spanGroups) { + aggTagUids.addAll(group.getAggregatedTagUids()); + } + + return aggTagUids; + } + + /** + * Returns a list of unique TSUIDs contained in the results + * + * @return an empty list if there were no results, otherwise a list of TSUIDs + */ + @Override + public List getTSUIDs() { + List tsuids = new ArrayList<>(); + + for (SpanGroup group : spanGroups) { + tsuids.addAll(group.getTSUIDs()); + } + + return tsuids; + } + + /** + * Compiles the annotations for each span into a new array list + * + * @return Null if none of the spans had any annotations, a list if one or + * more were found + */ + @Override + public List getAnnotations() { + List annotations = new ArrayList<>(); + + for (SpanGroup group : spanGroups) { + List groupAnnotations = group.getAnnotations(); + if (groupAnnotations != null) { + annotations.addAll(group.getAnnotations()); + } + } + + return annotations; + } + + /** + * Returns the number of data points. + *

+ * This method must be implemented in {@code O(1)} or {@code O(n)} + * where n = {@link #aggregatedSize} > 0. + * + * @return A positive integer. + */ + @Override + public int size() { + int size = 0; + for (SpanGroup group : spanGroups) { + size += group.size(); + } + return size; + } + + /** + * Returns the number of data points aggregated in this instance. + *

+ * When this instance represents the aggregation of multiple time series + * (same metric but different tags), {@link #size} returns the number of data + * points after aggregation, whereas this method returns the number of data + * points before aggregation. + *

+ * If this instance does not represent an aggregation of multiple time + * series, then 0 is returned. + * + * @return A positive integer. + */ + @Override + public int aggregatedSize() { + int aggregatedSize = 0; + for (SpanGroup group : spanGroups) { + aggregatedSize += group.aggregatedSize(); + } + return aggregatedSize; + } + + /** + * Returns a zero-copy view to go through {@code size()} data points. + *

+ * The iterator returned must return each {@link DataPoint} in {@code O(1)}. + * The {@link DataPoint} returned must not be stored and gets + * invalidated as soon as {@code next} is called on the iterator. If you + * want to store individual data points, you need to copy the timestamp + * and value out of each {@link DataPoint} into your own data structures. + */ + @Override + public SeekableView iterator() { + List iterators = new ArrayList<>(); + for (SpanGroup group : spanGroups) { + iterators.add(group.iterator()); + } + return new SeekableViewChain(iterators); + } + + /** + * Returns the timestamp associated with the {@code i}th data point. + * The first data point has index 0. + *

+ * This method must be implemented in + * O({@link #aggregatedSize}) or better. + *

+ * It is guaranteed that

timestamp(i) < timestamp(i+1)
+ * + * @param i + * @return A strictly positive integer. + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + */ + @Override + public long timestamp(int i) { + return getDataPoint(i).timestamp(); + } + + /** + * Tells whether or not the {@code i}th value is of integer type. + * The first data point has index 0. + *

+ * This method must be implemented in + * O({@link #aggregatedSize}) or better. + * + * @param i + * @return {@code true} if the {@code i}th value is of integer type, + * {@code false} if it's of floating point type. + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + */ + @Override + public boolean isInteger(int i) { + return getDataPoint(i).isInteger(); + } + + /** + * Returns the value of the {@code i}th data point as a long. + * The first data point has index 0. + *

+ * This method must be implemented in + * O({@link #aggregatedSize}) or better. + * Use {@link #iterator} to get successive {@code O(1)} accesses. + * + * @param i + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + * @throws ClassCastException if the + * {@link #isInteger isInteger(i)} == false. + * @see #iterator + */ + @Override + public long longValue(int i) { + return getDataPoint(i).longValue(); + } + + /** + * Returns the value of the {@code i}th data point as a float. + * The first data point has index 0. + *

+ * This method must be implemented in + * O({@link #aggregatedSize}) or better. + * Use {@link #iterator} to get successive {@code O(1)} accesses. + * + * @param i + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + * @throws ClassCastException if the + * {@link #isInteger isInteger(i)} == true. + * @see #iterator + */ + @Override + public double doubleValue(int i) { + return getDataPoint(i).doubleValue(); + } + + /** + * Return the query index that maps this datapoints to the original TSSubQuery. + * + * @return index of the query in the TSQuery class + * @throws UnsupportedOperationException if the implementing class can't map + * to a sub query. + * @since 2.2 + */ + @Override + public int getQueryIndex() { + return spanGroups.get(0).getQueryIndex(); + } + + /** + * Return whether these data points are the result of the percentile calculation + * on the histogram data points. The client can call {@code getPercentile} to get + * the percentile calculation parameter. + * + * @return true or false + * @since 2.4 + */ + @Override + public boolean isPercentile() { + return spanGroups.get(0).isPercentile(); + } + + /** + * Return the percentile calculation parameter. This interface and {@code isPercentile} are used + * to convert {@code HistogramDataPoints} to {@code DataPoints} + * + * @return the percentile parameter + * @since 2.4 + */ + @Override + public float getPercentile() { + return spanGroups.get(0).getPercentile(); + } + + /** + * Returns the group the spans in here belong to. + *

+ * Returns null if the NONE aggregator was requested in the query + * Returns an empty array if there were no group bys and they're all in the same group + * Returns the group otherwise + * + * @return The group + */ + public byte[] group() { + return spanGroups.get(0).group(); + } +} diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 119c207a24..36853cf1d1 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -23,6 +23,8 @@ import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import org.hbase.async.RegionLocation; +import org.hbase.async.HBaseRpc; import com.google.common.base.Strings; import com.google.common.io.Files; @@ -32,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.AppendRequest; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; @@ -101,11 +104,40 @@ public final class TSDB { /** The operation mode (role) of the TSD. */ public enum OperationMode { - READWRITE, - READONLY, - WRITEONLY + READWRITE(true, true), + READONLY(true, false), + WRITEONLY(false, true); + + private final boolean read; + private final boolean write; + + OperationMode(boolean read, boolean write) { + this.read = read; + this.write = write; + } + + /** Whether this mode allows reading */ + public boolean isRead() { + return read; + } + + /** Whether this mode allows writing */ + public boolean isWrite() { + return write; + } } - + + /** Whether tables are fully available, partially available, or unavailable. + * + * The order matters, since we do ordinal comparison—lower should mean + * less available. + */ + public enum TableAvailability { + NONE, + PARTIAL, + FULL, + } + /** Client for the HBase cluster to use. */ final HBaseClient client; @@ -134,6 +166,9 @@ public enum OperationMode { /** Timer used for various tasks such as idle timeouts or query timeouts */ private final HashedWheelTimer timer; + /** RpcResponder for doing response asynchronously*/ + private final RpcResponder rpcResponder; + /** * Row keys that need to be compacted. * Whenever we write a new data point to a row, we add the row key to this @@ -183,7 +218,13 @@ public enum OperationMode { /** Whether or not to block writing of derived rollups/pre-ags */ private final boolean rollups_block_derived; - + + /** + * Whether or not to enable splitting rollup queries if the rollup table is lagging + * Global config setting: tsd.rollups.split_query.enable = true + */ + private final boolean rollups_split_queries; + /** An optional histogram manger used when the TSD will be dealing with * histograms and sketches. Instantiated ONLY if * {@link #initializePlugins(boolean)} was called.*/ @@ -312,6 +353,7 @@ public TSDB(final HBaseClient client, final Config config) { agg_tag_key = config.getString("tsd.rollups.agg_tag_key"); raw_agg_tag_value = config.getString("tsd.rollups.raw_agg_tag_value"); rollups_block_derived = config.getBoolean("tsd.rollups.block_derived"); + rollups_split_queries = config.getBoolean("tsd.rollups.split_query.enable"); } else { rollup_config = null; default_interval = null; @@ -319,6 +361,7 @@ public TSDB(final HBaseClient client, final Config config) { agg_tag_key = null; raw_agg_tag_value = null; rollups_block_derived = false; + rollups_split_queries = false; } QueryStats.setEnableDuplicates( @@ -343,7 +386,10 @@ public TSDB(final HBaseClient client, final Config config) { // set any extra tags from the config for stats StatsCollector.setGlobalTags(config); - + + + rpcResponder = new RpcResponder(config); + LOG.debug(config.dumpConfiguration()); } @@ -549,7 +595,7 @@ public void initializePlugins(final boolean init_rpcs) { uid_filter.getClass().getCanonicalName() + "] version: " + uid_filter.version()); } - + // finally load the histo manager after plugins have been loaded. if (config.hasProperty("tsd.core.histograms.config")) { histogram_manager = new HistogramCodecManager(this); @@ -566,7 +612,7 @@ public void initializePlugins(final boolean init_rpcs) { public final Authentication getAuth() { return this.authentication; } - + /** * Returns the configured HBase client * @return The HBase client @@ -728,6 +774,145 @@ public Deferred> checkNecessaryTablesExist() { return Deferred.group(checks); } + /* Suffix for queries from availability check below. */ + private static byte[] PROBE_SUFFIX = { + ':', 'A', 's', 'y', 'n', 'c', 'H', 'B', 'a', 's', 'e', + '~', 'p', 'r', 'o', 'b', 'e', '~', '<', ';', '_', '<', + }; + + /** + * Get availability status of regions for a table. + * + * Implemented as separate method so we can override it in unit tests. + * + * @return Per-region availability. + * + * @since 2.5 + */ + Deferred> getTableRegionAvailability(String table) { + final String table_id = config.getString(table); + + /** Convert result to true. */ + final class SuccessToBoolCallback implements Callback> { + @Override + public Boolean call(final ArrayList o) { + LOG.info("Check HBase availability, got success."); + return true; + } + } + + /** Convert error result to false. */ + final class FailureToBoolCallback implements Callback { + @Override + public Boolean call(final Exception e) { + LOG.error("Check HBase availability, got error:", e); + return false; + } + } + + final SuccessToBoolCallback successCB = new SuccessToBoolCallback(); + final FailureToBoolCallback failureCB = new FailureToBoolCallback(); + + /** Lookup availability of each region. */ + final class RegionInfoCallback implements Callback>,List> { + @Override + public Deferred> call(final List regions) { + LOG.info("Availability check got this many regions: " + regions.size()); + ArrayList> available = new ArrayList>(); + for (RegionLocation region : regions) { + // Use suffix so we don't hit real data: + final byte[] key = region.startKey(); + final byte[] testKey = new byte[key.length + 64]; + System.arraycopy(key, 0, testKey, 0, key.length); + System.arraycopy(PROBE_SUFFIX, 0, + testKey, testKey.length - PROBE_SUFFIX.length, + PROBE_SUFFIX.length); + LOG.debug("Checking region with start key " + testKey + " end key " + region.stopKey()); + GetRequest probe = new GetRequest(table_id, testKey); + // If we don't get a response within 1 second, assume the region is + // unavailable. + probe.setTimeout(1000); + probe.setFailfast(true); + available.add(client.get(probe).addCallbacks(successCB, failureCB)); + } + return Deferred.group(available); + } + } + + return client.locateRegions(config.getString(table)) + .addCallbackDeferring(new RegionInfoCallback()); + } + + /** + * Check for full or partial data and UID tables availability in HBase. + * + * @return Status of table availability. + * + * @since 2.5 + */ + public Deferred checkNecessaryTablesAvailability() { + /** Convert list of booleans (indicating a region being available) into full + * (all were available), partial (some were available), none (none were + * available). + */ + final class TableAvailabilityCB implements Callback> { + @Override + public TableAvailability call(final ArrayList available) { + if (available.size() == 0) { + return TableAvailability.NONE; + } + boolean hasAvailable = false; + boolean hasUnavailable = false; + for (Boolean regionAvailable : available) { + if (regionAvailable) { + hasAvailable = true; + } else { + hasUnavailable = true; + } + } + if (hasAvailable && hasUnavailable) { + return TableAvailability.PARTIAL; + } else if (hasAvailable) { + return TableAvailability.FULL; + } else { + return TableAvailability.NONE; + } + } + } + + /** If getting regions fails, availability is NONE. */ + final class FailedRegionInfoCallback implements Callback { + @Override + public TableAvailability call(final Exception e) { + LOG.error("Failed to get regions during table availability check", e); + return TableAvailability.NONE; + } + } + + ArrayList> tables = new ArrayList>(); + tables.add(getTableRegionAvailability("tsd.storage.hbase.uid_table") + .addCallbacks(new TableAvailabilityCB(), new FailedRegionInfoCallback())); + tables.add(getTableRegionAvailability("tsd.storage.hbase.data_table") + .addCallbacks(new TableAvailabilityCB(), new FailedRegionInfoCallback())); + + /** Combine availability for two tables by picking the lower of the two. */ + final class CombineAvailabilityCB implements Callback> { + @Override + public TableAvailability call(final ArrayList availabilities) { + assert availabilities.size() == 2; + TableAvailability result = TableAvailability.FULL; + for (TableAvailability availability: availabilities) { + if (availability.ordinal() < result.ordinal()) { + result = availability; + } + } + return result; + } + } + + return Deferred.group(tables).addCallback(new CombineAvailabilityCB()); + } + /** Number of cache hits during lookups involving UIDs. */ public long uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() @@ -808,6 +993,13 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("class"); } + collector.addExtraTag("class", "IncomingDataPoints"); + try { + collector.record("uid.autometric.rejections", IncomingDataPoints.auto_metric_rejection_count, "method=put"); + } finally { + collector.clearExtraTag("class"); + } + collector.addExtraTag("class", "TSDB"); try { collector.record("datapoints.added", datapoints_added, "type=all"); @@ -906,6 +1098,14 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("plugin"); } } + if (meta_cache != null) { + try { + collector.addExtraTag("plugin", "metaCache"); + meta_cache.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -1006,6 +1206,7 @@ public Deferred addPoint(final String metric, final long value, final Map tags) { final byte[] v; + if (Byte.MIN_VALUE <= value && value <= Byte.MAX_VALUE) { v = new byte[] { (byte) value }; } else if (Short.MIN_VALUE <= value && value <= Short.MAX_VALUE) { @@ -1194,8 +1395,14 @@ public Deferred call(final Boolean allowed) throws Exception { result = client.append(point); } else if (!isHistogram(qualifier)) { scheduleForCompaction(row, (int) base_time); - final PutRequest point = RequestBuilder.buildPutRequest(config, table, row, FAMILY, qualifier, value, timestamp); + boolean isLong = ((flags & Const.FLAG_FLOAT) == 0x0); + if (isLong && config.use_hbase_counters()) { + AtomicIncrementRequest counterRequest = new AtomicIncrementRequest(table, row, FAMILY, qualifier, Bytes.getLong(value)); + result = client.atomicIncrement(counterRequest); + } else { + final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); result = client.put(point); + } } else { scheduleForCompaction(row, (int) base_time); final PutRequest histo_point = new PutRequest(table, row, FAMILY, qualifier, value); @@ -1657,20 +1864,43 @@ public String toString() { } } + final class RpcResponsderShutdown implements Callback { + @Override + public Object call(Object arg) throws Exception { + try { + TSDB.this.rpcResponder.close(); + } catch (Exception e) { + LOG.error( + "Run into unknown exception while closing RpcResponder.", e); + } finally { + return arg; + } + } + } + final class HClientShutdown implements Callback, ArrayList> { - public Deferred call(final ArrayList args) { + public Deferred call(final ArrayList args) { + Callback nextCallback; if (storage_exception_handler != null) { - return client.shutdown().addBoth(new SEHShutdown()); + nextCallback = new SEHShutdown(); + } else { + nextCallback = new FinalShutdown(); } - return client.shutdown().addBoth(new FinalShutdown()); + + if (TSDB.this.rpcResponder.isAsync()) { + client.shutdown().addBoth(new RpcResponsderShutdown()); + } + + return client.shutdown().addBoth(nextCallback); } - public String toString() { + + public String toString() { return "shutdown HBase client"; } } final class ShutdownErrback implements Callback { - public Object call(final Exception e) { + public Object call(final Exception e) { final Logger LOG = LoggerFactory.getLogger(ShutdownErrback.class); if (e instanceof DeferredGroupException) { final DeferredGroupException ge = (DeferredGroupException) e; @@ -1684,13 +1914,14 @@ public Object call(final Exception e) { } return new HClientShutdown().call(null); } - public String toString() { + + public String toString() { return "shutdown HBase client after error"; } } final class CompactCB implements Callback> { - public Object call(ArrayList compactions) throws Exception { + public Object call(ArrayList compactions) throws Exception { return null; } } @@ -2116,7 +2347,18 @@ public String getRawTagValue() { return raw_agg_tag_value; } - /** @return The optional histogram manager registered to this TSD. + /** + * Returns whether the global config setting allows splitting rollups queries + * if the rollups table to be hit is lagging. + * + * @return Whether or not splitting rollup queries is enabled + * @since 2.4 + */ + public boolean isRollupsSplittingEnabled() { + return rollups_split_queries; + } + + /** @return The optional histogram manager registered to this TSD. * @since 2.4 */ public HistogramCodecManager histogramManager() { return histogram_manager; @@ -2189,4 +2431,87 @@ final Deferred delete(final byte[] key, final byte[][] qualifiers) { return client.delete(new DeleteRequest(table, key, FAMILY, qualifiers)); } + /** Do response by RpcResponder */ + public void response(Runnable run) { + rpcResponder.response(run); + } + + + /** + * store a single long value data point in the TSDB as HBase counter. + * @param metric A non-empty string. + * @param timestamp The timestamp associated with the value. + * @param value The value of the data point. + * @param tags The tags on this series. This map must be non-empty. + * @return A deferred object that indicates the completion of the request. + * The {@link Object} has not special meaning and can be {@code null} (think + * of it as {@code Deferred}). But you probably want to attach at + * least an errback to this {@code Deferred} to handle failures. + * @throws IllegalArgumentException if the timestamp is less than or equal + * to the previous timestamp added or 0 for the first timestamp, or if the + * difference with the previous timestamp is too large. + * @throws IllegalArgumentException if the metric name is empty or contains + * illegal characters. + * @throws IllegalArgumentException if the value is NaN or infinite. + * @throws IllegalArgumentException if the tags list is empty or one of the + * elements contains illegal characters. + * @throws HBaseException (deferred) if there was a problem while persisting + * data. + */ + public Deferred addCounter(String metric, long timestamp, long valueAsLong, HashMap tags) { + + final byte[] value = Bytes.fromLong(valueAsLong); + final short flags = (short) (value.length - 1); + + // we only accept positive unix epoch timestamps in seconds or milliseconds + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && + timestamp > 9999999999999L)) { + throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + + " timestamp=" + timestamp + + " when trying to add value=" + Arrays.toString(value) + '/' + flags + + " to metric=" + metric + ", tags=" + tags); + } + IncomingDataPoints.checkMetricAndTags(metric, tags); + final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); + final long base_time; + final byte[] qualifier = Internal.buildQualifier(timestamp, flags); + + if ((timestamp & Const.SECOND_MASK) != 0) { + // drop the ms timestamp to seconds to calculate the base timestamp + base_time = ((timestamp / 1000) - + ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } else { + base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } + + Bytes.setInt(row, (int) base_time, metrics.width()); + scheduleForCompaction(row, (int) base_time); + + AtomicIncrementRequest counterRequest = new AtomicIncrementRequest(table, row, FAMILY, qualifier, Bytes.getLong(value)); + client.atomicIncrement(counterRequest); + + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && + !config.enable_tsuid_tracking() && rt_publisher == null) { + return null; + } + + final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, + Const.TIMESTAMP_BYTES); + + // for busy TSDs we may only enable TSUID tracking, storing a 1 in the + // counter field for a TSUID with the proper timestamp. If the user would + // rather have TSUID incrementing enabled, that will trump the PUT + if (config.enable_tsuid_tracking() && !config.enable_tsuid_incrementing()) { + final PutRequest tracking = new PutRequest(meta_table, tsuid, + TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); + client.put(tracking); + } else if (config.enable_tsuid_incrementing() || config.enable_realtime_ts()) { + TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } + + if (rt_publisher != null) { + rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); + } + return null; + } } diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index e571004d66..9e61ffeedf 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -176,9 +176,9 @@ public void validateAndSetQuery() { } else { end_time = System.currentTimeMillis(); } - if (end_time <= start_time) { + if (end_time < start_time) { throw new IllegalArgumentException( - "End time [" + end_time + "] must be greater than the start time [" + "End time [" + end_time + "] must be greater than or equal to the start time [" + start_time +"]"); } @@ -240,8 +240,15 @@ public Deferred buildQueriesAsync(final TSDB tsdb) { final List> deferreds = new ArrayList>(queries.size()); for (int i = 0; i < queries.size(); i++) { - final Query query = tsdb.newQuery(); - deferreds.add(query.configureFromQuery(this, i)); + Query query = tsdb.newQuery(); + Deferred resolution = query.configureFromQuery(this, i); + + if (query.needsSplitting() && (query instanceof TsdbQuery)) { + query = new SplitRollupQuery(tsdb, (TsdbQuery) query, resolution); + resolution = query.configureFromQuery(this, i); + } + deferreds.add(resolution); + tsdb_queries[i] = query; } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index a0dd841d6a..4c8d60c69f 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -20,6 +20,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedMap; import java.util.TreeMap; import org.slf4j.Logger; @@ -60,7 +61,7 @@ /** * Non-synchronized implementation of {@link Query}. */ -final class TsdbQuery implements Query { +final class TsdbQuery extends AbstractQuery { private static final Logger LOG = LoggerFactory.getLogger(TsdbQuery.class); @@ -97,22 +98,22 @@ final class TsdbQuery implements Query { /** End time (UNIX timestamp in seconds) on 32 bits ("unsigned" int). */ private long end_time = UNSET; - + /** Whether or not to delete the queried data */ private boolean delete; /** ID of the metric being looked up. */ private byte[] metric; - + /** Row key regex to pass to HBase if we have tags or TSUIDs */ private String regex; - + /** Whether or not to enable the fuzzy row filter for Hbase */ private boolean enable_fuzzy_filter; - + /** Whether or not the user wants to use the fuzzy filter */ private boolean override_fuzzy_filter; - + /** * Tags by which we must group the results. * Each element is a tag ID. @@ -131,7 +132,7 @@ final class TsdbQuery implements Query { /** Specifies the various options for rate calculations */ private RateOptions rate_options; - + /** Aggregator function to use. */ private Aggregator aggregator; @@ -140,55 +141,55 @@ final class TsdbQuery implements Query { /** Rollup interval and aggregator, null if not applicable. */ private RollupQuery rollup_query; - + /** Map of RollupInterval objects in the order of next best match * like 1d, 1h, 10m, 1m, for rollup of 1d. */ private List best_match_rollups; - + /** How to use the rollup data */ private ROLLUP_USAGE rollup_usage = ROLLUP_USAGE.ROLLUP_NOFALLBACK; - - /** Search the query on pre-aggregated table directly instead of post fetch + + /** Search the query on pre-aggregated table directly instead of post fetch * aggregation. */ private boolean pre_aggregate; - + /** Optional list of TSUIDs to fetch and aggregate instead of a metric */ private List tsuids; - + /** An index that links this query to the original sub query */ private int query_index; - + /** Tag value filters to apply post scan */ private List filters; - + /** An object for storing stats in regarding the query. May be null */ private QueryStats query_stats; - + /** Whether or not to match series with ONLY the given tags */ private boolean explicit_tags; - + private List percentiles; - + private boolean show_histogram_buckets; - + /** Set at filter resolution time to determine if we can use multi-gets */ private boolean use_multi_gets; /** Set by the user if they want to bypass multi-gets */ private boolean override_multi_get; - + /** Whether or not to use the search plugin for multi-get resolution. */ private boolean multiget_with_search; - + /** Whether or not to fall back on query failure. */ private boolean search_query_failure; - + /** The maximum number of bytes allowed per query. */ private long max_bytes = 0; - + /** The maximum number of data points allowed per query. */ private long max_data_points = 0; - + /** * Enum for rollup fallback control. * @since 2.4 @@ -198,7 +199,7 @@ public static enum ROLLUP_USAGE { ROLLUP_NOFALLBACK, //Use rollup data, and don't fallback on no data ROLLUP_FALLBACK, //Use rollup data and fallback to next best match on data ROLLUP_FALLBACK_RAW; //Use rollup data and fallback to raw on no data - + /** * Parse and transform a string to ROLLUP_USAGE object * @param str String to be parsed @@ -206,7 +207,7 @@ public static enum ROLLUP_USAGE { */ public static ROLLUP_USAGE parse(String str) { ROLLUP_USAGE def = ROLLUP_NOFALLBACK; - + if (str != null) { try { def = ROLLUP_USAGE.valueOf(str.toUpperCase()); @@ -216,10 +217,10 @@ public static ROLLUP_USAGE parse(String str) { + "uses raw data but don't fallback on no data"); } } - + return def; } - + /** * Whether to fallback to next best match or raw * @return true means fall back else false @@ -228,7 +229,7 @@ public boolean fallback() { return this == ROLLUP_FALLBACK || this == ROLLUP_FALLBACK_RAW; } } - + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -248,15 +249,15 @@ public String getRollupTable() { return "raw"; } } - - /** Search the query on pre-aggregated table directly instead of post fetch - * aggregation. - * @since 2.4 + + /** Search the query on pre-aggregated table directly instead of post fetch + * aggregation. + * @since 2.4 */ public boolean isPreAggregate() { return this.pre_aggregate; } - + /** * Sets the start time for the query * @param timestamp Unix epoch timestamp in seconds or milliseconds @@ -265,12 +266,12 @@ public boolean isPreAggregate() { */ @Override public void setStartTime(final long timestamp) { - if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && timestamp > 9999999999999L)) { throw new IllegalArgumentException("Invalid timestamp: " + timestamp); - } else if (end_time != UNSET && timestamp >= getEndTime()) { + } else if (end_time != UNSET && timestamp > getEndTime()) { throw new IllegalArgumentException("new start time (" + timestamp - + ") is greater than or equal to end time: " + getEndTime()); + + ") is greater than end time: " + getEndTime()); } start_time = timestamp; } @@ -299,9 +300,9 @@ public void setEndTime(final long timestamp) { if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && timestamp > 9999999999999L)) { throw new IllegalArgumentException("Invalid timestamp: " + timestamp); - } else if (start_time != UNSET && timestamp <= getStartTime()) { + } else if (start_time != UNSET && timestamp < getStartTime()) { throw new IllegalArgumentException("new end time (" + timestamp - + ") is less than or equal to start time: " + getStartTime()); + + ") is less than start time: " + getStartTime()); } end_time = timestamp; } @@ -428,10 +429,72 @@ public void setTimeSeries(final List tsuids, public void setExplicitTags(final boolean explicit_tags) { this.explicit_tags = explicit_tags; } - + + /** + * Splits this query into one query for the part that is covered by the rollup + * table (as defined in its SLA) and one to get the data for the remaining time + * range from the raw table. + * @param query The original TSQuery as parsed + * @param index The index of the TSQuery + * @param rawQuery A new TsdbQuery instance that will be configured to hit the raw table + * for the correct time range + * @return the deferred analogous to {@link TsdbQuery#configureFromQuery(TSQuery, int)} + * @throws IllegalStateException if the query is not eligible or splitting is disabled + */ + public Deferred split(final TSQuery query, final int index, final TsdbQuery rawQuery) { + if (!needsSplitting()) { + throw new IllegalStateException("Query is not eligible for splitting" + this.toString()); + } + + Deferred rawResolutionDeferred = rawQuery.configureFromQuery(query, index, true); + + long lastRollupTimestampMillis = rollup_query.getLastRollupTimestampSeconds() * 1000L; + + boolean needsRawAndRollupData = QueryUtil.isTimestampAfter(lastRollupTimestampMillis, getStartTime()); + if (needsRawAndRollupData) { + updateRollupSplitTimes(rawQuery, lastRollupTimestampMillis); + } + + return rawResolutionDeferred; + } + + /** + * Updates the timestamp of this query and the corresponding raw part in the case of a split. + * + * Sets the start and end times for this query so that it hits the rollup table until the given timestamp. + * Also updates the passed {@param rawQuery} with the new start time so that it hits the raw table for points from the + * given timestamp onwards. + * + * Makes sure that all timestamps are in milliseconds. + * + * @param rawQuery The raw query part + * @param splitTimestamp The timestamp until when rollup data is guaranteed to be available + */ + private void updateRollupSplitTimes(final TsdbQuery rawQuery, long splitTimestamp) { + setEndTime(splitTimestamp); + + boolean isStartTimeInSeconds = (getStartTime() & Const.SECOND_MASK) == 0; + if (isStartTimeInSeconds) { + setStartTime(getStartTime() * 1000L); + } + + boolean isRawEndTimeInSeconds = (rawQuery.getEndTime() & Const.SECOND_MASK) == 0; + if (isRawEndTimeInSeconds) { + rawQuery.setEndTime(rawQuery.getEndTime() * 1000L); + } + + rawQuery.setStartTime(splitTimestamp); + } + @Override - public Deferred configureFromQuery(final TSQuery query, - final int index) { + public Deferred configureFromQuery(final TSQuery query, + final int index) { + return configureFromQuery(query, index, false); + } + + + public Deferred configureFromQuery(final TSQuery query, + final int index, boolean force_raw) { if (query.getQueries() == null || query.getQueries().isEmpty()) { throw new IllegalArgumentException("Missing sub queries"); } @@ -476,7 +539,7 @@ public Deferred configureFromQuery(final TSQuery query, percentiles = sub_query.getPercentiles(); show_histogram_buckets = sub_query.getShowHistogramBuckets(); - if (rollup_usage != ROLLUP_USAGE.ROLLUP_RAW) { + if (!force_raw && rollup_usage != ROLLUP_USAGE.ROLLUP_RAW) { //Check whether the down sampler is set and rollup is enabled transformDownSamplerToRollupQuery(aggregator, sub_query.getDownsample()); } @@ -575,7 +638,7 @@ private List> resolveTagFilters() { return deferreds; } } - + // fire off the callback chain by resolving the metric first return tsdb.metrics.getIdAsync(sub_query.getMetric()) .addCallbackDeferring(new MetricCB()); @@ -586,7 +649,7 @@ private List> resolveTagFilters() { public void downsample(final long interval, final Aggregator downsampler, final FillPolicy fill_policy) { this.downsampler = new DownsamplingSpecification( - interval, downsampler,fill_policy); + interval, downsampler, fill_policy); } /** @@ -703,43 +766,11 @@ private void findGroupBys() { } } } - /** - * Executes the query. - * NOTE: Do not run the same query multiple times. Construct a new query with - * the same parameters again if needed - * TODO(cl) There are some strange occurrences when unit testing where the end - * time, if not set, can change between calls to run() - * @return An array of data points with one time series per array value - */ - @Override - public DataPoints[] run() throws HBaseException { - try { - return runAsync().joinUninterruptibly(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Should never be here", e); - } - } - - @Override - public DataPoints[] runHistogram() throws HBaseException { - if (!isHistogramQuery()) { - throw new RuntimeException("Should never be here"); - } - - try { - return runHistogramAsync().joinUninterruptibly(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Should never be here", e); - } - } - + @Override public Deferred runAsync() throws HBaseException { Deferred result = null; + if (use_multi_gets && override_multi_get) { result = this.findSpansWithMultiGetter().addCallback(new GroupByAndAggregateCB()); } else { @@ -776,10 +807,45 @@ public boolean isHistogramQuery() { if ((this.percentiles != null && this.percentiles.size() > 0) || show_histogram_buckets) { return true; } - + return false; } - + + @Override + public boolean isRollupQuery() { + return RollupQuery.isValidQuery(rollup_query); + } + + /** + * Returns whether this query needs to be split. It does if + * - splitting of queries is enabled globally AND + * - it can be split (i.e. it's a valid rollups query) AND + * - the table it is hitting has an SLA configured that describes the blackout period AND + * - the query is actually looking at data from the time beyond the SLA + * + * @return whether this query needs to be split. + * @since 2.4 + */ + @Override + public boolean needsSplitting() { + if (!tsdb.isRollupsSplittingEnabled()) { + // Don't split if the global config doesn't allow it + return false; + } + + if (!isRollupQuery()) { + // Don't split if it's hitting the raw table anyway + return false; + } + + if (rollup_query.getRollupInterval().getMaximumLag() <= 0) { + // Don't split if the table doesn't have a maximum lag configured + return false; + } + + return rollup_query.isInBlackoutPeriod(getEndTime()); + } + /** * Finds all the {@link Span}s that match this query. * This is what actually scans the HBase table and loads the data into @@ -791,7 +857,7 @@ public boolean isHistogramQuery() { * perform the search. * @throws IllegalArgumentException if bad data was retrieved from HBase. */ - private Deferred> findSpans() throws HBaseException { + private Deferred> findSpans() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap spans = // The key is a row key from HBase. new TreeMap(new SpanCmp( @@ -831,14 +897,14 @@ private Deferred> findSpans() throws HBaseException { } } - private Deferred> findSpansWithMultiGetter() throws HBaseException { + private Deferred> findSpansWithMultiGetter() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap spans = // The key is a row key from HBase. new TreeMap(new SpanCmp(metric_width)); scan_start_time = System.nanoTime(); - - return new MultiGetQuery(tsdb, this, metric, row_key_literals_list, + + return new MultiGetQuery(tsdb, this, metric, row_key_literals_list, getScanStartTimeSeconds(), getScanEndTimeSeconds(), tableToBeScanned(), spans, null, 0, rollup_query, query_stats, query_index, 0, false, search_query_failure).fetch(); @@ -857,7 +923,7 @@ private Deferred> findSpansWithMultiGetter() throws HBaseE * perform the search. * @throws IllegalArgumentException if bad data was retreived from HBase. */ - private Deferred> findHistogramSpans() throws HBaseException { + private Deferred> findHistogramSpans() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap histSpans = new TreeMap(new SpanCmp(metric_width)); @@ -896,7 +962,7 @@ private Deferred> findHistogramSpans() throws HBa } } - private Deferred> findHistogramSpansWithMultiGetter() throws HBaseException { + private Deferred> findHistogramSpansWithMultiGetter() throws HBaseException { final short metric_width = tsdb.metrics.width(); // The key is a row key from HBase final TreeMap histSpans = new TreeMap(new SpanCmp(metric_width)); @@ -913,7 +979,7 @@ private Deferred> findHistogramSpansWithMultiGett * {@link TsdbQuery#findSpans} to group and sort the results. */ private class GroupByAndAggregateCB implements - Callback>{ + Callback>{ /** * Creates the {@link SpanGroup}s to form the final results of this query. @@ -923,7 +989,7 @@ private class GroupByAndAggregateCB implements * any 'GROUP BY' formulated in this query. */ @Override - public DataPoints[] call(final TreeMap spans) throws Exception { + public DataPoints[] call(final SortedMap spans) throws Exception { if (query_stats != null) { query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, (System.nanoTime() - TsdbQuery.this.scan_start_time)); @@ -953,7 +1019,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - rollup_query); + rollup_query, + null); group.add(span); groups[i++] = group; } @@ -973,7 +1040,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - rollup_query); + rollup_query, + new byte[0]); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); } @@ -1017,6 +1085,11 @@ public DataPoints[] call(final TreeMap spans) throws Exception { //LOG.info("Span belongs to group " + Arrays.toString(group) + ": " + Arrays.toString(row)); SpanGroup thegroup = groups.get(group); if (thegroup == null) { + // Copy the array because we're going to keep `group' and overwrite + // its contents. So we want the collection to have an immutable copy. + final byte[] group_copy = new byte[group.length]; + System.arraycopy(group, 0, group_copy, 0, group.length); + thegroup = new SpanGroup(tsdb, getScanStartTimeSeconds(), getScanEndTimeSeconds(), null, rate, rate_options, aggregator, @@ -1024,11 +1097,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - rollup_query); - // Copy the array because we're going to keep `group' and overwrite - // its contents. So we want the collection to have an immutable copy. - final byte[] group_copy = new byte[group.length]; - System.arraycopy(group, 0, group_copy, 0, group.length); + rollup_query, + group_copy); groups.put(group_copy, thegroup); } thegroup.add(entry.getValue()); @@ -1048,7 +1118,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { * {@link TsdbQuery#findHistogramSpans} to group and sort the results. */ private class HistogramGroupByAndAggregateCB implements - Callback>{ + Callback>{ /** * Creates the {@link HistogramSpanGroup}s to form the final results of this query. @@ -1057,7 +1127,7 @@ private class HistogramGroupByAndAggregateCB implements * @return A possibly empty array of {@link HistogramSpanGroup}s built according to * any 'GROUP BY' formulated in this query. */ - public DataPoints[] call(final TreeMap spans) throws Exception { + public DataPoints[] call(final SortedMap spans) throws Exception { if (query_stats != null) { query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, (System.nanoTime() - TsdbQuery.this.scan_start_time)); @@ -1394,8 +1464,8 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { final Scanner scanner = QueryUtil.getMetricScanner(tsdb, salt_bucket, metric, (int) getScanStartTimeSeconds(), end_time == UNSET ? -1 // Will scan until the end (0xFFF...). - : (int) getScanEndTimeSeconds(), - tableToBeScanned(), + : (int) getScanEndTimeSeconds(), + tableToBeScanned(), TSDB.FAMILY()); if(tsdb.getConfig().use_otsdb_timestamp()) { long stTime = (getScanStartTimeSeconds() * 1000); @@ -1428,7 +1498,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() .getBytes(Const.ASCII_CHARSET)))); rollup_filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(new byte[] { + new BinaryPrefixComparator(new byte[] { (byte) tsdb.getRollupConfig().getIdForAggregator( rollup_query.getRollupAgg().toString()) }))); @@ -1440,7 +1510,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() .getBytes(Const.ASCII_CHARSET)))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(new byte[] { + new BinaryPrefixComparator(new byte[] { (byte) tsdb.getRollupConfig().getIdForAggregator( rollup_query.getRollupAgg().toString()) }))); @@ -1457,10 +1527,10 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { (byte) tsdb.getRollupConfig().getIdForAggregator("sum") }))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(new byte[] { + new BinaryPrefixComparator(new byte[] { (byte) tsdb.getRollupConfig().getIdForAggregator("count") }))); - + if (existing != null) { final List combined = new ArrayList(2); combined.add(existing); @@ -1475,14 +1545,14 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { } /** - * Identify the table to be scanned based on the roll up and pre-aggregate + * Identify the table to be scanned based on the roll up and pre-aggregate * query parameters * @return table name as byte array * @since 2.4 */ private byte[] tableToBeScanned() { final byte[] tableName; - + if (RollupQuery.isValidQuery(rollup_query)) { if (pre_aggregate) { tableName= rollup_query.getRollupInterval().getGroupbyTable(); @@ -1497,12 +1567,12 @@ else if (pre_aggregate) { else { tableName = tsdb.dataTable(); } - + return tableName; } - + /** Returns the UNIX timestamp from which we must start scanning. */ - private long getScanStartTimeSeconds() { + long getScanStartTimeSeconds() { // Begin with the raw query start time. long start = getStartTime(); @@ -1510,15 +1580,15 @@ private long getScanStartTimeSeconds() { if ((start & Const.SECOND_MASK) != 0L) { start /= 1000L; } - + // if we have a rollup query, we have different row key start times so find // the base time from which we need to search if (rollup_query != null) { - long base_time = RollupUtils.getRollupBasetime(start, + long base_time = RollupUtils.getRollupBasetime(start, rollup_query.getRollupInterval()); if (rate) { // scan one row back so we can get the first rate value. - base_time = RollupUtils.getRollupBasetime(base_time - 1, + base_time = RollupUtils.getRollupBasetime(base_time - 1, rollup_query.getRollupInterval()); } return base_time; @@ -1544,24 +1614,25 @@ private long getScanStartTimeSeconds() { } /** Returns the UNIX timestamp at which we must stop scanning. */ - private long getScanEndTimeSeconds() { + @VisibleForTesting + protected long getScanEndTimeSeconds() { // Begin with the raw query end time. long end = getEndTime(); // Convert to seconds if we have a query in ms. if ((end & Const.SECOND_MASK) != 0L) { end /= 1000L; - if (end - (end * 1000) < 1) { + if (end == 0) { // handle an edge case where a user may request a ms time between // 0 and 1 seconds. Just bump it a second. end++; } } - + if (rollup_query != null) { - return RollupUtils.getRollupBasetime(end + - (rollup_query.getRollupInterval().getIntervalSeconds() * - rollup_query.getRollupInterval().getIntervals()), + return RollupUtils.getRollupBasetime(end + + (rollup_query.getRollupInterval().getIntervalSeconds() * + rollup_query.getRollupInterval().getIntervals()), rollup_query.getRollupInterval()); } @@ -1677,9 +1748,6 @@ public void transformDownSamplerToRollupQuery(final Aggregator group_by, rollup_query = new RollupQuery(best_match_rollups.remove(0), downsampler.getFunction(), downsampler.getInterval(), group_by); - if (group_by == Aggregators.COUNT) { - aggregator = Aggregators.SUM; - } } catch (NoSuchRollupForIntervalException nre) { LOG.error("There is no such rollup for the downsample interval " @@ -1825,6 +1893,14 @@ public int compare(final byte[] a, final byte[] b) { } + RateOptions getRateOptions() { return rate_options; } + boolean isRate() { return rate; } + Aggregator getAggregator() {return aggregator; } + DownsamplingSpecification getDownsampler() { return downsampler; } + RollupQuery getRollupQuery() { return rollup_query; } + int getQueryIndex() { return query_index; } + + /** Helps unit tests inspect private methods. */ @VisibleForTesting static class ForTesting { diff --git a/src/create_table.sh b/src/create_table.sh index 1cbe666319..917a4ff3de 100755 --- a/src/create_table.sh +++ b/src/create_table.sh @@ -23,7 +23,7 @@ COMPRESSION=`echo "$COMPRESSION" | tr a-z A-Z` # This can save a lot of storage space. DATA_BLOCK_ENCODING=${DATA_BLOCK_ENCODING-'DIFF'} DATA_BLOCK_ENCODING=`echo "$DATA_BLOCK_ENCODING" | tr a-z A-Z` -TSDB_TTL=${TSDB_TTL-'FOREVER'} +TSDB_TTL=${TSDB_TTL-'2147483647'} case $COMPRESSION in (NONE|LZO|GZIP|SNAPPY) :;; # Known good. diff --git a/src/logback.xml b/src/logback.xml index ff97a50889..49eff6d58e 100644 --- a/src/logback.xml +++ b/src/logback.xml @@ -63,6 +63,9 @@ + + + diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 203616bd22..bf19f2e46b 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -26,13 +27,18 @@ import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; -import org.hbase.async.FilterList; import org.hbase.async.FuzzyRowFilter; +import org.hbase.async.FuzzyRowFilter.FuzzyFilterPair; import org.hbase.async.KeyRegexpFilter; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.FilterList.Operator; +import org.hbase.async.FilterList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.hbase.async.ScanFilter; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + import org.hbase.async.Scanner; /** @@ -175,9 +181,223 @@ public static String getRowKeyUIDRegex( return buf.toString(); } + /** + * Crafts a regular expression for scanning over data table rows and filtering + * time series that the user doesn't want. + * @param row_key_literals An optional list of key value pairs to filter on. + * May be null. + * @param explicit_tags Whether or not explicit tags are enabled so that the + * regex only picks out series with the specified tags + * @return A regular expression string to pass to the storage layer. + */ + private static String getRowKeyUIDRegex( + final ByteMap row_key_literals, + final boolean explicit_tags) { + final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES; + final short name_width = TSDB.tagk_width(); + final short value_width = TSDB.tagv_width(); + final short tagsize = (short) (name_width + value_width); + // Generate a regexp for our tags. Say we have 2 tags: { 0 0 1 0 0 2 } + // and { 4 5 6 9 8 7 }, the regexp will be: + // "^.{7}(?:.{6})*\\Q\000\000\001\000\000\002\\E(?:.{6})*\\Q\004\005\006\011\010\007\\E(?:.{6})*$" + final StringBuilder buf = new StringBuilder( + 15 // "^.{N}" + "(?:.{M})*" + "$" + + ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E" + * ((row_key_literals == null ? 0 : row_key_literals.size())))); + + // Alright, let's build this regexp. From the beginning... + buf.append("(?s)" // Ensure we use the DOTALL flag. + + "^.{") + // ... start by skipping the salt, metric ID and timestamp. + .append(prefix_width) + .append("}"); + + final Iterator> it = row_key_literals == null ? + new ByteMap().iterator() : row_key_literals.iterator(); + + while(it.hasNext()) { + Entry entry = it.hasNext() ? it.next() : null; + // TODO - This look ahead may be expensive. We need to get some data around + // whether it's faster for HBase to scan with a look ahead or simply pass + // the rows back to the TSD for filtering. + final boolean not_key = + entry.getValue() != null && entry.getValue().length == 0; + + // Skip any number of tags. + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } + + if (not_key) { + // start the lookahead as we have a key we explicitly do not want in the + // results + buf.append("(?!"); + } + buf.append("\\Q"); + + addId(buf, entry.getKey(), true); + if (entry.getValue() != null && entry.getValue().length > 0) { // Add a group_by. + // We want specific IDs. List them: /(AAA|BBB|CCC|..)/ + buf.append("(?:"); + for (final byte[] value_id : entry.getValue()) { + if (value_id == null) { + continue; + } + buf.append("\\Q"); + addId(buf, value_id, true); + buf.append('|'); + } + // Replace the pipe of the last iteration. + buf.setCharAt(buf.length() - 1, ')'); + } else { + buf.append(".{").append(value_width).append('}'); // Any value ID. + } + + if (not_key) { + // be sure to close off the look ahead + buf.append(")"); + } + } + + // Skip any number of tags before the end. + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } + buf.append("$"); + return buf.toString(); + } + + /** + * Crafts a list of FuzzyFilters for scanning over data table rows and + * filtering time series that the user doesn't want. + * Note: The caller has to restrict the scan to proper start and stop + * for the filter to work correctly. + * @param row_key_literals A list of key value pairs to filter on. + * @param fuzzy_key The starting row key we'll adjust for proper filtering. + * @return A sorted, non-empty list of FuzzyFilterPair + */ + private static List buildFuzzyFilters( + final ByteMap row_key_literals, + final byte[] fuzzy_key) { + final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES; + final short name_width = TSDB.tagk_width(); + final short value_width = TSDB.tagv_width(); + final short tag_width = (short) (name_width + value_width); + int row_key_size = prefix_width; + if (row_key_literals != null) { + for(byte[][] v: row_key_literals.values()) { + final boolean not_key = v!=null && v.length==0; + if (!not_key) { + row_key_size += tag_width; + } + } + } + final List fuzzy_filter_pairs = + new ArrayList(row_key_literals.size()); + + // Initialize first_fuzzy_key and first_fuzzy_mask + // these will serve as model for the fuzzy filter list + // generated for tags with multiple values (|) + byte[] first_fuzzy_key = Arrays.copyOf(fuzzy_key, fuzzy_key.length); + byte[] first_fuzzy_mask = new byte[fuzzy_key.length]; + int fuzzy_offset = 0; + + // TODO - see if it's less expensive to skip the salt, timestamp and metric. + // skip salt & timestamp (filtering should be done by start/stop + // of the scanner) + while(fuzzy_offset < prefix_width) { + first_fuzzy_key[fuzzy_offset] = 0; + first_fuzzy_mask[fuzzy_offset++] = + (row_key_literals != null) ? (byte)1 : (byte)0; + } + + // first pass to build the key and mask + Iterator> it = row_key_literals.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + final boolean not_key = + entry.getValue() != null && entry.getValue().length == 0; + + if (!not_key) { + final byte[] tag_key = entry.getKey(); + System.arraycopy(tag_key, 0, + first_fuzzy_key, fuzzy_offset, name_width); + for (int i=0; i 0) { + tag_value = entry.getValue()[0]; + } else { + tag_value = null; + } + + if (tag_value!=null) { + System.arraycopy(tag_value, 0, + first_fuzzy_key, fuzzy_offset, value_width); + for (int i=0; i skip + for (int i=0; i entry = it.next(); + fuzzy_offset += name_width; + + // if multiple values value, generate a new combination of filters + // for each value + if (entry.getValue()!=null && entry.getValue().length > 1) { + for (int i=1; i { + @Override + public int compare(FuzzyFilterPair pair1, FuzzyFilterPair pair2) { + return Bytes.memcmp(pair2.getRowKey(), pair1.getRowKey()); + } + } + private static FuzzyFilterComparator FUZZY_FILTER_CMP = new FuzzyFilterComparator(); + /** * Sets a filter or filter list on the scanner based on whether or not the * query had tags it needed to match. + * NOTE: This method will sort the group bys. * @param scanner The scanner to modify. * @param group_bys An optional list of tag keys that we want to group on. May * be null. @@ -205,56 +425,72 @@ public static void setDataTableScanFilter( return; } + if (group_bys != null) { + Collections.sort(group_bys, Bytes.MEMCMP); + } + final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; - final short name_width = TSDB.tagk_width(); - final short value_width = TSDB.tagv_width(); - final byte[] fuzzy_key; - final byte[] fuzzy_mask; - if (explicit_tags && enable_fuzzy_filter) { - fuzzy_key = new byte[prefix_width + (row_key_literals.size() * - (name_width + value_width))]; - fuzzy_mask = new byte[prefix_width + (row_key_literals.size() * - (name_width + value_width))]; + + final FuzzyRowFilter fuzzy_filter; + if (explicit_tags && + enable_fuzzy_filter && + row_key_literals != null && + !row_key_literals.isEmpty()) { + + final byte[] fuzzy_key = new byte[prefix_width + (row_key_literals.size() * + (TSDB.tagk_width() + TSDB.tagv_width()))]; System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, scanner.getCurrentKey().length); + + final List fuzzy_filter_pairs = + buildFuzzyFilters(row_key_literals, fuzzy_key); + + // The Fuzzy Filter list is sorted: the first and last filters row key + // can be used to build the stop key for the scanner + final byte[] stop_key = Arrays.copyOf( + fuzzy_filter_pairs.get(fuzzy_filter_pairs.size() - 1).getRowKey(), + fuzzy_key.length); + System.arraycopy(scanner.getCurrentKey(), 0, stop_key, 0, prefix_width); + Internal.setBaseTime(stop_key, end_time); + int idx = prefix_width + TSDB.tagk_width(); + // max out the tag values + while (idx < stop_key.length) { + for (int i = 0; i < TSDB.tagv_width(); i++) { + stop_key[idx++] = (byte) 0xFF; + } + idx += TSDB.tagk_width(); + } + + scanner.setStartKey(fuzzy_key); + scanner.setStopKey(stop_key); + fuzzy_filter = new FuzzyRowFilter(fuzzy_filter_pairs); } else { - fuzzy_key = fuzzy_mask = null; + fuzzy_filter = null; } - final String regex = getRowKeyUIDRegex(group_bys, row_key_literals, - explicit_tags, fuzzy_key, fuzzy_mask); - final KeyRegexpFilter regex_filter = new KeyRegexpFilter( - regex.toString(), Const.ASCII_CHARSET); - if (LOG.isDebugEnabled()) { - LOG.debug("Regex for scanner: " + scanner + ": " + - byteRegexToString(regex)); + final String regex = getRowKeyUIDRegex(row_key_literals, explicit_tags); + final KeyRegexpFilter regex_filter; + if (!Strings.isNullOrEmpty(regex)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Regex for scanner: " + scanner + ": " + + byteRegexToString(regex)); + } + regex_filter = new KeyRegexpFilter(regex.toString(), + Const.ASCII_CHARSET); + } else { + regex_filter = null; } - if (!(explicit_tags && enable_fuzzy_filter)) { + if (fuzzy_filter != null && !Strings.isNullOrEmpty(regex)) { + final FilterList filter = new FilterList(Lists.newArrayList(fuzzy_filter, + regex_filter),Operator.MUST_PASS_ALL); + scanner.setFilter(filter); + } else if (fuzzy_filter != null) { + scanner.setFilter(fuzzy_filter); + } else if (!Strings.isNullOrEmpty(regex)) { scanner.setFilter(regex_filter); - return; } - - scanner.setStartKey(fuzzy_key); - final byte[] stop_key = Arrays.copyOf(fuzzy_key, fuzzy_key.length); - Internal.setBaseTime(stop_key, end_time); - int idx = Const.SALT_WIDTH() + TSDB.metrics_width() + - Const.TIMESTAMP_BYTES + TSDB.tagk_width(); - // max out the tag values - while (idx < stop_key.length) { - for (int i = 0; i < TSDB.tagv_width(); i++) { - stop_key[idx++] = (byte) 0xFF; - } - idx += TSDB.tagk_width(); - } - scanner.setStopKey(stop_key); - final List filters = new ArrayList(2); - filters.add( - new FuzzyRowFilter( - new FuzzyRowFilter.FuzzyFilterPair(fuzzy_key, fuzzy_mask))); - filters.add(regex_filter); - scanner.setFilter(new FilterList(filters)); } /** @@ -403,4 +639,24 @@ public static String byteRegexToString(final String regexp) { } return buf.toString(); } + + /** + * Compares two timestamps where either can be in seconds or milliseconds. + * + * @param ts1 The first timestamp in either seconds or milliseconds. + * @param ts2 The second timestamp in either seconds or milliseconds. + * @return Whether the first timestamp is after the second + */ + public static boolean isTimestampAfter(long ts1, long ts2) { + boolean ts1InSeconds = (ts1 & Const.SECOND_MASK) == 0; + boolean ts2InSeconds = (ts2 & Const.SECOND_MASK) == 0; + + if (ts1InSeconds && !ts2InSeconds) { + ts1 *= 1000L; + } else if (!ts1InSeconds && ts2InSeconds) { + ts2 *= 1000L; + } + + return ts1 > ts2; + } } diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index e0fbdd44e8..43358e6eb6 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -37,6 +37,7 @@ public final class ExpressionFactory { available_functions.put("highestMax", new HighestMax()); available_functions.put("shift", new TimeShift()); available_functions.put("timeShift", new TimeShift()); + available_functions.put("firstDiff", new FirstDifference()); } /** Don't instantiate me! */ diff --git a/src/query/expression/FirstDifference.java b/src/query/expression/FirstDifference.java new file mode 100644 index 0000000000..6dc75bc088 --- /dev/null +++ b/src/query/expression/FirstDifference.java @@ -0,0 +1,101 @@ +// 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.query.expression; + +import java.util.ArrayList; + +import java.util.List; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.Aggregators.Interpolation; + +/** + * Implements a difference function, calculates the first difference of a given series + * + * @since 2.3 + */ +public class FirstDifference implements net.opentsdb.query.expression.Expression { + + @Override + public DataPoints[] evaluate(final TSQuery data_query, + final List query_results, final List params) { + if (data_query == null) { + throw new IllegalArgumentException("Missing time series query"); + } + if (query_results == null || query_results.isEmpty()) { + return new DataPoints[]{}; + } + + + int num_results = 0; + for (final DataPoints[] results : query_results) { + num_results += results.length; + } + final DataPoints[] results = new DataPoints[num_results]; + + int ix = 0; + // one or more sub queries (m=...&m=...&m=...) + for (final DataPoints[] sub_query_result : query_results) { + // group bys (m=sum:foo{host=*}) + for (final DataPoints dps : sub_query_result) { + results[ix++] = firstDiff(dps); + } + } + + return results; + + } + + /** + * return the first difference of datapoints + * + * @param points The data points to do difference + * @return The resulting data points + */ + private DataPoints firstDiff(final DataPoints points) { + final List dps = new ArrayList(); + final SeekableView view = points.iterator(); + List nums = new ArrayList(); + List times = new ArrayList(); + while (view.hasNext()) { + DataPoint pt = view.next(); + nums.add(pt.toDouble()); + times.add(pt.timestamp()); + } + List diff = new ArrayList(); + diff.add(0.0); + for (int j =0;j query_params, + final String inner_expression) { + return "firstDiff(" + inner_expression + ")"; + } + +} \ No newline at end of file diff --git a/src/rollup/RollupInterval.java b/src/rollup/RollupInterval.java index 3dea39542a..0ccedce6be 100644 --- a/src/rollup/RollupInterval.java +++ b/src/rollup/RollupInterval.java @@ -82,6 +82,16 @@ public class RollupInterval { * also it might be compacted. */ private final boolean is_default_interval; + + /** + * The delay SLA for this rollup interval. If a query is asking for data from a + * recent enough time interval that might not be available (or partially unavailable) + * in the table, the data points will be read from the raw table. + */ + private final String delay_sla; + + /** The delay SLA in seconds */ + private int max_delay_seconds; /** * Protected ctor used by the builder. @@ -93,6 +103,7 @@ protected RollupInterval(final Builder builder) { string_interval = builder.interval; row_span = builder.rowSpan; is_default_interval = builder.defaultInterval; + delay_sla = builder.delaySla != null ? builder.delaySla : ""; final String parsed_units = DateTime.getDurationUnits(row_span); if (parsed_units.length() > 1) { @@ -116,7 +127,8 @@ public String toString() { .append(", unit_multipier=").append(unit_multiplier) .append(", intervals=").append(intervals) .append(", interval=").append(interval) - .append(", interval_units=").append(interval_units); + .append(", interval_units=").append(interval_units) + .append(", delay_sla=").append(delay_sla); return buf.toString(); } @@ -133,6 +145,7 @@ public HashCode buildHashCode() { .putString(string_interval, Const.UTF8_CHARSET) .putString(row_span, Const.UTF8_CHARSET) .putBoolean(is_default_interval) + .putString(delay_sla, Const.UTF8_CHARSET) .hash(); } @@ -152,7 +165,8 @@ public boolean equals(final Object obj) { && Objects.equal(groupby_table_name, interval.groupby_table_name) && Objects.equal(row_span, interval.row_span) && Objects.equal(string_interval, interval.string_interval) - && Objects.equal(is_default_interval, interval.is_default_interval); + && Objects.equal(is_default_interval, interval.is_default_interval) + && Objects.equal(delay_sla, interval.delay_sla); } /** @@ -185,15 +199,20 @@ void validateAndCompile() { } interval = (int) (DateTime.parseDuration(string_interval) / 1000); - if (interval < 1) { - throw new IllegalArgumentException("Millisecond intervals are not supported"); - } + if (interval >= Integer.MAX_VALUE) { throw new IllegalArgumentException("Interval is too big: " + interval); } // The line above will validate for us interval_units = string_interval.charAt(string_interval.length() - 1); + if (delay_sla != null && !delay_sla.isEmpty()) { + max_delay_seconds = (int) (DateTime.parseDuration(delay_sla) / 1000); + if (max_delay_seconds < 1) { + throw new IllegalArgumentException("Milliseconds are not supported as the maximum delay"); + } + } + int num_span = 0; switch (units) { case 'h': @@ -303,6 +322,15 @@ public boolean isDefaultInterval() { public String getRowSpan() { return row_span; } + + /** + * Rollup tables can have an SLA configured specifying by how much time the + * data in the table can be delayed. + * @return the maximum delay in seconds for a table as configured. + */ + public int getMaximumLag() { + return max_delay_seconds; + } public static Builder builder() { return new Builder(); @@ -321,6 +349,8 @@ public static class Builder { private String rowSpan; @JsonProperty private boolean defaultInterval; + @JsonProperty + private String delaySla; public Builder setTable(final String table) { this.table = table; @@ -346,6 +376,11 @@ public Builder setDefaultInterval(final boolean defaultInterval) { this.defaultInterval = defaultInterval; return this; } + + public Builder setDelaySla(final String delaySla) { + this.delaySla = delaySla; + return this; + } public RollupInterval build() { return new RollupInterval(this); diff --git a/src/rollup/RollupQuery.java b/src/rollup/RollupQuery.java index 648d5b33ff..46d37cdce2 100644 --- a/src/rollup/RollupQuery.java +++ b/src/rollup/RollupQuery.java @@ -17,6 +17,7 @@ import net.opentsdb.core.Aggregator; import net.opentsdb.core.Aggregators; +import net.opentsdb.utils.DateTime; /** * Holds information about a rollup interval and rollup aggregator. @@ -185,4 +186,26 @@ public long getSampleIntervalInMS() { public boolean isLowerSamplingRate() { return this.rollup_interval.getIntervalSeconds() * 1000 < sample_interval_ms; } + + /** + * Looks at the SLA configured for the table to be queried and determines the + * timestamp of the latest data point that is guaranteed to be covered by the + * table. + * @return last timestamp in seconds of the period guaranteed to be covered + */ + public int getLastRollupTimestampSeconds() { + return (int) (DateTime.currentTimeMillis()/1000 - getRollupInterval().getMaximumLag()); + } + + /** + * Checks whether the passed timestamp is in the blackout period (between + * the latest guaranteed timestamp and now) + * @param timestampMillis The timestamp to check in milliseconds + * @return whether the timestamp is in the blackout period + */ + public boolean isInBlackoutPeriod(long timestampMillis) { + long latestRollupPointTimestamp = DateTime.currentTimeMillis() - getRollupInterval().getMaximumLag()*1000L; + + return timestampMillis > latestRollupPointTimestamp; + } } diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index 82d242f383..4ff0903d33 100644 --- a/src/rollup/RollupSeq.java +++ b/src/rollup/RollupSeq.java @@ -96,8 +96,8 @@ public RollupSeq(final TSDB tsdb, final RollupQuery rollup_query) { this.rollup_query = rollup_query; // TODO - others - need_count = rollup_query.getGroupBy() == Aggregators.AVG || - rollup_query.getGroupBy() == Aggregators.DEV; + need_count = rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV; // WARNING overallocation qualifiers = new byte[rollup_query.getRollupInterval().getIntervals() * 2]; diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index fa39c17bb1..c470abbd03 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -177,7 +177,7 @@ public enum QueryStat { AVG_UID_TO_STRING ("avgUidToStringTime", true), MAX_COMPACTION_TIME ("maxCompactionTime", true), AVG_COMPACTION_TIME ("avgCompactionTime", true), - MAX_SCANNER_UID_TO_STRING_TIME ("maxScannerUidtoStringTime", true), + MAX_SCANNER_UID_TO_STRING_TIME ("maxScannerUidToStringTime", true), AVG_SCANNER_UID_TO_STRING_TIME ("avgScannerUidToStringTime", true), MAX_SCANNER_MERGE_TIME ("maxSaltScannerMergeTime", true), AVG_SCANNER_MERGE_TIME ("avgSaltScannerMergeTime", true), @@ -410,7 +410,7 @@ public static Map getRunningAndCompleteStats() { obj.put("query", stats.query); obj.put("remote", stats.remote_address); obj.put("user", stats.user); - obj.put("headers", stats.headers);; + obj.put("headers", stats.headers); obj.put("queryStart", stats.query_start_ms); obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(), stats.query_start_ns)); diff --git a/src/tools/FsckOptions.java b/src/tools/FsckOptions.java index 9112b45564..9fc005e71e 100644 --- a/src/tools/FsckOptions.java +++ b/src/tools/FsckOptions.java @@ -96,6 +96,7 @@ public static void addDataOptions(final ArgP argp) { "Delete compacted columns that cannot be parsed."); argp.addOption("--threads", "NUMBER", "Number of threads to use when executing a full table scan."); + argp.addOption("--sync", "Wait for each fix operation to finish to continue."); } /** @return Whether or not to fix errors while processing. Does not affect diff --git a/src/tsd/AnnotationRpc.java b/src/tsd/AnnotationRpc.java index 2ac6c23903..c6dd803092 100644 --- a/src/tsd/AnnotationRpc.java +++ b/src/tsd/AnnotationRpc.java @@ -47,7 +47,9 @@ final class AnnotationRpc implements HttpRpc { */ public void execute(final TSDB tsdb, HttpQuery query) throws IOException { final HttpMethod method = query.getAPIMethod(); - + + RpcUtil.allowedMethods(method, HttpMethod.GET.getName(), HttpMethod.POST.getName(), HttpMethod.DELETE.getName(), HttpMethod.PUT.getName()); + final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; if (endpoint != null && endpoint.toLowerCase().endsWith("bulk")) { @@ -125,11 +127,6 @@ public Deferred call(Boolean success) throws Exception { throw new RuntimeException(e); } query.sendStatusOnly(HttpResponseStatus.NO_CONTENT); - - } else { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + method.getName() + - "] is not permitted for this endpoint"); } } @@ -141,14 +138,12 @@ public Deferred call(Boolean success) throws Exception { * @param query The query to parse and respond to */ void executeBulk(final TSDB tsdb, final HttpMethod method, HttpQuery query) { + RpcUtil.allowedMethods(query.method(), HttpMethod.PUT.getName(), HttpMethod.POST.getName(), HttpMethod.DELETE.getName()); + if (method == HttpMethod.POST || method == HttpMethod.PUT) { executeBulkUpdate(tsdb, method, query); } else if (method == HttpMethod.DELETE) { executeBulkDelete(tsdb, query); - } else { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); } } diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 229cff97af..0fba2d25f8 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -35,6 +35,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; +import org.jboss.netty.handler.codec.http.HttpMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,6 +108,10 @@ public GraphHandler() { } public void execute(final TSDB tsdb, final HttpQuery query) { + + // only accept GET/POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + if (!query.hasQueryStringParam("json") && !query.hasQueryStringParam("png") && !query.hasQueryStringParam("ascii")) { diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index 2a7d81fa44..ec737df0f8 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -419,6 +419,10 @@ public void badRequest(final BadRequestException exception) { HttpQuery.escapeJson(exception.getMessage(), buf); buf.append("\"}"); sendReply(HttpResponseStatus.BAD_REQUEST, buf); + } else if (hasQueryStringParam("png")) { + final StringBuilder buf = new StringBuilder(10 + + exception.getDetails().length()); + sendReply(HttpResponseStatus.BAD_REQUEST, buf); } else { sendReply(HttpResponseStatus.BAD_REQUEST, makePage("Bad Request", "Looks like it's your fault this time", diff --git a/src/tsd/HttpRpc.java b/src/tsd/HttpRpc.java index 40dec97cc4..73b3fe783c 100644 --- a/src/tsd/HttpRpc.java +++ b/src/tsd/HttpRpc.java @@ -26,6 +26,6 @@ interface HttpRpc { * @param tsdb The TSDB to use. * @param query The HTTP query to execute. */ - void execute(TSDB tsdb, HttpQuery query) throws IOException; + void execute(TSDB tsdb, HttpQuery query) throws BadRequestException, IOException; } diff --git a/src/tsd/LogsRpc.java b/src/tsd/LogsRpc.java index 7aa91259a7..da8afa8cd1 100644 --- a/src/tsd/LogsRpc.java +++ b/src/tsd/LogsRpc.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import org.jboss.netty.handler.codec.http.HttpMethod; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonGenerationException; @@ -35,8 +36,13 @@ final class LogsRpc implements HttpRpc { public void execute(final TSDB tsdb, final HttpQuery query) - throws JsonGenerationException, IOException { + throws BadRequestException, IOException { + + // only accept GET/POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + LogIterator logmsgs = new LogIterator(); + if (query.hasQueryStringParam("json")) { ArrayList logs = new ArrayList(); for (String log : logmsgs) { diff --git a/src/tsd/PutDataPointCounterRpc.java b/src/tsd/PutDataPointCounterRpc.java new file mode 100644 index 0000000000..86096350e6 --- /dev/null +++ b/src/tsd/PutDataPointCounterRpc.java @@ -0,0 +1,282 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2012 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.tsd; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.uid.NoSuchUniqueName; + +public class PutDataPointCounterRpc implements TelnetRpc, HttpRpc { + + private static final Logger LOG = LoggerFactory.getLogger(PutDataPointCounterRpc.class); + private static final AtomicLong requests = new AtomicLong(); + private static final AtomicLong hbase_errors = new AtomicLong(); + private static final AtomicLong invalid_values = new AtomicLong(); + private static final AtomicLong illegal_arguments = new AtomicLong(); + private static final AtomicLong unknown_metrics = new AtomicLong(); + + /** + * Collects the stats and metrics tracked by this instance. + * + * @param collector The collector to use. + */ + public static void collectStats(final StatsCollector collector) { + collector.record("rpc.received", requests, "type=put"); + collector.record("rpc.errors", hbase_errors, "type=hbase_errors"); + collector.record("rpc.errors", invalid_values, "type=invalid_values"); + collector.record("rpc.errors", illegal_arguments, "type=illegal_arguments"); + collector.record("rpc.errors", unknown_metrics, "type=unknown_metrics"); + } + + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + requests.incrementAndGet(); + String errmsg = null; + try { + final class PutErrback implements Callback { + public Exception call(final Exception arg) { + if (chan.isConnected()) { + chan.write("putctr: HBase error: " + arg.getMessage() + '\n'); + } + hbase_errors.incrementAndGet(); + return arg; + } + + public String toString() { + return "report error to channel"; + } + } + return importDataPoint(tsdb, cmd).addErrback(new PutErrback()); + } catch (NumberFormatException x) { + errmsg = "putctr: invalid value: " + x.getMessage() + '\n'; + invalid_values.incrementAndGet(); + } catch (IllegalArgumentException x) { + errmsg = "putctr: illegal argument: " + x.getMessage() + '\n'; + illegal_arguments.incrementAndGet(); + } catch (NoSuchUniqueName x) { + errmsg = "putctr: unknown metric: " + x.getMessage() + '\n'; + unknown_metrics.incrementAndGet(); + } + if (errmsg != null) { + LOG.debug(errmsg); + if (chan.isConnected()) { + chan.write(errmsg); + } + } + return Deferred.fromResult(null); + } + + /** + * Handles HTTP RPC put requests + * + * @param tsdb The TSDB to which we belong + * @param query The HTTP query from the user + * @throws IOException if there is an error parsing the query or formatting + * the output + * @throws BadRequestException if the user supplied bad data + * @since 2.0 + */ + public void execute(final TSDB tsdb, final HttpQuery query) + throws IOException { + requests.incrementAndGet(); + + // only accept POST + if (query.method() != HttpMethod.POST) { + throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, + "Method not allowed", "The HTTP method [" + query.method().getName() + + "] is not permitted for this endpoint"); + } + + final List dps = query.serializer().parsePutV1(); + if (dps.size() < 1) { + throw new BadRequestException("No datapoints found in content"); + } + + final boolean show_details = query.hasQueryStringParam("details"); + final boolean show_summary = query.hasQueryStringParam("summary"); + final ArrayList> details = show_details + ? new ArrayList>() : null; + long success = 0; + long total = 0; + + for (IncomingDataPoint dp : dps) { + total++; + try { + if (dp.getMetric() == null || dp.getMetric().isEmpty()) { + if (show_details) { + details.add(this.getHttpDetails("Metric name was empty", dp)); + } + LOG.warn("Metric name was empty: " + dp); + continue; + } + if (dp.getTimestamp() <= 0) { + if (show_details) { + details.add(this.getHttpDetails("Invalid timestamp", dp)); + } + LOG.warn("Invalid timestamp: " + dp); + continue; + } + if (dp.getValue() == null || dp.getValue().isEmpty()) { + if (show_details) { + details.add(this.getHttpDetails("Empty value", dp)); + } + LOG.warn("Empty value: " + dp); + continue; + } + if (dp.getTags() == null || dp.getTags().size() < 1) { + if (show_details) { + details.add(this.getHttpDetails("Missing tags", dp)); + } + LOG.warn("Missing tags: " + dp); + continue; + } + if (Tags.looksLikeInteger(dp.getValue())) { + tsdb.addCounter(dp.getMetric(), dp.getTimestamp(), + Tags.parseLong(dp.getValue()), dp.getTags()); + } else { + Float valueAsFloat = Float.parseFloat(dp.getValue()); + tsdb.addCounter(dp.getMetric(), dp.getTimestamp(), + valueAsFloat.longValue(), dp.getTags()); + } + success++; + } catch (NumberFormatException x) { + if (show_details) { + details.add(this.getHttpDetails("Unable to parse value to a number", + dp)); + } + LOG.warn("Unable to parse value to a number: " + dp); + invalid_values.incrementAndGet(); + } catch (IllegalArgumentException iae) { + if (show_details) { + details.add(this.getHttpDetails(iae.getMessage(), dp)); + } + LOG.warn(iae.getMessage() + ": " + dp); + illegal_arguments.incrementAndGet(); + } catch (NoSuchUniqueName nsu) { + if (show_details) { + details.add(this.getHttpDetails("Unknown metric", dp)); + } + LOG.warn("Unknown metric: " + dp); + unknown_metrics.incrementAndGet(); + } + } + + final long failures = total - success; + if (!show_summary && !show_details) { + if (failures > 0) { + throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "One or more data points had errors", + "Please see the TSD logs or append \"details\" to the putctr request"); + } else { + query.sendReply(HttpResponseStatus.NO_CONTENT, "".getBytes()); + } + } else { + final HashMap summary = new HashMap(); + summary.put("success", success); + summary.put("failed", failures); + if (show_details) { + summary.put("errors", details); + } + + if (failures > 0) { + query.sendReply(HttpResponseStatus.BAD_REQUEST, + query.serializer().formatPutV1(summary)); + } else { + query.sendReply(query.serializer().formatPutV1(summary)); + } + } + } + + /** + * Imports a single data point. + * + * @param tsdb The TSDB to import the data point into. + * @param words The words describing the data point to import, in + * the following format: {@code [metric, timestamp, value, ..tags..]} + * @return A deferred object that indicates the completion of the request. + * @throws NumberFormatException if the timestamp or value is invalid. + * @throws IllegalArgumentException if any other argument is invalid. + * @throws NoSuchUniqueName if the metric isn't registered. + */ + private Deferred importDataPoint(final TSDB tsdb, final String[] words) { + words[0] = null; // Ditch the "put". + if (words.length < 5) { // Need at least: metric timestamp value tag + // ^ 5 and not 4 because words[0] is "put". + throw new IllegalArgumentException("not enough arguments" + + " (need least 4, got " + (words.length - 1) + ')'); + } + final String metric = words[1]; + if (metric.length() <= 0) { + throw new IllegalArgumentException("empty metric name"); + } + final long timestamp; + if (words[2].contains(".")) { + timestamp = Tags.parseLong(words[2].replace(".", "")); + } else { + timestamp = Tags.parseLong(words[2]); + } + if (timestamp <= 0) { + throw new IllegalArgumentException("invalid timestamp: " + timestamp); + } + final String value = words[3]; + if (value.length() <= 0) { + throw new IllegalArgumentException("empty value"); + } + final HashMap tags = new HashMap(); + for (int i = 4; i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + if (Tags.looksLikeInteger(value)) { + return tsdb.addCounter(metric, timestamp, Tags.parseLong(value), tags); + } else { // floating point value + Float valueAsFloat = Float.parseFloat(value); + return tsdb.addCounter(metric, timestamp, valueAsFloat.longValue(), tags); + } + } + + /** + * Simple helper to format an error trying to save a data point + * + * @param message The message to return to the user + * @param dp The datapoint that caused the error + * @return A hashmap with information + * @since 2.0 + */ + final private HashMap getHttpDetails(final String message, + final IncomingDataPoint dp) { + final HashMap map = new HashMap(); + map.put("error", message); + map.put("datapoint", dp); + return map; + } +} + diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index f5d82a7643..7a053a0b64 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -274,11 +274,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) http_requests.incrementAndGet(); // only accept POST - if (query.method() != HttpMethod.POST) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); - } + RpcUtil.allowedMethods(query.method(), HttpMethod.POST.getName()); final List dps; //noinspection TryWithIdenticalCatches @@ -616,60 +612,65 @@ class GroupCB implements Callback> { public GroupCB(final int queued) { this.queued = queued; } - + @Override public Object call(final ArrayList results) { - if (sending_response.get()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Put data point call " + query + " was marked as timedout"); - } - return null; - } else { - sending_response.set(true); - if (timeout != null) { - timeout.cancel(); - } - } - int good_writes = 0; - int failed_writes = 0; - for (final boolean result : results) { - if (result) { - ++good_writes; - } else { - ++failed_writes; - } - } - - final int failures = dps.size() - queued; - if (!show_summary && !show_details) { - if (failures + failed_writes > 0) { - query.sendReply(HttpResponseStatus.BAD_REQUEST, - query.serializer().formatErrorV1( - new BadRequestException(HttpResponseStatus.BAD_REQUEST, - "One or more data points had errors", - "Please see the TSD logs or append \"details\" to the put request"))); - } else { - query.sendReply(HttpResponseStatus.NO_CONTENT, "".getBytes()); - } - } else { - final HashMap summary = new HashMap(); - if (sync_timeout > 0) { - summary.put("timeouts", 0); - } - summary.put("success", results.isEmpty() ? queued : good_writes); - summary.put("failed", failures + failed_writes); - if (show_details) { - summary.put("errors", details); - } - - if (failures > 0) { - query.sendReply(HttpResponseStatus.BAD_REQUEST, - query.serializer().formatPutV1(summary)); - } else { - query.sendReply(query.serializer().formatPutV1(summary)); + tsdb.response(new Runnable() { + @Override + public void run() { + if (sending_response.get()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Put data point call " + query + " was marked as timedout"); + } + return; + } else { + sending_response.set(true); + if (timeout != null) { + timeout.cancel(); + } + } + int good_writes = 0; + int failed_writes = 0; + for (final boolean result : results) { + if (result) { + ++good_writes; + } else { + ++failed_writes; + } + } + + final int failures = dps.size() - queued; + if (!show_summary && !show_details) { + if (failures + failed_writes > 0) { + query.sendReply(HttpResponseStatus.BAD_REQUEST, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "One or more data points had errors", + "Please see the TSD logs or append \"details\" to the put request"))); + } else { + query.sendReply(HttpResponseStatus.NO_CONTENT, "".getBytes()); + } + } else { + final HashMap summary = new HashMap(); + if (sync_timeout > 0) { + summary.put("timeouts", 0); + } + summary.put("success", results.isEmpty() ? queued : good_writes); + summary.put("failed", failures + failed_writes); + if (show_details) { + summary.put("errors", details); + } + + if (failures > 0) { + query.sendReply(HttpResponseStatus.BAD_REQUEST, + query.serializer().formatPutV1(summary)); + } else { + query.sendReply(query.serializer().formatPutV1(summary)); + } + } } - } - + }); + return null; } @Override diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index fb1e1fedc2..d875acd9bc 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -87,16 +87,12 @@ final class QueryRpc implements HttpRpc { */ @Override public void execute(final TSDB tsdb, final HttpQuery query) - throws IOException { - + throws BadRequestException, IOException { + // only accept GET/POST/DELETE - if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST && - query.method() != HttpMethod.DELETE) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); - } - if (query.method() == HttpMethod.DELETE && + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.DELETE.getName(), HttpMethod.POST.getName()); + + if (query.method() == HttpMethod.DELETE && !tsdb.getConfig().getBoolean("tsd.http.query.allow_delete")) { throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, "Bad request", diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index 0a89c99e0e..1424bc4217 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -70,7 +70,6 @@ final class RpcHandler extends IdleStateAwareChannelUpstreamHandler { * Constructor that loads the CORS domain list and prepares for * handling requests. This constructor creates its own {@link RpcManager}. * @param tsdb The TSDB to use. - * @param manager instance of a ready-to-use {@link RpcManager}. * @throws IllegalArgumentException if there was an error with the CORS domain * list */ @@ -248,6 +247,14 @@ private boolean applyCorsConfig(final HttpRequest req, final AbstractHttpQuery q * @param req The parsed HTTP request. */ private void handleHttpQuery(final TSDB tsdb, final Channel chan, final HttpRequest req) { + // quick bail if not GET/POST/OPTIONS/PUT/DELETE, no other methods are allowed anywhere + try { + RpcUtil.allowedMethods(req.getMethod(), HttpMethod.GET.getName(), HttpMethod.POST.getName(), + HttpMethod.OPTIONS.getName(), HttpMethod.PUT.getName(), HttpMethod.DELETE.getName()); + } catch (BadRequestException bre) { + sendStatusAndClose(chan, HttpResponseStatus.METHOD_NOT_ALLOWED); + } + AbstractHttpQuery abstractQuery = null; try { abstractQuery = createQueryInstance(tsdb, req, chan); diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index d757d686dc..32ccfff40b 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -22,6 +22,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; +import com.google.common.collect.Table; +import net.opentsdb.core.TSDB.TableAvailability; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -108,6 +110,8 @@ public final class RpcManager { private ImmutableMap http_plugin_commands; /** List of activated RPC plugins */ private ImmutableList rpc_plugins; + /** Status command—we keep a reference so we can explicitly shut it down. */ + private Status status; /** The TSDB that owns us. */ private TSDB tsdb; @@ -133,7 +137,7 @@ public static synchronized RpcManager instance(final TSDB tsdb) { } final RpcManager manager = new RpcManager(tsdb); - + // Load any plugins that are enabled via Config. Fail if any plugin cannot be loaded. final ImmutableList.Builder rpcBuilder = ImmutableList.builder(); @@ -242,10 +246,10 @@ boolean isHttpRpcPluginPath(final String uri) { /** * Load and init instances of {@link TelnetRpc}s and {@link HttpRpc}s. * These are not generally configurable via TSDB config. - * @param mode is this TSD in read/write ("rw") or read-only ("ro") - * mode? * @param telnet a map of telnet command names to {@link TelnetRpc} * instances. + * @param mode is this TSD in read/write ("rw") or read-only ("ro") + * mode? * @param http a map of API endpoints to {@link HttpRpc} instances. */ private void initializeBuiltinRpcs(final OperationMode mode, @@ -263,10 +267,12 @@ private void initializeBuiltinRpcs(final OperationMode mode, final ListAggregators aggregators = new ListAggregators(); final DropCachesRpc dropcaches = new DropCachesRpc(); final Version version = new Version(); - + status = new Status(); + telnet.put("stats", stats); telnet.put("dropcaches", dropcaches); telnet.put("version", version); + telnet.put("status", status); telnet.put("exit", new Exit()); telnet.put("help", new Help()); @@ -283,6 +289,7 @@ private void initializeBuiltinRpcs(final OperationMode mode, http.put("api/dropcaches", dropcaches); http.put("api/stats", stats); http.put("api/version", version); + http.put("api/status", status); } final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); @@ -305,7 +312,7 @@ private void initializeBuiltinRpcs(final OperationMode mode, http.put("api/rollup", rollups); http.put("api/histogram", histos); http.put("api/tree", new TreeRpc()); - http.put("api/uid", new UniqueIdRpc()); + http.put("api/uid", new UniqueIdRpc(mode)); } break; case READONLY: @@ -321,6 +328,7 @@ private void initializeBuiltinRpcs(final OperationMode mode, http.put("api/query", new QueryRpc()); http.put("api/search", new SearchRpc()); http.put("api/suggest", suggest_rpc); + http.put("api/uid", new UniqueIdRpc(mode)); } break; @@ -347,11 +355,9 @@ private void initializeBuiltinRpcs(final OperationMode mode, http.put("api/rollup", rollups); http.put("api/histogram", histos); http.put("api/tree", new TreeRpc()); - http.put("api/uid", new UniqueIdRpc()); + http.put("api/uid", new UniqueIdRpc(mode)); } } - - if (enableDieDieDie) { final DieDieDie diediedie = new DieDieDie(); @@ -377,6 +383,7 @@ private void initializeBuiltinRpcs(final OperationMode mode, protected void initializeHttpRpcPlugins(final OperationMode mode, final String[] pluginClassNames, final ImmutableMap.Builder http) { + for (final String plugin : pluginClassNames) { final HttpRpcPlugin rpc = createAndInitialize(plugin, HttpRpcPlugin.class); validateHttpRpcPluginPath(rpc.getPath()); @@ -480,6 +487,8 @@ protected T createAndInitialize(final String pluginClassName, final Class * (think of it as {@code Deferred}). */ public Deferred> shutdown() { + status.shutdown(); + // Clear shared instance. INSTANCE.set(null); @@ -627,8 +636,8 @@ private static final class ListAggregators implements HttpRpc { public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { - // only accept GET / POST - RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + // only accept GET + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName()); if (query.apiVersion() > 0) { query.sendReply( @@ -639,6 +648,80 @@ public void execute(final TSDB tsdb, final HttpQuery query) } } + /** The "status" command. */ + static final class Status implements TelnetRpc, HttpRpc { + String status = "startup"; + + /** Called by RpcManager when it is shutdown. */ + public void shutdown() { + status = "shutting-down"; + } + + /** Update status, return Deferred that fires when status is updated. */ + private Deferred updateStatus(final TSDB tsdb) { + // Once we're in shutdown mode the status never changes. + if (status == "shutting-down") { + return Deferred.fromResult(null); + } + + Deferred availability = tsdb.checkNecessaryTablesAvailability(); + + final class AvailabilityToStatusCB implements Callback { + @Override + public Object call(final TableAvailability availability) { + // If we're in startup mode, lack of availability may just be due to + // starting up, so don't consider that an error state. + if ((status == "startup") && (availability == TableAvailability.NONE)) { + return null; + } + + if (availability == TableAvailability.FULL) { + status = "ok"; + } else if (availability == TableAvailability.PARTIAL) { + status = "partial"; + } else { + status = "error"; + } + return null; + } + } + return availability.addCallback(new AvailabilityToStatusCB()); + } + + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + final class WriteStatusCB implements Callback { + @Override + public Object call(final Object o) { + if (chan.isConnected()) { + chan.write(status + '\n'); + } + return null; + } + } + + return updateStatus(tsdb).addCallback(new WriteStatusCB()); + } + + public void execute(final TSDB tsdb, final HttpQuery query) throws + IOException { + // only accept GET + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName()); + + final class WriteStatusCB implements Callback { + @Override + public Object call(final Object o) { + final HashMap result = new HashMap(); + result.put("status", status); + query.sendReply(JSON.serializeToBytes(result)); + return null; + } + } + + updateStatus(tsdb).addCallback(new WriteStatusCB()); + } + } + /** The "version" command. */ private static final class Version implements TelnetRpc, HttpRpc { public Deferred execute(final TSDB tsdb, final Channel chan, @@ -653,8 +736,8 @@ public Deferred execute(final TSDB tsdb, final Channel chan, public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { - // only accept GET / POST - RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + // only accept GET + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName()); final HashMap version = new HashMap(); version.put("version", BuildData.version); diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index d4bc3c9ee8..4b15856b03 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -55,12 +55,9 @@ final class SearchRpc implements HttpRpc { */ @Override public void execute(TSDB tsdb, HttpQuery query) { - final HttpMethod method = query.getAPIMethod(); - if (method != HttpMethod.GET && method != HttpMethod.POST) { - throw new BadRequestException("Unsupported method: " + method.getName()); - } - + RpcUtil.allowedMethods(method, HttpMethod.GET.getName(), HttpMethod.POST.getName()); + // the uri will be /api/vX/search/ or /api/search/ final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; diff --git a/src/tsd/StaticFileRpc.java b/src/tsd/StaticFileRpc.java index f3f8c552ef..28f4220bf9 100644 --- a/src/tsd/StaticFileRpc.java +++ b/src/tsd/StaticFileRpc.java @@ -15,6 +15,7 @@ import java.io.IOException; import net.opentsdb.core.TSDB; +import org.jboss.netty.handler.codec.http.HttpMethod; /** Implements the "/s" endpoint to serve static files. */ final class StaticFileRpc implements HttpRpc { @@ -26,13 +27,18 @@ public StaticFileRpc() { } public void execute(final TSDB tsdb, final HttpQuery query) - throws IOException { + throws BadRequestException, IOException { + + // only accept GET + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName()); + final String uri = query.request().getUri(); if ("/favicon.ico".equals(uri)) { query.sendFile(tsdb.getConfig().getDirectoryName("tsd.http.staticroot") + "/favicon.ico", 31536000 /*=1yr*/); return; } + if (uri.length() < 3) { // Must be at least 3 because of the "/s/". throw new BadRequestException("URI too short " + uri + ""); } @@ -41,6 +47,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) if (uri.indexOf("..", 3) > 0) { throw new BadRequestException("Malformed URI " + uri + ""); } + final int questionmark = uri.indexOf('?', 3); final int pathend = questionmark > 0 ? questionmark : uri.length(); query.sendFile(tsdb.getConfig().getDirectoryName("tsd.http.staticroot") diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index bd0ee910d1..7dd67c2503 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import java.io.IOException; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; @@ -67,18 +68,14 @@ public Deferred execute(final TSDB tsdb, final Channel chan, } /** - * HTTP resposne handler + * HTTP response handler * @param tsdb The TSDB to which we belong * @param query The query to parse and respond to */ - public void execute(final TSDB tsdb, final HttpQuery query) { - // only accept GET/POST - if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); - } - + public void execute(final TSDB tsdb, final HttpQuery query) throws BadRequestException, IOException { + + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + try { final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1].toLowerCase() : ""; diff --git a/src/tsd/SuggestRpc.java b/src/tsd/SuggestRpc.java index 7c8601ddf8..6e4c677919 100644 --- a/src/tsd/SuggestRpc.java +++ b/src/tsd/SuggestRpc.java @@ -39,14 +39,10 @@ final class SuggestRpc implements HttpRpc { */ public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { - + // only accept GET/POST - if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); - } - + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + final String type; final String q; final String max; diff --git a/src/tsd/TreeRpc.java b/src/tsd/TreeRpc.java index 380c4eb308..3cdfe436fd 100644 --- a/src/tsd/TreeRpc.java +++ b/src/tsd/TreeRpc.java @@ -52,6 +52,9 @@ final class TreeRpc implements HttpRpc { */ @Override public void execute(TSDB tsdb, HttpQuery query) throws IOException { + // only accept GET/POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + // the uri will be /api/vX/tree/? or /api/tree/? final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; @@ -208,11 +211,9 @@ private void handleTree(TSDB tsdb, HttpQuery query) { * @throws BadRequestException if the request was invalid. */ private void handleBranch(TSDB tsdb, HttpQuery query) { - if (query.getAPIMethod() != HttpMethod.GET) { - throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, - "Unsupported HTTP request method"); - } - + + RpcUtil.allowedMethods(query.getAPIMethod(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + try { final int tree_id = parseTreeId(query, false); final String branch_hex = diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index a9057866f8..11bca6e664 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -47,6 +47,12 @@ */ final class UniqueIdRpc implements HttpRpc { + private final TSDB.OperationMode mode; + + public UniqueIdRpc(TSDB.OperationMode mode) { + this.mode = mode; + } + @Override public void execute(TSDB tsdb, HttpQuery query) throws IOException { @@ -87,13 +93,14 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { * @param query The query for this request */ private void handleAssign(final TSDB tsdb, final HttpQuery query) { - // only accept GET And POST - if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + query.method().getName() + - "] is not permitted for this endpoint"); + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); } - + + // only accept GET/POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + final HashMap> source; if (query.method() == HttpMethod.POST) { source = query.serializer().parseUidAssignV1(); @@ -160,24 +167,34 @@ private void handleAssign(final TSDB tsdb, final HttpQuery query) { private void handleUIDMeta(final TSDB tsdb, final HttpQuery query) { final HttpMethod method = query.getAPIMethod(); + RpcUtil.allowedMethods(method, HttpMethod.GET.getName(), HttpMethod.POST.getName(), HttpMethod.PUT.getName(), HttpMethod.DELETE.getName()); + // GET if (method == HttpMethod.GET) { + if (!mode.isRead()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in wo mode."); + } final String uid = query.getRequiredQueryStringParam("uid"); final UniqueIdType type = UniqueId.stringToUniqueIdType( query.getRequiredQueryStringParam("type")); try { final UIDMeta meta = UIDMeta.getUIDMeta(tsdb, type, uid) - .joinUninterruptibly(); + .joinUninterruptibly(); query.sendReply(query.serializer().formatUidMetaV1(meta)); } catch (NoSuchUniqueId e) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, - "Could not find the requested UID", e); + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Could not find the requested UID", e); } catch (Exception e) { throw new RuntimeException(e); } - // POST + // POST } else if (method == HttpMethod.POST || method == HttpMethod.PUT) { + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); + } final UIDMeta meta; if (query.hasContent()) { @@ -185,30 +202,30 @@ private void handleUIDMeta(final TSDB tsdb, final HttpQuery query) { } else { meta = this.parseUIDMetaQS(query); } - + /** * Storage callback used to determine if the storage call was successful * or not. Also returns the updated object from storage. */ class SyncCB implements Callback, Boolean> { - + @Override public Deferred call(Boolean success) throws Exception { if (!success) { throw new BadRequestException( - HttpResponseStatus.INTERNAL_SERVER_ERROR, - "Failed to save the UIDMeta to storage", - "This may be caused by another process modifying storage data"); + HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Failed to save the UIDMeta to storage", + "This may be caused by another process modifying storage data"); } - + return UIDMeta.getUIDMeta(tsdb, meta.getType(), meta.getUID()); } - + } - + try { - final Deferred process_meta = meta.syncToStorage(tsdb, - method == HttpMethod.PUT).addCallbackDeferring(new SyncCB()); + final Deferred process_meta = meta.syncToStorage(tsdb, + method == HttpMethod.PUT).addCallbackDeferring(new SyncCB()); final UIDMeta updated_meta = process_meta.joinUninterruptibly(); tsdb.indexUIDMeta(updated_meta); query.sendReply(query.serializer().formatUidMetaV1(updated_meta)); @@ -217,13 +234,17 @@ public Deferred call(Boolean success) throws Exception { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } catch (NoSuchUniqueId e) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Could not find the requested UID", e); } catch (Exception e) { throw new RuntimeException(e); } - // DELETE + // DELETE } else if (method == HttpMethod.DELETE) { + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); + } final UIDMeta meta; if (query.hasContent()) { @@ -231,23 +252,18 @@ public Deferred call(Boolean success) throws Exception { } else { meta = this.parseUIDMetaQS(query); } - try { + try { meta.delete(tsdb).joinUninterruptibly(); tsdb.deleteUIDMeta(meta); } catch (IllegalArgumentException e) { throw new BadRequestException("Unable to delete UIDMeta information", e); } catch (NoSuchUniqueId e) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, - "Could not find the requested UID", e); + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Could not find the requested UID", e); } catch (Exception e) { throw new RuntimeException(e); } query.sendStatusOnly(HttpResponseStatus.NO_CONTENT); - - } else { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + method.getName() + - "] is not permitted for this endpoint"); } } @@ -259,8 +275,14 @@ public Deferred call(Boolean success) throws Exception { private void handleTSMeta(final TSDB tsdb, final HttpQuery query) { final HttpMethod method = query.getAPIMethod(); + RpcUtil.allowedMethods(method, HttpMethod.GET.getName(), HttpMethod.POST.getName(), HttpMethod.DELETE.getName(), HttpMethod.PUT.getName()); + // GET if (method == HttpMethod.GET) { + if (!mode.isRead()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in wo mode."); + } String tsuid = null; if (query.hasQueryStringParam("tsuid")) { @@ -313,6 +335,10 @@ private void handleTSMeta(final TSDB tsdb, final HttpQuery query) { } // POST / PUT } else if (method == HttpMethod.POST || method == HttpMethod.PUT) { + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); + } final TSMeta meta; if (query.hasContent()) { @@ -431,6 +457,10 @@ public Boolean call(Boolean exists) throws Exception { } // DELETE } else if (method == HttpMethod.DELETE) { + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); + } final TSMeta meta; if (query.hasContent()) { @@ -445,10 +475,6 @@ public Boolean call(Boolean exists) throws Exception { throw new BadRequestException("Unable to delete TSMeta information", e); } query.sendStatusOnly(HttpResponseStatus.NO_CONTENT); - } else { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + method.getName() + - "] is not permitted for this endpoint"); } } @@ -491,14 +517,15 @@ private UIDMeta parseUIDMetaQS(final HttpQuery query) { * @param query The query for this request */ private void handleRename(final TSDB tsdb, final HttpQuery query) { - // only accept GET and POST - if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method[" + query.method().getName() + - "] is not permitted for this endpoint"); + if (!mode.isWrite()) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Operation not allowed", + "This operation is not allowed in ro mode."); } + // only accept GET and POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); final HashMap source; + if (query.method() == HttpMethod.POST) { source = query.serializer().parseUidRenameV1(); } else { diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index e273b51d97..ac3bc5456c 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -424,8 +424,8 @@ public boolean buildQueryString(final StringBuilder url) { } url.append(':').append(metric); List filters = getFilters(true); + url.append('{'); if (!filters.isEmpty()) { - url.append('{'); for (int i = 0; i < filters.size(); i++) { if (i > 0) { url.append(","); @@ -434,8 +434,8 @@ public boolean buildQueryString(final StringBuilder url) { .append("=") .append(filters.get(i).tagv); } - url.append('}'); } + url.append('}'); // now the non-group bys filters = getFilters(false); if (!filters.isEmpty()) { diff --git a/src/utils/Config.java b/src/utils/Config.java index f92f2f5b56..be101dbae9 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Properties; +import net.opentsdb.core.RpcResponder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +74,9 @@ public class Config { /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; + /** tsd.storage.sum_duplicates */ + private boolean sum_duplicates = false; + /** tsd.storage.enable_appends */ private boolean enable_appends = false; @@ -208,6 +212,10 @@ public boolean auto_tagv() { return auto_tagv; } + public boolean sum_duplicates() { + return sum_duplicates; + } + /** @param auto_metric whether or not to auto create metrics */ public void setAutoMetric(boolean auto_metric) { this.auto_metric = auto_metric; @@ -340,6 +348,23 @@ public final int getInt(final String property) { return Integer.parseInt(sanitize(properties.get(property))); } + /** + * Returns the given property as an integer. + * If no such property is specified, or if the specified value is not a valid + * Int, then default_val is returned. + * + * @param property The property to load + * @param default_val default value + * @return A parsed integer or default_val. + */ + public final int getInt(final String property, final int default_val) { + try { + return getInt(property); + } catch (Exception e) { + return default_val; + } + } + /** * Returns the given string trimed or null if is null * @param string The string be trimmed of @@ -420,6 +445,23 @@ public final boolean getBoolean(final String property) { return false; } + /** + * Returns the given property as an boolean. + * If no such property is specified, or if the specified value is not a valid + * boolean, then default_val is returned. + * + * @param property The property to load + * @param default_val default value + * @return A parsed boolean or default_val. + */ + public final boolean getBoolean(final String property, final boolean default_val) { + try { + return getBoolean(property); + } catch (Exception e) { + return default_val; + } + } + /** * Returns the directory name, making sure the end is an OS dependent slash * @param property The property to load @@ -577,6 +619,7 @@ protected void setDefaults() { default_map.put("tsd.rollups.agg_tag_key", "_aggregate"); default_map.put("tsd.rollups.raw_agg_tag_value", "RAW"); default_map.put("tsd.rollups.block_derived", "true"); + default_map.put("tsd.rollups.split_query.enable", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); @@ -590,12 +633,14 @@ protected void setDefaults() { default_map.put("tsd.storage.hbase.data_table", "tsdb"); default_map.put("tsd.storage.hbase.uid_table", "tsdb-uid"); default_map.put("tsd.storage.hbase.tree_table", "tsdb-tree"); + default_map.put("tsd.storage.hbase.use_hbase_counters", "false"); default_map.put("tsd.storage.hbase.meta_table", "tsdb-meta"); default_map.put("tsd.storage.hbase.zk_quorum", "localhost"); default_map.put("tsd.storage.hbase.zk_basedir", "/hbase"); default_map.put("tsd.storage.hbase.prefetch_meta", "false"); default_map.put("tsd.storage.enable_appends", "false"); default_map.put("tsd.storage.repair_appends", "false"); + default_map.put("tsd.storage.sum_duplicates", "false"); default_map.put("tsd.storage.enable_compaction", "true"); default_map.put("tsd.storage.compaction.flush_interval", "10"); default_map.put("tsd.storage.compaction.min_flush_threshold", "100"); @@ -719,6 +764,7 @@ public void loadStaticVariables() { auto_tagk = this.getBoolean("tsd.core.auto_create_tagks"); auto_tagv = this.getBoolean("tsd.core.auto_create_tagvs"); enable_compactions = this.getBoolean("tsd.storage.enable_compaction"); + sum_duplicates = this.getBoolean("tsd.storage.sum_duplicates"); enable_appends = this.getBoolean("tsd.storage.enable_appends"); repair_appends = this.getBoolean("tsd.storage.repair_appends"); enable_chunked_requests = this.getBoolean("tsd.http.request.enable_chunked"); diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 4649fc3097..8989ae85f5 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.TimeZone; +import com.google.common.base.Strings; import net.opentsdb.core.Tags; /** @@ -184,6 +185,10 @@ public static final long parseDateTimeString(final String datetime, * @throws IllegalArgumentException if the interval was malformed. */ public static final long parseDuration(final String duration) { + if (duration == null || duration.isEmpty()) { + throw new IllegalArgumentException("Cannot parse null or empty duration"); + } + long interval; long multiplier; double temp; @@ -614,7 +619,7 @@ public static Calendar previousInterval(final long ts, final int interval, * @since 2.3 */ public static int unitsToCalendarType(final String units) { - if (units == null || units.isEmpty()) { + if (Strings.isNullOrEmpty(units)) { throw new IllegalArgumentException("Units cannot be null or empty"); } diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 9dbc6c31e3..bafe0eae95 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -31,6 +31,8 @@ import net.opentsdb.auth.Authentication; import net.opentsdb.auth.Authorization; import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; @@ -914,6 +916,23 @@ protected void storeAnnotation(final long timestamp) throws Exception { note.syncToStorage(tsdb, false).joinUninterruptibly(); } + RollupQuery makeRollupQuery() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", true); + final RollupInterval oneHourWithDelay = RollupInterval.builder() + .setTable("fake-rollup-table") + .setPreAggregationTable("fake-preagg-table") + .setInterval("1h") + .setRowSpan("1d") + .setDelaySla("2d") + .build(); + return new RollupQuery( + oneHourWithDelay, + Aggregators.SUM, + 3600000, + Aggregators.SUM + ); + } + /** * A fake {@link org.jboss.netty.util.Timer} implementation. * Instead of executing the task it will store that task in a internal state @@ -984,4 +1003,4 @@ public UnitTestException(final String msg) { } private static final long serialVersionUID = -4404095849459619922L; } -} \ No newline at end of file +} diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index d0024716bf..5432b4c48c 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -21,6 +21,7 @@ import java.util.Calendar; import java.util.List; +import java.util.Locale; import java.util.TimeZone; import com.google.common.collect.Lists; @@ -591,6 +592,9 @@ public void testDownsampler_calendarDay() { @Test public void testDownsampler_calendarWeek() { + // Test assumes Sunday is first day of week. + Locale.setDefault(Locale.US); + source = SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofLongValue(DST_TS, 1), // a Tuesday in UTC land MutableDataPoint.ofLongValue(DST_TS + (86400000L * 7), 2), @@ -901,7 +905,9 @@ public void testDownsampler_1week() { MutableDataPoint.ofLongValue(1357430400000L, 4), MutableDataPoint.ofLongValue(1357732800000L, 8) })); - + + // Test assumes Sunday is first day of week. + Locale.setDefault(Locale.US); specification = new DownsamplingSpecification("1wc-sum"); downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); @@ -925,7 +931,9 @@ public void testDownsampler_1week_timezone() { MutableDataPoint.ofLongValue(1357448400000L, 4), MutableDataPoint.ofLongValue(1357750800000L, 8) })); - + + // Test assumes Sunday is first day of week. + Locale.setDefault(Locale.US); specification = new DownsamplingSpecification("1wc-sum"); specification.setTimezone(EST_TIME_ZONE); downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); @@ -1269,14 +1277,23 @@ public void testDownsampler_rollupCount() { .setInterval("1h") .setRowSpan("1d") .build(); + + // This query, in combination with the configuration/interval above, is asking for rolled up COUNTs (i.e. already + // downsampled). These COUNTs should then be SUMmed over some interval we'll define later in a DownsamplingSpecification final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.COUNT, 3600000, Aggregators.SUM); + + // These points represent rolled up COUNTs that would be stored in the rollup table (e.g. tsdb-rollup-1h) and as + // such we don't expect these to be COUNTed again on retrieval. These points are at 5s intervals source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 2, 4), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 3, 8) })); + + // The rolled up points above are at 5s intervals but we want to downsample these further so that we only get + // points at 10s intervals specification = new DownsamplingSpecification("10s-count"); downsampler = new Downsampler(source, specification, 0, 0, rollup_query); verify(source, never()).next(); @@ -1289,10 +1306,16 @@ public void testDownsampler_rollupCount() { timestamps_in_millis.add(dp.timestamp()); } + // Asserts here different to upstream as there is a bug upstream and the original test was written to pass with the bug. + // 2 points are expected because the 4 points at 5s intervals will be downsampled to 2 points at 10s intervals assertEquals(2, values.size()); - assertEquals(2, values.get(0), 0.0000001); + + // Expect the SUM of points 1 and 2 + assertEquals(3, values.get(0), 0.0000001); assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); - assertEquals(2, values.get(1), 0.0000001); + + // Expect the SUM of points 3 and 4 + assertEquals(12, values.get(1), 0.0000001); assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); } diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index 8f38e41bed..dbcc056cf5 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -975,7 +975,7 @@ public void testDownsampler_rollupAvgMissing() { step(downsampler, timestamp += 100, Double.NaN); assertFalse(downsampler.hasNext()); } - + @Test public void testDownsampler_rollupCount() { final RollupInterval interval = RollupInterval.builder() @@ -984,35 +984,49 @@ public void testDownsampler_rollupCount() { .setInterval("1h") .setRowSpan("1d") .build(); + + // This query, in combination with the configuration/interval above, is asking for rolled up COUNTs (i.e. already + // downsampled). These COUNTs should then be SUMmed over some interval we'll define later in a DownsamplingSpecification final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.COUNT, 3600000, Aggregators.SUM); final long baseTime = 1000L; + + // These points represent rolled up COUNTs that would be stored in the rollup table (e.g. tsdb-rollup-1h) and as + // such we don't expect these to be COUNTed again on retrieval. These points are at 25ms intervals final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(baseTime + 25L * 0L, 12.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 1L, 11.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 2L, 10.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 3L, 9.), - + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 8.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 7.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 6L, 6.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 5.), - + MutableDataPoint.ofDoubleValue(baseTime + 25L * 8L, 4.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 9L, 3.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 10L, 2.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 11L, 1.), }); + // The rolled up points above are at 25ms intervals but we want to downsample these further so that we only get + // points at 100ms intervals specification = new DownsamplingSpecification("100ms-count-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, baseTime + 12L * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; - step(downsampler, timestamp, 4); - step(downsampler, timestamp += 100, 4); - step(downsampler, timestamp += 100, 4); + + // Expect the SUM of points 1 to 4 + step(downsampler, timestamp, 42); + + // Expect the SUM of points 5 to 8 + step(downsampler, timestamp += 100, 26); + + // Expect the SUM of points 9 to 12 + step(downsampler, timestamp += 100, 10); assertFalse(downsampler.hasNext()); } diff --git a/test/core/TestMultiGetQuery.java b/test/core/TestMultiGetQuery.java index 217bb613fd..bf04d90c87 100644 --- a/test/core/TestMultiGetQuery.java +++ b/test/core/TestMultiGetQuery.java @@ -27,6 +27,7 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; +import java.util.SortedMap; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.GetRequest; @@ -690,7 +691,7 @@ public void fetch() throws Exception { start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); assertSame(spans, results); verify(client, times(1)).get(anyList()); System.out.println(spans); @@ -705,7 +706,7 @@ public void fetchMultigetNoMeta() throws Exception { start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); assertSame(spans, results); verify(client, times(1)).get(anyList()); System.out.println(spans); @@ -719,7 +720,7 @@ public void fetchMoreThanMaxBytes() throws Exception { MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags, start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); } @Test @@ -730,7 +731,7 @@ public void fetchEmptyTable() throws Exception { start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); assertSame(spans, results); assertTrue(spans.isEmpty()); verify(client, times(1)).get(anyList()); @@ -745,7 +746,7 @@ public void fetchSmallBatch() throws Exception { start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); assertSame(spans, results); verify(client, times(4)).get(anyList()); validateSpans(); @@ -761,7 +762,7 @@ public void fetchSmallBatchAndSmallConcurrent() throws Exception { start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, 0, max_bytes, false, multiget_no_meta); - final TreeMap results = mgq.fetch().join(); + final SortedMap results = mgq.fetch().join(); assertSame(spans, results); verify(client, times(4)).get(anyList()); validateSpans(); diff --git a/test/core/TestRpcResponsder.java b/test/core/TestRpcResponsder.java new file mode 100644 index 0000000000..5ddbf73baf --- /dev/null +++ b/test/core/TestRpcResponsder.java @@ -0,0 +1,65 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2017 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 net.opentsdb.utils.Config; +import org.jboss.netty.util.internal.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +public class TestRpcResponsder { + + private final AtomicInteger complete_counter = new AtomicInteger(0); + + @Test(timeout = 60000) + public void testGracefulShutdown() throws InterruptedException { + RpcResponder rpcResponder = new RpcResponder(new Config()); + + final int n = 100; + for (int i = 0; i < n; i++) { + rpcResponder.response(new MockResponseProcess()); + } + + Thread.sleep(500); + rpcResponder.close(); + + try { + rpcResponder.response(new MockResponseProcess()); + Assert.fail("Expect an IllegalStateException"); + } catch (IllegalStateException ignore) { + } + + Assert.assertEquals(n, complete_counter.get()); + } + + private class MockResponseProcess implements Runnable { + + @Override + public void run() { + long duration = ThreadLocalRandom.current().nextInt(5000); + while (duration > 0) { + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + } + duration -= 100; + } + complete_counter.incrementAndGet(); + } + } + + +} diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index 0e3dfebed0..d3504d3b62 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -45,6 +45,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import com.stumbleupon.async.Deferred; +import com.google.common.collect.Maps; @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", @@ -136,7 +137,7 @@ public void ctorSpansHaveData() { public void scanNoData() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertTrue(spans.isEmpty()); } @@ -145,7 +146,7 @@ public void scan() throws Exception { setupMockScanners(false); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); Span span = spans.get(KEY_A); @@ -181,7 +182,7 @@ public void scanWithFilter() throws Exception { .setTagk(TAGK_STRING).build()); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); Span span = spans.get(KEY_A); @@ -219,7 +220,7 @@ public void scanWithTwoFilter() throws Exception { .setTagk(TAGK_STRING).build()); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); Span span = spans.get(KEY_A); @@ -256,7 +257,7 @@ public void scanWithFilterNoMatch() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); @@ -272,7 +273,7 @@ public void scanWithTwoFiltersNoMatch() throws Exception { .setTagk(TAGK_STRING).build()); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index 0d63fa1bda..76d779b0fd 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -39,6 +39,8 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; +import com.google.common.collect.Maps; + import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.util.ArrayList; @@ -197,7 +199,7 @@ public void scan() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans, 0, 0); - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -230,7 +232,7 @@ public void scanWithFilter() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans, 0, 0); - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -265,7 +267,7 @@ public void scanWithFiltersOnSameTag() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans, 0, 0); - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -299,7 +301,7 @@ public void scanWithFiltersOnSameTagOneFail() throws Exception { final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, filters, false, null, query_stats, 0, spans, 0, 0); - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); } diff --git a/test/core/TestSeekableViewChain.java b/test/core/TestSeekableViewChain.java new file mode 100644 index 0000000000..5da9efbc62 --- /dev/null +++ b/test/core/TestSeekableViewChain.java @@ -0,0 +1,99 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2017 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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@RunWith(PowerMockRunner.class) +public class TestSeekableViewChain { + + private static final long BASE_TIME = 1356998400000L; + private static final DataPoint[] DATA_POINTS_1 = new DataPoint[]{ + MutableDataPoint.ofLongValue(BASE_TIME, 40), + MutableDataPoint.ofLongValue(BASE_TIME + 10000, 50), + MutableDataPoint.ofLongValue(BASE_TIME + 30000, 70) + }; + + @Before + public void before() throws Exception { + + } + + @Test + public void testIteratorChain() { + List iterators = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + iterators.add(SeekableViewsForTest.fromArray(DATA_POINTS_1)); + } + SeekableViewChain chain = new SeekableViewChain(iterators); + + int items = 0; + while (chain.hasNext()) { + chain.next(); + items += 1; + } + + assertEquals(9, items); + } + + @Test + public void testSeek() { + List iterators = new ArrayList<>(); + iterators.add(SeekableViewsForTest.generator( + BASE_TIME, 10000, 5, true + )); + iterators.add(SeekableViewsForTest.generator( + BASE_TIME + 50000, 10000, 5, true + )); + + SeekableViewChain chain = new SeekableViewChain(iterators); + + chain.seek(BASE_TIME + 75000); + + int items = 0; + while (chain.hasNext()) { + chain.next(); + items += 1; + } + + assertEquals(2, items); + } + + @Test + public void testEmptyChain() { + SeekableViewChain chain = new SeekableViewChain(new ArrayList<>()); + assertFalse(chain.hasNext()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testRemoveUnsupported() { + makeChain(1).remove(); + } + + private SeekableViewChain makeChain(int numIterators) { + List iterators = new ArrayList<>(); + for (int i = 0; i < numIterators; i++) { + iterators.add(SeekableViewsForTest.fromArray(DATA_POINTS_1)); + } + return new SeekableViewChain(iterators); + } +} diff --git a/test/core/TestSplitRollupQuery.java b/test/core/TestSplitRollupQuery.java new file mode 100644 index 0000000000..d2006ca9d2 --- /dev/null +++ b/test/core/TestSplitRollupQuery.java @@ -0,0 +1,329 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015-2017 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 com.stumbleupon.async.Deferred; +import net.opentsdb.utils.DateTime; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.util.*; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({DateTime.class, TsdbQuery.class}) +public class TestSplitRollupQuery extends BaseTsdbTest { + private SplitRollupQuery queryUnderTest; + private TsdbQuery rollupQuery; + + @Before + public void beforeLocal() { + rollupQuery = spy(new TsdbQuery(tsdb)); + queryUnderTest = new SplitRollupQuery(tsdb, rollupQuery, Deferred.fromResult(null)); + } + + @Test + public void setStartTime() { + queryUnderTest.setStartTime(42L); + assertEquals(42000L, queryUnderTest.getStartTime()); + assertEquals(42000L, rollupQuery.getStartTime()); + } + + @Test + public void setStartTimeBeyondOriginalEnd() { + TsdbQuery rawQuery = new TsdbQuery(tsdb); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + rollupQuery.setEndTime(41L); + queryUnderTest.setStartTime(42L); + + assertEquals(42000L, queryUnderTest.getStartTime()); + } + + @Test + public void setStartTimeWithoutRollupQuery() { + TsdbQuery rawQuery = new TsdbQuery(tsdb); + Whitebox.setInternalState(queryUnderTest, "rollupQuery", (TsdbQuery)null); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.setStartTime(42L); + + assertEquals(42000L, queryUnderTest.getStartTime()); + } + + @Test + public void setEndTime() { + TsdbQuery rawQuery = new TsdbQuery(tsdb); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + rollupQuery.setStartTime(0); + rollupQuery.setEndTime(21000L); + rawQuery.setStartTime(21000L); + + queryUnderTest.setEndTime(42L); + + assertEquals(42000L, queryUnderTest.getEndTime()); + assertEquals(21000L, rollupQuery.getEndTime()); + assertEquals(42000L, rawQuery.getEndTime()); + } + + @Test(expected = IllegalArgumentException.class) + public void setEndTimeBeforeRawStartTime() { + TsdbQuery rawQuery = new TsdbQuery(tsdb); + rawQuery.setStartTime(DateTime.currentTimeMillis()); + + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.setEndTime(42L); + } + + @Test + public void setDeletePassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.setDelete(true); + + verify(rollupQuery).setDelete(true); + verify(rawQuery).setDelete(true); + } + + @Test + public void setTimeSeriesPassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + RateOptions options = new RateOptions(); + + queryUnderTest.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false, options); + + verify(rollupQuery).setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false, options); + verify(rawQuery).setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false, options); + } + + @Test + public void setTimeSeriesWithoutRateOptionsPassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + verify(rollupQuery).setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + verify(rawQuery).setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + } + + @Test + public void setTimeSeriesWithTSUIDsPassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + List tsuids = Arrays.asList("000001000001000001", "000001000001000002"); + RateOptions options = new RateOptions(); + + queryUnderTest.setTimeSeries(tsuids, Aggregators.SUM, false, options); + + verify(rollupQuery).setTimeSeries(tsuids, Aggregators.SUM, false, options); + verify(rawQuery).setTimeSeries(tsuids, Aggregators.SUM, false, options); + } + + @Test + public void setTimeSeriesWithTSUIDsWithoutRateOptionsPassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + List tsuids = Arrays.asList("000001000001000001", "000001000001000002"); + + queryUnderTest.setTimeSeries(tsuids, Aggregators.SUM, false); + + verify(rollupQuery).setTimeSeries(tsuids, Aggregators.SUM, false); + verify(rawQuery).setTimeSeries(tsuids, Aggregators.SUM, false); + } + + @Test + public void downsamplePassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.downsample(42L, Aggregators.SUM, FillPolicy.ZERO); + + verify(rollupQuery).downsample(42L, Aggregators.SUM, FillPolicy.ZERO); + verify(rawQuery).downsample(42L, Aggregators.SUM, FillPolicy.ZERO); + } + + @Test + public void downsampleWithoutFillPolicyPassesThroughToBoth() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + queryUnderTest.downsample(42L, Aggregators.SUM); + + verify(rollupQuery).downsample(42L, Aggregators.SUM); + verify(rawQuery).downsample(42L, Aggregators.SUM); + } + + @Test(expected = UnsupportedOperationException.class) + public void configureFromQueryThrowsIfForcedRaw() { + queryUnderTest.configureFromQuery(null, 0, true); + } + + @Test + public void configureFromQuerySplitsRollupQuery() { + mockEnableRollupQuerySplitting(); + doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(), anyInt(), any()); + + assertNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); + + rollupQuery.setStartTime(0); + queryUnderTest.configureFromQuery(null, 0, false); + + verify(rollupQuery).split(eq(null), eq(0), anyObject()); + assertNotNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); + } + + @Test + public void configureFromQuerySplitsRollupQueryWithRawOnlyQuery() { + mockEnableRollupQuerySplitting(); + doReturn(true).when(rollupQuery).needsSplitting(); + doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(), anyInt(), any()); + + rollupQuery.setStartTime(DateTime.currentTimeMillis()); + + assertNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); + + queryUnderTest.configureFromQuery(null, 0, false); + + verify(rollupQuery).split(eq(null), eq(0), anyObject()); + assertNotNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); + assertNull(Whitebox.getInternalState(queryUnderTest, "rollupQuery")); + } + + @Test + public void setPercentiles() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + List percentiles = Arrays.asList(50f, 75f, 99f, 99.9f); + + queryUnderTest.setPercentiles(percentiles); + + verify(rollupQuery).setPercentiles(percentiles); + verify(rawQuery).setPercentiles(percentiles); + } + + @Test + public void run() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + doReturn(Deferred.fromResult(new DataPoints[0])).when(rollupQuery).runAsync(); + doReturn(Deferred.fromResult(new DataPoints[0])).when(rawQuery).runAsync(); + + DataPoints[] actualPoints = queryUnderTest.run(); + + verify(rollupQuery).runAsync(); + verify(rawQuery).runAsync(); + + assertEquals(0, actualPoints.length); + } + + @Test + public void runHistogram() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + doReturn(true).when(rollupQuery).isHistogramQuery(); + doReturn(Deferred.fromResult(new DataPoints[0])).when(rollupQuery).runHistogramAsync(); + doReturn(true).when(rawQuery).isHistogramQuery(); + doReturn(Deferred.fromResult(new DataPoints[0])).when(rawQuery).runHistogramAsync(); + + DataPoints[] actualPoints = queryUnderTest.runHistogram(); + + verify(rollupQuery).runHistogramAsync(); + verify(rawQuery).runHistogramAsync(); + + assertEquals(0, actualPoints.length); + } + + @Test + public void runAsyncMergesResults() { + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + Whitebox.setInternalState(queryUnderTest, "rawQuery", rawQuery); + + rollupQuery.setStartTime(0); + rollupQuery.setEndTime(21); + rawQuery.setStartTime(21); + rawQuery.setEndTime(42); + + DataPoints[] rollupDataPoints = new DataPoints[] { + makeSpanGroup("group1"), + makeSpanGroup("group2"), + makeSpanGroup("group3"), + }; + + DataPoints[] rawDataPoints = new DataPoints[] { + makeSpanGroup("group2"), + makeSpanGroup("group3"), + makeSpanGroup("group4"), + makeSpanGroup("group5"), + }; + + doReturn(Deferred.fromResult(rollupDataPoints)).when(rollupQuery).runAsync(); + doReturn(Deferred.fromResult(rawDataPoints)).when(rawQuery).runAsync(); + + DataPoints[] actualPoints = queryUnderTest.run(); + + verify(rollupQuery).runAsync(); + verify(rawQuery).runAsync(); + + List actualGroups = new ArrayList<>(actualPoints.length); + for (DataPoints dataPoints : actualPoints) { + actualGroups.add(new String(((SplitRollupSpanGroup) dataPoints).group())); + } + Collections.sort(actualGroups); + + assertEquals(Arrays.asList("group1", "group2", "group3", "group4", "group5"), actualGroups); + } + + private SpanGroup makeSpanGroup(String group) { + + return new SpanGroup( + tsdb, + 0, + 42, + new ArrayList<>(), + false, + new RateOptions(), + Aggregators.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, + 0, + 42, + 0, + null, + group.getBytes() + ); + } + + private void mockEnableRollupQuerySplitting() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", true); + Whitebox.setInternalState(rollupQuery, "rollup_query", makeRollupQuery()); + } +} diff --git a/test/core/TestSplitRollupSpanGroup.java b/test/core/TestSplitRollupSpanGroup.java new file mode 100644 index 0000000000..30ee198a0c --- /dev/null +++ b/test/core/TestSplitRollupSpanGroup.java @@ -0,0 +1,194 @@ +// 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 net.opentsdb.rollup.RollupSpan; +import net.opentsdb.utils.Config; +import org.hbase.async.Bytes; +import org.hbase.async.HBaseClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, HBaseClient.class, Config.class, SpanGroup.class, + Span.class, RollupSpan.class}) +public class TestSplitRollupSpanGroup { + private final static long START_TS = 1356998400L; + private final static long SPLIT_TS = 1356998500L; + private final static long END_TS = 1356998600L; + + private TSDB tsdb; + private SpanGroup rollupSpanGroup; + private SpanGroup rawSpanGroup; + + @Before + public void before() { + rawSpanGroup = PowerMockito.spy(new SpanGroup(tsdb, START_TS, SPLIT_TS, null, false, Aggregators.SUM, 0, null)); + rollupSpanGroup = PowerMockito.spy(new SpanGroup(tsdb, SPLIT_TS, END_TS, null, false, Aggregators.SUM, 0, null)); + + doAnswer(new SeekableViewAnswer(START_TS, 100, 1, true)).when(rollupSpanGroup).iterator(); + doAnswer(new SeekableViewAnswer(SPLIT_TS, 100, 2, true)).when(rawSpanGroup).iterator(); + + tsdb = PowerMockito.mock(TSDB.class); + } + + @Test + public void testConstructorFiltersNullSpanGroups() { + SplitRollupSpanGroup group = new SplitRollupSpanGroup(null, rawSpanGroup); + final ArrayList actual = Whitebox.getInternalState(group, "spanGroups"); + assertEquals(1, actual.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsWhenAllNull() { + new SplitRollupSpanGroup(null, null); + } + + @Test + public void testMetricName() { + when(rollupSpanGroup.metricName()).thenReturn("metric name"); + assertEquals("metric name", (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).metricName()); + verifyZeroInteractions(rawSpanGroup); + } + + @Test + public void testMetricUID() { + when(rollupSpanGroup.metricUID()).thenReturn(new byte[]{0, 0, 1}); + assertArrayEquals(new byte[]{0, 0, 1}, (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).metricUID()); + verifyZeroInteractions(rawSpanGroup); + } + + @Test + public void testSize() { + when(rollupSpanGroup.size()).thenReturn(5); + when(rawSpanGroup.size()).thenReturn(7); + assertEquals(12, (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).size()); + } + + @Test + public void testAggregatedSize() { + when(rollupSpanGroup.aggregatedSize()).thenReturn(5); + when(rawSpanGroup.aggregatedSize()).thenReturn(7); + assertEquals(12, (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).aggregatedSize()); + } + + @Test + public void testGetTagUids() { + final Bytes.ByteMap uids1 = new Bytes.ByteMap<>(); + uids1.put(new byte[]{0, 0, 1}, new byte[]{0, 0, 2}); + final Bytes.ByteMap uids2 = new Bytes.ByteMap<>(); + uids2.put(new byte[]{0, 0, 3}, new byte[]{0, 0, 4}); + + when(rollupSpanGroup.getTagUids()).thenReturn(uids1); + when(rawSpanGroup.getTagUids()).thenReturn(uids2); + + Bytes.ByteMap actual = (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).getTagUids(); + + assertEquals(2, actual.size()); + assertArrayEquals(new byte[]{0, 0, 1}, actual.firstKey()); + assertArrayEquals(new byte[]{0, 0, 2}, actual.firstEntry().getValue()); + assertArrayEquals(new byte[]{0, 0, 3}, actual.lastKey()); + assertArrayEquals(new byte[]{0, 0, 4}, actual.lastEntry().getValue()); + } + + @Test + public void testGetAggregatedTagUids() { + final List uids1 = new ArrayList<>(); + uids1.add(new byte[]{0, 0, 1}); + final List uids2 = new ArrayList<>(); + uids2.add(new byte[]{0, 0, 2}); + + when(rollupSpanGroup.getAggregatedTagUids()).thenReturn(uids1); + when(rawSpanGroup.getAggregatedTagUids()).thenReturn(uids2); + + List actual = (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).getAggregatedTagUids(); + + assertEquals(2, actual.size()); + assertArrayEquals(new byte[]{0, 0, 1}, actual.get(0)); + assertArrayEquals(new byte[]{0, 0, 2}, actual.get(1)); + } + + @Test + public void testIterator() { + SeekableView iterator = (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).iterator(); + int size = 0; + while (iterator.hasNext()) { + size++; + iterator.next(); + } + assertEquals(3, size); + } + + @Test + public void testTimestamp() { + SplitRollupSpanGroup group = new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup); + assertEquals(START_TS, group.timestamp(0)); + assertEquals(SPLIT_TS, group.timestamp(1)); + assertEquals(END_TS, group.timestamp(2)); + } + + @Test + public void testIsInteger() { + assertTrue(new SplitRollupSpanGroup(rollupSpanGroup).isInteger(0)); + } + + @Test + public void testLongValue() { + assertEquals(0L, new SplitRollupSpanGroup(rollupSpanGroup).longValue(0)); + } + + @Test + public void testDoubleValue() { + doAnswer(new SeekableViewAnswer(START_TS, 100, 1, false)).when(rollupSpanGroup).iterator(); + assertEquals(0, new SplitRollupSpanGroup(rollupSpanGroup).doubleValue(0), 0.0); + } + + @Test + public void testGroup() { + when(rollupSpanGroup.metricName()).thenReturn("group name"); + assertEquals("group name", (new SplitRollupSpanGroup(rollupSpanGroup, rawSpanGroup)).metricName()); + verifyZeroInteractions(rawSpanGroup); + } + + static class SeekableViewAnswer implements Answer { + private final long timestamp; + private final int samplePeriod; + private final int numPoints; + private final boolean isInteger; + + SeekableViewAnswer(long startTimestamp, int samplePeriod, int numPoints, boolean isInteger) { + this.timestamp = startTimestamp; + this.samplePeriod = samplePeriod; + this.numPoints = numPoints; + this.isInteger = isInteger; + } + + @Override + public SeekableView answer(InvocationOnMock ignored) { + return SeekableViewsForTest.generator(timestamp, samplePeriod, numPoints, isInteger); + } + } +} diff --git a/test/core/TestTSDBAddAggregatePoint.java b/test/core/TestTSDBAddAggregatePoint.java index d1c1cfd2bf..60478b88f6 100644 --- a/test/core/TestTSDBAddAggregatePoint.java +++ b/test/core/TestTSDBAddAggregatePoint.java @@ -84,6 +84,7 @@ public void beforeLocal() throws Exception { Whitebox.setInternalState(tsdb, "default_interval", rollup_config.getRollupInterval("1m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); + Whitebox.setInternalState(tsdb, "rollups_split_queries", false); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); Whitebox.setInternalState(tsdb, "raw_agg_tag_value", diff --git a/test/core/TestTSDBAddAggregatePointSalted.java b/test/core/TestTSDBAddAggregatePointSalted.java index 7bc7821af7..6b3642c8cc 100644 --- a/test/core/TestTSDBAddAggregatePointSalted.java +++ b/test/core/TestTSDBAddAggregatePointSalted.java @@ -85,6 +85,7 @@ public void beforeLocal() throws Exception { Whitebox.setInternalState(tsdb, "default_interval", rollup_config.getRollupInterval("1m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); + Whitebox.setInternalState(tsdb, "rollups_split_queries", false); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); Whitebox.setInternalState(tsdb, "raw_agg_tag_value", diff --git a/test/core/TestTSDBTableAvailability.java b/test/core/TestTSDBTableAvailability.java new file mode 100644 index 0000000000..e3d442db69 --- /dev/null +++ b/test/core/TestTSDBTableAvailability.java @@ -0,0 +1,171 @@ +// 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 . +package net.opentsdb.core; + +import java.util.ArrayList; +import java.util.List; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.hbase.async.AtomicIncrementRequest; +import org.hbase.async.GetRequest; +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.PutRequest; +import org.hbase.async.Scanner; +import org.hbase.async.RegionLocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.stumbleupon.async.Deferred; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, HBaseClient.class, + CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, + Scanner.class, AtomicIncrementRequest.class, Const.class, }) +public final class TestTSDBTableAvailability extends BaseTsdbTest { + + /** If locateRegions() throws an exception, availability is NONE */ + @Test + public void failedToGetRegions() throws Exception { + Deferred> d = new Deferred>(); + d.callback(new Exception()); + Deferred> d2 = new Deferred>(); + d2.callback(new Exception()); + TSDB tsdb = new TSDB(mock(HBaseClient.class), config); + when(tsdb.getClient() + .locateRegions(config.getString("tsd.storage.hbase.uid_table")) + ).thenReturn(d); + when(tsdb.getClient() + .locateRegions(config.getString("tsd.storage.hbase.data_table")) + ).thenReturn(d2); + assertEquals(tsdb.checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + } + + /** If locateRegions() returns empty list, availability is NONE */ + @Test + public void noRegions() throws Exception { + TSDB tsdb = new TSDB(mock(HBaseClient.class), config); + String uid_table = config.getString("tsd.storage.hbase.uid_table"); + String data_table = config.getString("tsd.storage.hbase.data_table"); + + Deferred> d = new Deferred>(); + d.callback(new ArrayList()); + when(tsdb.getClient().locateRegions(uid_table)).thenReturn(d); + + Deferred> d2 = new Deferred>(); + d2.callback(new ArrayList()); + when(tsdb.getClient().locateRegions(data_table)).thenReturn(d2); + assertEquals(tsdb.checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + } + + private TSDB createTSDB(ArrayList uid_regions, + ArrayList data_regions) { + TSDB original = new TSDB(mock(HBaseClient.class), config); + TSDB tsdb = PowerMockito.spy(original); + + Deferred> get_results = new Deferred>(); + get_results.callback(uid_regions); + Deferred> get_results2 = new Deferred>(); + get_results2.callback(data_regions); + + PowerMockito.doReturn(get_results).when(tsdb) + .getTableRegionAvailability("tsd.storage.hbase.uid_table"); + PowerMockito.doReturn(get_results2).when(tsdb) + .getTableRegionAvailability("tsd.storage.hbase.data_table"); + + return tsdb; + } + + /* If all returned regions return a result, availability is FULL. */ + @Test + public void allRegionsAvailable() throws Exception { + ArrayList region_availability = new ArrayList(); + region_availability.add(true); + + TSDB tsdb = createTSDB(region_availability, region_availability); + assertEquals(tsdb.checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.FULL); + } + + /* If one out of many regions returned by locateRegions(), one is unavailable, + availability is PARTIAL. */ + @Test + public void partialRegionsAvailable() throws Exception { + ArrayList region_availability = new ArrayList(); + region_availability.add(false); + region_availability.add(true); + + TSDB tsdb = createTSDB(region_availability, region_availability); + assertEquals(tsdb.checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.PARTIAL); + } + + /* If one table returns PARTIAL and the other returns FULL, final result is + PARTIAL. If one table returns NONE and the other returns FULL, final result + is NONE. If one returns PARTIAL and the other NONE, final is result is + NONE. */ + @Test + public void differentRegionsAvailable() throws Exception { + ArrayList full_availability = new ArrayList(); + full_availability.add(true); + full_availability.add(true); + ArrayList partial_availability = new ArrayList(); + partial_availability.add(false); + partial_availability.add(true); + ArrayList no_availability = new ArrayList(); + no_availability.add(false); + + assertEquals(createTSDB(full_availability, partial_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.PARTIAL); + assertEquals(createTSDB(partial_availability, full_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.PARTIAL); + assertEquals(createTSDB(full_availability, no_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + assertEquals(createTSDB(no_availability, full_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + assertEquals(createTSDB(partial_availability, no_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + assertEquals(createTSDB(no_availability, partial_availability) + .checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + } + + +} diff --git a/test/core/TestTSQuery.java b/test/core/TestTSQuery.java index d528eaaf56..40f6622e84 100644 --- a/test/core/TestTSQuery.java +++ b/test/core/TestTSQuery.java @@ -17,12 +17,13 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.HashMap; +import com.stumbleupon.async.Deferred; import net.opentsdb.utils.DateTime; import org.junit.Test; @@ -32,7 +33,7 @@ import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest({ TSQuery.class, DateTime.class }) +@PrepareForTest({ TSQuery.class, TsdbQuery.class, TSDB.class, SplitRollupQuery.class, DateTime.class }) public final class TestTSQuery { @Test @@ -633,6 +634,43 @@ public void testEqualsSame() { TSQuery sub1 = getMetricForValidate(); assertTrue(sub1.equals(sub1)); } + + @Test + public void testSplitsEligibleRollupQuery() throws Exception { + final TSQuery queryUnderTest = getMetricForValidate(); + + TSDB tsdb = PowerMockito.mock(TSDB.class); + TsdbQuery mockTsdbQuery = PowerMockito.mock(TsdbQuery.class); + when(mockTsdbQuery.configureFromQuery(eq(queryUnderTest), anyInt())).thenReturn(Deferred.fromResult(null)); + when(mockTsdbQuery.needsSplitting()).thenReturn(true); + when(tsdb.newQuery()).thenReturn(mockTsdbQuery); + + SplitRollupQuery mockSplitQuery = PowerMockito.mock(SplitRollupQuery.class); + when(mockSplitQuery.configureFromQuery(eq(queryUnderTest), anyInt())).thenReturn(Deferred.fromResult(null)); + + PowerMockito.whenNew(SplitRollupQuery.class).withAnyArguments().thenReturn(mockSplitQuery); + + queryUnderTest.buildQueriesAsync(tsdb); + + verify(mockSplitQuery).configureFromQuery(queryUnderTest, 0); + } + + @Test + public void testDoesNotSplitIneligibleRollupQuery() { + final TSQuery queryUnderTest = getMetricForValidate(); + + TSDB tsdb = PowerMockito.mock(TSDB.class); + TsdbQuery mockTsdbQuery = PowerMockito.mock(TsdbQuery.class); + when(mockTsdbQuery.configureFromQuery(eq(queryUnderTest), anyInt())).thenReturn(Deferred.fromResult(null)); + when(mockTsdbQuery.needsSplitting()).thenReturn(false); + when(tsdb.newQuery()).thenReturn(mockTsdbQuery); + + SplitRollupQuery mockedSplitQuery = PowerMockito.mock(SplitRollupQuery.class); + + queryUnderTest.buildQueriesAsync(tsdb); + + verifyZeroInteractions(mockedSplitQuery); + } /** * Sets up an object with good, common values for testing the validation diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 366e9951b2..0d0ccbde03 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -12,26 +12,23 @@ // see . package net.opentsdb.core; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.util.ArrayList; import java.util.Collections; import java.util.List; +import com.stumbleupon.async.Deferred; import net.opentsdb.core.TsdbQuery.ForTesting; import net.opentsdb.query.QueryLimitOverride; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.query.filter.TagVWildcardFilter; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.DateTime; +import org.jboss.netty.util.internal.ThreadLocalRandom; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +39,13 @@ import com.stumbleupon.async.DeferredGroupException; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.spy; + /** * This class is for unit testing the TsdbQuery class. Pretty much making sure * the various ctors and methods function as expected. For actually running the @@ -49,8 +53,11 @@ * {@link TestTsdbQueryQueries} */ @RunWith(PowerMockRunner.class) -@PrepareForTest({ DateTime.class }) +@PrepareForTest({ DateTime.class, TsdbQuery.class }) public final class TestTsdbQuery extends BaseTsdbTest { + + private static final long ONE_DAY_MS = 24 * 60 * 60 * 1000; + private TsdbQuery query = null; @Before @@ -79,7 +86,7 @@ public void setStartTimeInvalidTooBig() throws Exception { query.setStartTime(17592186044416L); } - @Test (expected = IllegalArgumentException.class) + @Test public void setStartTimeEqualtoEndTime() throws Exception { query.setEndTime(1356998400L); query.setStartTime(1356998400L); @@ -97,6 +104,24 @@ public void setEndTime() throws Exception { assertEquals(1356998400L, query.getEndTime()); } + @Test + public void getScanEndTimeSeconds() { + long now = System.currentTimeMillis() / 1000; + long baseTime = now - (now % Const.MAX_TIMESPAN); + long expectedEndScanTime = baseTime + Const.MAX_TIMESPAN; + + for (int i = 0; i < 3600; i++) { + long sec = baseTime + i; + long ms = sec * 1000 + ThreadLocalRandom.current().nextInt(1000); + query.setEndTime(sec); + Assert.assertEquals("EndTime=" + sec, expectedEndScanTime, + query.getScanEndTimeSeconds()); + query.setEndTime(ms); + Assert.assertEquals("EndTime=" + ms, expectedEndScanTime, + query.getScanEndTimeSeconds()); + } + } + @Test (expected = IllegalStateException.class) public void getStartTimeNotSet() throws Exception { query.getStartTime(); @@ -356,6 +381,26 @@ public void configureFromQueryWithGroupByAndRegularFilters() throws Exception { assertNotNull(ForTesting.getRateOptions(query)); } + @Test + public void configureFromQueryWithForceRaw() throws Exception { + setDataPointStorage(); + mockEnableRollupQuerySplitting(); + + final TSQuery ts_query = getTSQuery(TsdbQuery.ROLLUP_USAGE.ROLLUP_NOFALLBACK); + ts_query.validateAndSetQuery(); + query = spy(new TsdbQuery(tsdb)); + query.configureFromQuery(ts_query, 0, true).joinUninterruptibly(); + + assertFalse(query.isRollupQuery()); + verify(query, never()).transformDownSamplerToRollupQuery(any(), any()); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getFilters(query).size()); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertNotNull(ForTesting.getRateOptions(query)); + } + @Test (expected = IllegalArgumentException.class) public void configureFromQueryNullSubs() throws Exception { final TSQuery ts_query = new TSQuery(); @@ -566,21 +611,180 @@ public void scannerException() throws Exception { } } + @Test + public void needsSplittingReturnsFalseIfDisabled() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", false); + assertFalse(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsFalseIfNotARollupQuery() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", true); + Whitebox.setInternalState(query, "rollup_query", (RollupQuery) null); + assertFalse(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsFalseIfNoSLAConfigured() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", true); + RollupInterval oneHourWithDelay = RollupInterval.builder() + .setTable("fake-rollup-table") + .setPreAggregationTable("fake-preagg-table") + .setInterval("1h") + .setRowSpan("1d") + .setDelaySla(null) + .build(); + RollupQuery rollup_query = new RollupQuery( + oneHourWithDelay, + Aggregators.SUM, + 3600000, + Aggregators.SUM + ); + Whitebox.setInternalState(query, "rollup_query", rollup_query); + + assertTrue(query.isRollupQuery()); + + assertFalse(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsFalseIfNotInBlackoutPeriod() { + mockSystemTime(1356998400000L); + mockEnableRollupQuerySplitting(); + + query.setStartTime(0); + query.setEndTime(1); + + assertTrue(query.isRollupQuery()); + + assertFalse(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsFalseIfQueryEndsWithLastRollupTimestamp() { + mockSystemTime(1356998400000L); + mockEnableRollupQuerySplitting(); + + query.setStartTime(0); + query.setEndTime(query.getRollupQuery().getLastRollupTimestampSeconds() * 1000L); + + assertTrue(query.isRollupQuery()); + + assertFalse(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsTrueIfQueryStartsWithLastRollupTimestamp() { + long mockNowTimestamp = 1356998400000L; + mockSystemTime(mockNowTimestamp); mockEnableRollupQuerySplitting(); + + query.setStartTime(query.getRollupQuery().getLastRollupTimestampSeconds() * 1000L); + + assertTrue(query.isRollupQuery()); + + assertTrue(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsTrueIfInBlackoutPeriod() { + long mockNowTimestamp = 1356998400000L; + mockSystemTime(mockNowTimestamp); + mockEnableRollupQuerySplitting(); + + query.setStartTime(0L); + query.setEndTime(mockNowTimestamp); + + assertTrue(query.isRollupQuery()); + + assertTrue(query.needsSplitting()); + } + + @Test + public void needsSplittingReturnsTrueIfStartAndEndInBlackoutPeriod() { + long mockNowTimestamp = 1356998400000L; + mockSystemTime(mockNowTimestamp); + mockEnableRollupQuerySplitting(); + + int oneHour = 60 * 60 * 1000; + + query.setStartTime(mockNowTimestamp - oneHour); + query.setEndTime(mockNowTimestamp); + + assertTrue(query.isRollupQuery()); + + assertTrue(query.needsSplitting()); + } + + @Test + public void split() { + long mockSystemTime = 1356998400000L; + mockSystemTime(mockSystemTime); + mockEnableRollupQuerySplitting(); + + TSQuery tsQuery = getTSQuery(); + TsdbQuery rawQuery = spy(new TsdbQuery(tsdb)); + + query.setStartTime(mockSystemTime - 7 * ONE_DAY_MS); + + doReturn(Deferred.fromResult(null)).when(rawQuery).configureFromQuery(eq(tsQuery), eq(0), eq(true)); + + query.split(tsQuery, 0, rawQuery); + + verify(rawQuery).configureFromQuery(eq(tsQuery), eq(0), eq(true)); + + assertEquals(mockSystemTime - 7 * ONE_DAY_MS, query.getStartTime()); + assertEquals(mockSystemTime - 2 * ONE_DAY_MS, query.getEndTime()); + assertEquals(mockSystemTime - 2 * ONE_DAY_MS, rawQuery.getStartTime()); + assertEquals(mockSystemTime, rawQuery.getEndTime()); + } + + @Test(expected = IllegalStateException.class) + public void splitThrowsIfNotSplittable() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", false); + + query.split(getTSQuery(), 0, new TsdbQuery(tsdb)); + } + /** @return a simple TSQuery object for testing */ private TSQuery getTSQuery() { + return getTSQuery(null); + } + + private TSQuery getTSQuery(TsdbQuery.ROLLUP_USAGE rollupUsage) { final TSQuery ts_query = new TSQuery(); ts_query.setStart("1356998400"); + final ArrayList sub_queries = new ArrayList(1); + sub_queries.add(getSubQuery(rollupUsage)); + + ts_query.setQueries(sub_queries); + return ts_query; + } + + private TSSubQuery getSubQuery(TsdbQuery.ROLLUP_USAGE rollupUsage) { final TSSubQuery sub_query = new TSSubQuery(); sub_query.setMetric(METRIC_STRING); sub_query.setAggregator("sum"); sub_query.setTags(tags); - final ArrayList sub_queries = new ArrayList(1); - sub_queries.add(sub_query); + if (rollupUsage != null) { + sub_query.setRollupUsage(rollupUsage.name()); + } - ts_query.setQueries(sub_queries); - return ts_query; + return sub_query; + } + + private void mockSystemTime(long newTimestamp) { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(newTimestamp); + PowerMockito.when(DateTime.getDurationUnits(anyString())).thenCallRealMethod(); + PowerMockito.when(DateTime.getDurationInterval(anyString())).thenCallRealMethod(); + PowerMockito.when(DateTime.parseDuration(anyString())).thenCallRealMethod(); + } + + private void mockEnableRollupQuerySplitting() { + Whitebox.setInternalState(tsdb, "rollups_split_queries", true); + Whitebox.setInternalState(query, "rollup_query", makeRollupQuery()); } } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 8acbc61bc4..b6c0359aeb 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -30,9 +30,14 @@ import java.util.List; import java.util.Map; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVLiteralOrFilter; +import net.opentsdb.rollup.RollupConfig; import net.opentsdb.rollup.RollupInterval; import org.hbase.async.Bytes; import org.hbase.async.FilterList; +import org.hbase.async.FuzzyRowFilter; +import org.hbase.async.KeyRegexpFilter; import org.hbase.async.Scanner; import org.junit.Before; import org.junit.Test; @@ -41,6 +46,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; +import com.google.common.collect.Lists; import com.stumbleupon.async.Deferred; import net.opentsdb.storage.MockBase; @@ -1552,6 +1558,75 @@ public void runRegexpNoMatch() throws Exception { verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); assertEquals(0, dps.length); } + @Test + public void runRollupFiltering() throws Exception { + storeLongTimeSeriesSeconds(false, false); + final List families = new ArrayList(); + families.add("t".getBytes(MockBase.ASCII())); + storage.addTable("tsdb-agg".getBytes(), families); + setupGroupByTagValues(); + long start_timestamp = 1559347200L; + + + RollupInterval defaultInterval = RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1m") + .setRowSpan("1h") + .build(); + + RollupInterval rollupInterval = RollupInterval.builder() + .setTable("tsdb-agg") + .setPreAggregationTable("tsdb-agg") + .setInterval("1h") + .setRowSpan("1d") + .build(); + Whitebox.setInternalState(tsdb, "default_interval", defaultInterval); + + RollupConfig rollupConfig = RollupConfig.builder().setAggregationIds(new HashMap() {{ + put("sum", 0); + put("count", 1); + put("min", 2); + put("max", 3); + put("avg", 4); + }}).setIntervals(Arrays.asList(defaultInterval, rollupInterval)).build(); + + Whitebox.setInternalState(tsdb, "default_interval", defaultInterval); + Whitebox.setInternalState(tsdb,"rollup_config", rollupConfig); + + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap() {{ put("host", "web01");}}, false, "1h", "sum", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap() {{ put("host", "web02");}}, false, "1h", "sum", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap() {{ put("host", "web01");}}, false, "1h", "count", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap() {{ put("host", "web02");}}, false, "1h", "count", null); + + TSQuery ts_query = new TSQuery(); + ts_query.setStart("1559343600"); + ts_query.setEnd("1559350800"); + + final TSSubQuery sub = new TSSubQuery(); + sub.setMetric(METRIC_STRING); + sub.setAggregator("sum"); + sub.setDownsample("1h-sum"); + sub.setFilters(Lists.newArrayList(new TagVLiteralOrFilter("host", TAGV_STRING))); + + ts_query.setQueries(Arrays.asList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(1, dps[0].aggregatedSize()); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + long ts = start_timestamp * 1000; + final DataPoint dp = dps[0].iterator().next(); + assertEquals(42, dp.doubleValue(), 0); + assertEquals(ts, dp.timestamp()); + assertEquals(1, dps[0].size()); + } @Test public void runPreAggregate() throws Exception { @@ -1633,6 +1708,10 @@ public void filterExplicitTagsOK() throws Exception { // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { assertTrue(scanner.getFilter() instanceof FilterList); + FilterList filter_list = (FilterList) scanner.getFilter(); + assertEquals(2, filter_list.size()); + assertTrue(filter_list.filters().get(0) instanceof FuzzyRowFilter); + assertTrue(filter_list.filters().get(1) instanceof KeyRegexpFilter); } } @@ -1664,6 +1743,10 @@ public void filterExplicitTagsGroupByOK() throws Exception { // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { assertTrue(scanner.getFilter() instanceof FilterList); + FilterList filter_list = (FilterList) scanner.getFilter(); + assertEquals(2, filter_list.size()); + assertTrue(filter_list.filters().get(0) instanceof FuzzyRowFilter); + assertTrue(filter_list.filters().get(1) instanceof KeyRegexpFilter); } } @@ -1690,6 +1773,10 @@ public void filterExplicitTagsMissing() throws Exception { // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { assertTrue(scanner.getFilter() instanceof FilterList); + FilterList filter_list = (FilterList) scanner.getFilter(); + assertEquals(2, filter_list.size()); + assertTrue(filter_list.filters().get(0) instanceof FuzzyRowFilter); + assertTrue(filter_list.filters().get(1) instanceof KeyRegexpFilter); } } diff --git a/test/query/TestQueryUtil.java b/test/query/TestQueryUtil.java index 03760e8499..ca0d12f25d 100644 --- a/test/query/TestQueryUtil.java +++ b/test/query/TestQueryUtil.java @@ -12,6 +12,8 @@ // see . package net.opentsdb.query; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -19,6 +21,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import net.opentsdb.core.Query; +import net.opentsdb.utils.DateTime; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.FilterList; import org.hbase.async.KeyRegexpFilter; @@ -129,7 +133,7 @@ public void setDataTableScanFilterEnableExplicit() throws Exception { @Test public void setDataTableScanFilterEnableBoth() throws Exception { - when(scanner.getCurrentKey()).thenReturn(new byte[] { 0, 0, 0, 1 }); + when(scanner.getCurrentKey()).thenReturn(new byte[] { 0, 0, 0, 0, 0, 0, 1 }); final ByteMap tags = new ByteMap(); tags.put(new byte[] { 0, 0, 1 }, new byte[][] { new byte[] {0, 0, 1} }); QueryUtil.setDataTableScanFilter( @@ -139,10 +143,26 @@ public void setDataTableScanFilterEnableBoth() throws Exception { true, true, 0); - verify(scanner, times(2)).getCurrentKey(); + verify(scanner, times(3)).getCurrentKey(); // TODO - validate the regex and fuzzy filter verify(scanner, times(1)).setFilter(any(FilterList.class)); verify(scanner, times(1)).setStartKey(any(byte[].class)); verify(scanner, times(1)).setStopKey(any(byte[].class)); } + + @Test + public void timestampComparison() { + long now = DateTime.currentTimeMillis() / 1000L; + assertFalse(QueryUtil.isTimestampAfter(now*1000, now+1)); + assertFalse(QueryUtil.isTimestampAfter(now-1, now*1000L)); + assertFalse(QueryUtil.isTimestampAfter(now-1, now)); + assertFalse(QueryUtil.isTimestampAfter((now-1)*1000L, now*1000L)); + + assertTrue(QueryUtil.isTimestampAfter(now+1, now*1000L)); + assertTrue(QueryUtil.isTimestampAfter(now*1000L, now-1)); + assertTrue(QueryUtil.isTimestampAfter(now, now-1)); + assertTrue(QueryUtil.isTimestampAfter(now*1000L, (now-1)*1000L)); + + assertFalse(QueryUtil.isTimestampAfter(now, now)); + } } diff --git a/test/query/expression/TestFirstDifference.java b/test/query/expression/TestFirstDifference.java new file mode 100644 index 0000000000..031f0e5e34 --- /dev/null +++ b/test/query/expression/TestFirstDifference.java @@ -0,0 +1,348 @@ +// 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.query.expression; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.SeekableViewsForTest; +import net.opentsdb.core.TSQuery; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.stumbleupon.async.Deferred; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({TSQuery.class}) +public class TestFirstDifference { + + private static long START_TIME = 1356998400000L; + private static int INTERVAL = 60000; + private static int NUM_POINTS = 5; + private static String METRIC = "sys.cpu"; + + private TSQuery data_query; + private SeekableView view; + private DataPoints dps; + private DataPoints[] group_bys; + private List query_results; + private List params; + private net.opentsdb.query.expression.FirstDifference func; + + @Before + public void before() throws Exception { + view = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, true, 1, 1); + data_query = mock(TSQuery.class); + when(data_query.startTime()).thenReturn(START_TIME); + when(data_query.endTime()).thenReturn(START_TIME + (INTERVAL * NUM_POINTS)); + + dps = PowerMockito.mock(DataPoints.class); + when(dps.iterator()).thenReturn(view); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); + + group_bys = new DataPoints[]{dps}; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new net.opentsdb.query.expression.FirstDifference(); + } + + @Test + public void evaluatePositiveGroupByLong() throws Exception { + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, true, 10, 1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + long v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(),0.001); + ts += INTERVAL; + v = 1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1; + } + } + + @Test + public void evaluatePositiveGroupByDouble() throws Exception { + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, false, 10, 1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + double v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v =1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1; + } + } + + @Test + public void evaluatePositiveGroupBy1point5Double() throws Exception { + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, false, 10, 1.5); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + double v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v =1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1.5; + } + } + + @Test + public void evaluateFactorNegativeGroupByLong() throws Exception { + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, true, -10, -1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + long v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = -1; + } + } + + @Test + public void evaluateNegativeGroupByDouble() throws Exception { + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, false, -10, -1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + double v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = -1; + } + } + + @Test + public void evaluateNegativeSubQuerySeries() throws Exception { + params.add("1"); + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, true, -10, -1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); + group_bys = new DataPoints[]{dps, dps2}; + query_results.clear(); + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals(METRIC, results[0].metricName()); + assertEquals("sys.mem", results[1].metricName()); + + long ts = START_TIME; + long v = 0; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = 1; + } + ts = START_TIME; + v = 0; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v = -1; + } + } + + @Test(expected = IllegalArgumentException.class) + public void evaluateNullQuery() throws Exception { + params.add("1"); + func.evaluate(null, query_results, params); + } + + @Test + public void evaluateNullResults() throws Exception { + params.add("1"); + final DataPoints[] results = func.evaluate(data_query, null, params); + assertEquals(0, results.length); + } + + @Test + public void evaluateNullParams() throws Exception { + assertNotNull(func.evaluate(data_query, query_results, null)); + } + + @Test + public void evaluateEmptyResults() throws Exception { + params.add("1"); + final DataPoints[] results = func.evaluate(data_query, + Collections.emptyList(), params); + assertEquals(0, results.length); + } + + @Test + public void evaluateEmptyParams() throws Exception { + assertNotNull(func.evaluate(data_query, query_results, null)); + } + + @Test + public void writeStringField() throws Exception { + params.add("1"); + assertEquals("firstDiff(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("firstDiff(null)", func.writeStringField(params, null)); + assertEquals("firstDiff()", func.writeStringField(params, "")); + assertEquals("firstDiff(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} diff --git a/test/rollup/TestRollupConfig.java b/test/rollup/TestRollupConfig.java index 0aef49e861..612c28f010 100644 --- a/test/rollup/TestRollupConfig.java +++ b/test/rollup/TestRollupConfig.java @@ -43,13 +43,16 @@ public class TestRollupConfig { private final static String tsdb_table = "tsdb"; private final static String rollup_table = "tsdb-rollup-10m"; private final static String preagg_table = "tsdb-rollup-agg-10m"; + private final static String rollup_table_1h = "tsdb-rollup-1h"; + private final static String preagg_table_1h = "tsdb-rollup-agg-1h"; private TSDB tsdb; private HBaseClient client; private RollupConfig.Builder builder; private RollupInterval raw; private RollupInterval tenmin; - + private RollupInterval oneHourWithDelay; + @Before public void before() throws Exception { tsdb = PowerMockito.mock(TSDB.class); @@ -70,26 +73,38 @@ public void before() throws Exception { .setInterval("10m") .setRowSpan("1d") .build(); - + + oneHourWithDelay = RollupInterval.builder() + .setTable(rollup_table_1h) + .setPreAggregationTable(preagg_table_1h) + .setInterval("1h") + .setRowSpan("1d") + .setDelaySla("2d") + .build(); + builder = RollupConfig.builder() .addAggregationId("Sum", 0) .addAggregationId("Max", 1) .addInterval(raw) - .addInterval(tenmin); + .addInterval(tenmin) + .addInterval(oneHourWithDelay); } @Test public void ctor() throws Exception { RollupConfig config = builder.build(); - assertEquals(2, config.forward_intervals.size()); + assertEquals(3, config.forward_intervals.size()); assertSame(raw, config.forward_intervals.get("1m")); assertSame(tenmin, config.forward_intervals.get("10m")); - - assertEquals(3, config.reverse_intervals.size()); + assertSame(oneHourWithDelay, config.forward_intervals.get("1h")); + + assertEquals(5, config.reverse_intervals.size()); assertSame(raw, config.reverse_intervals.get(tsdb_table)); assertSame(tenmin, config.reverse_intervals.get(rollup_table)); assertSame(tenmin, config.reverse_intervals.get(preagg_table)); - + assertSame(oneHourWithDelay, config.reverse_intervals.get(rollup_table_1h)); + assertSame(oneHourWithDelay, config.reverse_intervals.get(preagg_table_1h)); + assertEquals(2, config.aggregations_to_ids.size()); assertEquals(2, config.ids_to_aggregations.size()); @@ -102,7 +117,8 @@ public void ctor() throws Exception { // missing aggregations builder = RollupConfig.builder() .addInterval(raw) - .addInterval(tenmin); + .addInterval(tenmin) + .addInterval(oneHourWithDelay); try { builder.build(); fail("Expected IllegalArgumentException"); @@ -113,7 +129,8 @@ public void ctor() throws Exception { .addAggregationId("Sum", 1) .addAggregationId("Max", 1) .addInterval(raw) - .addInterval(tenmin); + .addInterval(tenmin) + .addInterval(oneHourWithDelay); try { builder.build(); fail("Expected IllegalArgumentException"); @@ -124,7 +141,8 @@ public void ctor() throws Exception { .addAggregationId("Sum", 0) .addAggregationId("Max", 128) .addInterval(raw) - .addInterval(tenmin); + .addInterval(tenmin) + .addInterval(oneHourWithDelay); try { builder.build(); fail("Expected IllegalArgumentException"); @@ -175,7 +193,8 @@ public void getRollupIntervalString() throws Exception { assertSame(raw, config.getRollupInterval("1m")); assertSame(tenmin, config.getRollupInterval("10m")); - + assertSame(oneHourWithDelay, config.getRollupInterval("1h")); + try { config.getRollupInterval("5m"); fail("Expected NoSuchRollupForIntervalException"); @@ -199,6 +218,8 @@ public void getRollupIntervalForTable() throws Exception { assertSame(raw, config.getRollupIntervalForTable(tsdb_table)); assertSame(tenmin, config.getRollupIntervalForTable(rollup_table)); assertSame(tenmin, config.getRollupIntervalForTable(preagg_table)); + assertSame(oneHourWithDelay, config.getRollupIntervalForTable(rollup_table_1h)); + assertSame(oneHourWithDelay, config.getRollupIntervalForTable(preagg_table_1h)); try { config.getRollupIntervalForTable("nosuchtable"); @@ -270,5 +291,7 @@ public Deferred answer(InvocationOnMock invocation) verify(client, times(2)).ensureTableExists(tsdb_table.getBytes()); verify(client, times(1)).ensureTableExists(rollup_table.getBytes()); verify(client, times(1)).ensureTableExists(preagg_table.getBytes()); + verify(client, times(1)).ensureTableExists(rollup_table_1h.getBytes()); + verify(client, times(1)).ensureTableExists(preagg_table_1h.getBytes()); } } diff --git a/test/rollup/TestRollupInterval.java b/test/rollup/TestRollupInterval.java index 61dd252a8d..d82674d61c 100644 --- a/test/rollup/TestRollupInterval.java +++ b/test/rollup/TestRollupInterval.java @@ -31,7 +31,7 @@ public class TestRollupInterval { private final static byte[] agg_table = preagg_table.getBytes(CHARSET); @Test - public void ctor1SecondHour() throws Exception { + public void ctor1SecondHourNoSla() throws Exception { final RollupInterval interval = RollupInterval.builder() .setTable(rollup_table) .setPreAggregationTable(preagg_table) @@ -47,16 +47,18 @@ public void ctor1SecondHour() throws Exception { assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + assertEquals(0, interval.getMaximumLag()); } // test odd boundaries @Test - public void ctor7SecondHour() throws Exception { + public void ctor7SecondHourTwoHoursDelay() throws Exception { final RollupInterval interval = RollupInterval.builder() .setTable(rollup_table) .setPreAggregationTable(preagg_table) .setInterval("7s") .setRowSpan("1h") + .setDelaySla("2h") .build(); assertEquals('h', interval.getUnits()); assertEquals("7s", interval.getInterval()); @@ -67,6 +69,7 @@ public void ctor7SecondHour() throws Exception { assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + assertEquals(7200, interval.getMaximumLag()); } @Test @@ -404,7 +407,7 @@ public void ctorUnknownSpan() throws Exception { .build(); } - @Test (expected = NullPointerException.class) + @Test (expected = IllegalArgumentException.class) public void ctorNullInterval() throws Exception { RollupInterval.builder() .setTable(rollup_table) @@ -414,7 +417,7 @@ public void ctorNullInterval() throws Exception { .build(); } - @Test (expected = StringIndexOutOfBoundsException.class) + @Test (expected = IllegalArgumentException.class) public void ctorEmptyInterval() throws Exception { RollupInterval.builder() .setTable(rollup_table) diff --git a/test/rollup/TestRollupQuery.java b/test/rollup/TestRollupQuery.java new file mode 100644 index 0000000000..31c0232dae --- /dev/null +++ b/test/rollup/TestRollupQuery.java @@ -0,0 +1,75 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015-2017 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.rollup; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.utils.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.junit.Assert.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ + DateTime.class +}) +public class TestRollupQuery { + + private static final long MOCK_TIMESTAMP = 1554117071000L; + private static final int ONE_HOUR_SECONDS = 60 * 60; + private static final int ONE_DAY_SECONDS = 24 * ONE_HOUR_SECONDS; + private static final int TWO_DAYS_SECONDS = 2 * ONE_DAY_SECONDS; + + private RollupQuery query; + + @Before + public void before() { + final RollupInterval oneHourWithDelay = RollupInterval.builder() + .setTable("fake-rollup-table") + .setPreAggregationTable("fake-preagg-table") + .setInterval("1h") + .setRowSpan("1d") + .setDelaySla("2d") + .build(); + query = new RollupQuery( + oneHourWithDelay, + Aggregators.SUM, + 3600000, + Aggregators.SUM + ); + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(MOCK_TIMESTAMP); + } + + + @Test + public void testGetLastRollupTimestamp() { + long nowSeconds = MOCK_TIMESTAMP / 1000; + long twoDaysAgo = nowSeconds - TWO_DAYS_SECONDS; + + assertEquals(twoDaysAgo, query.getLastRollupTimestampSeconds()); + } + + @Test + public void testIsInBlackoutPeriod() { + long oneHourAgo = MOCK_TIMESTAMP - ONE_HOUR_SECONDS * 1000; + assertTrue(query.isInBlackoutPeriod(oneHourAgo)); + + long threeDaysAgo = MOCK_TIMESTAMP - 3 * ONE_DAY_SECONDS * 1000; + assertFalse(query.isInBlackoutPeriod(threeDaysAgo)); + } +} diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java index c1b0ff4c0f..536f7fe639 100644 --- a/test/rollup/TestRollupSeq.java +++ b/test/rollup/TestRollupSeq.java @@ -144,6 +144,16 @@ public class TestRollupSeq { Aggregators.COUNT, 3600000, Aggregators.COUNT); + protected static final RollupQuery rollup_query_1h_avg_group_by_sum = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1h") + .setRowSpan("1d") + .build(), + Aggregators.AVG, + 3600000, + Aggregators.SUM); @Before public void before() throws Exception { @@ -215,7 +225,7 @@ public void setRowAlreadySet() throws Exception { rollup_config.getIdForAggregator("SUM"), rollup_query_sum); rs.setRow(kv1); } - + @Test public void addRow() throws Exception { final KeyValue kv1 = getRollupKeyValue(key, 1356998400000L, 4L, @@ -1432,7 +1442,7 @@ public void rollup10mSeekOOB() throws Exception { it.seek(1420075200000L); assertFalse(it.hasNext()); } - + @Test public void rollup10mSeekSeconds() throws Exception { @@ -1811,7 +1821,7 @@ public void rollup10mTimestamp() throws Exception { rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); - + assertEquals(1420070400000L, rs.timestamp(0)); assertEquals(1420071000000L, rs.timestamp(1)); assertEquals(1420071600000L, rs.timestamp(2)); @@ -1826,6 +1836,40 @@ public void rollup10mTimestamp() throws Exception { fail("Excpected an IndexOutOfBoundsException"); } catch (IndexOutOfBoundsException e) { } } + + @Test + public void rollupRowWithDifferentAggregators() throws Exception { + + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg_group_by_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_avg_group_by_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_avg_group_by_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_avg_group_by_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_avg_group_by_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_avg_group_by_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_avg_group_by_sum)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420070400L); + long value = 1; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(value, dp.valueCount()); + ++value; + ts += 60 * 60 * 1000; + } + } private static KeyValue getRollupKeyValue(final byte[] key, final long timestamp, diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 72a6bb5f71..699824283c 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -1673,7 +1673,14 @@ public Deferred>> answer( if (pattern != null) { final String from_bytes = new String(last_row, regex_charset); if (!pattern.matcher(from_bytes).find()) { - continue; + if (filter instanceof FilterList) { + FilterList.Operator op = Whitebox.getInternalState(filter, "op"); + if (op == FilterList.Operator.MUST_PASS_ALL) { + continue; + } + } else { + continue; + } } } diff --git a/test/tsd/TestQueryRpcLastDataPointWhenEnableAppends.java b/test/tsd/TestQueryRpcLastDataPointWhenEnableAppends.java new file mode 100644 index 0000000000..b91d2d1d47 --- /dev/null +++ b/test/tsd/TestQueryRpcLastDataPointWhenEnableAppends.java @@ -0,0 +1,957 @@ +// 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.tsd; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.Query; +import net.opentsdb.core.TSDB; +import net.opentsdb.meta.TestTSUIDQuery; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.nio.charset.Charset; + +import static org.junit.Assert.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, HBaseClient.class, Config.class, HttpQuery.class, + Query.class, Deferred.class, UniqueId.class, DateTime.class, KeyValue.class, + Scanner.class }) +public class TestQueryRpcLastDataPointWhenEnableAppends extends BaseTsdbTest { + private QueryRpc rpc; + + @Before + public void beforeLocal() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", true); + Whitebox.setInternalState(config, "enable_realtime_ts", true); + Whitebox.setInternalState(config, "enable_appends", true); + rpc = new QueryRpc(); + storage = new MockBase(tsdb, client, true, true, true, true); + TestTSUIDQuery.setupStorage(tsdb, storage); + } + + @Test + public void qsMetricMeta() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricMetaScan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricMetaScanResolve() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user&resolve"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + assertTrue(json.contains("\"tags\":{\"host\":\"web02\"}")); + } + + @Test + public void qsMetricMetaScanOneMissing() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertFalse(json.contains("\"value\":\"42\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricMetaScanNoResults() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals("[]", json); + } + + @Test + public void qsMetricMetaScanBackscanZero() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user&back_scan=0"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricBackscanResolved() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "&back_scan=1&resolve=true"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + } + + @Test + public void qsMetricBackscanNoResult() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("[]", json); + } + + @Test + public void qsMetricTwoQueriesBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "×eries=sys.cpu.user{host=web02}&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricTwoQueriesBackscanResolve() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "×eries=sys.cpu.user{host=web02}&back_scan=1&resolve"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + assertTrue(json.contains("\"tags\":{\"host\":\"web02\"}")); + } + + @Test + public void qsMetricTwoQueriesOneMissingBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "×eries=sys.cpu.user{host=web02}&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertFalse(json.contains("\"value\":\"42\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsMetricBackscanMissingTags() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user&back_scan=1"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("Tags")); + } + } + + @Test + public void qsMetricNSUNMetric() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.nice{host=web01}"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such name")); + assertTrue(e.getMessage().contains("metric")); + } + } + + @Test + public void qsMetricNSUNTagk() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{dc=web01}"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such name")); + assertTrue(e.getMessage().contains("tagk")); + } + } + + @Test + public void qsMetricNSUNTagv() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web03}"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such name")); + assertTrue(e.getMessage().contains("tagv")); + } + } + + @Test + public void qsTSUIDMeta() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"value\":\"24\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDMetaCommaSeparated() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001,000001000001000002"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDMetaTwoQueries() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001&tsuids=000001000001000002"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDMetaNoResults() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals("[]", json); + } + + @Test + public void qsTSUIDBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDBackscanNoResult() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("[]", json); + } + + @Test + public void qsTSUIDCommaSeparatedBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001,000001000001000002&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDCommaSeparatedOneMissingBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001,000001000001000002&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"value\":\"24\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDTwoQueriesBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001" + + "&tsuids=000001000001000002&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDTwoQueriesOneMissingBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000001" + + "&tsuids=000001000001000002&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"value\":\"24\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsTSUIDNSUIMetric() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + storage.addColumn(MockBase.stringToBytes("00000350E22700000001000001"), + new byte[] { 0, 0 }, new byte[] { 0x2A }); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000003000001000001&back_scan=1&resolve"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such unique ID")); + assertTrue(e.getMessage().contains("metric")); + } + } + + @Test + public void qsTSUIDNSUITagk() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + storage.addColumn(MockBase.stringToBytes("00000150E22700000004000001"), + new byte[] { 0, 0 }, new byte[] { 0x2A }); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000004000001&back_scan=1&resolve"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such unique ID")); + assertTrue(e.getMessage().contains("tagk")); + } + } + + @Test + public void qsTSUIDNSUITagv() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000003"), + new byte[] { 0, 0 }, new byte[] { 0x2A }); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000003&back_scan=1&resolve"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { + assertTrue(e.getMessage().contains("No such unique ID")); + assertTrue(e.getMessage().contains("tagv")); + } + } + + @Test + public void qsDualMeta() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "&tsuids=000001000001000002"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsDualBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?timeseries=sys.cpu.user{host=web01}" + + "&tsuids=000001000001000002&back_scan=1"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void qsEmpty() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query/last"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { } + } + + @Test + public void postMetricMetaWithTags() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web01\"}}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postMetricMetaWithoutTags() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\"}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postMetricMetaWithoutTagsResolve() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\"}],\"resolveNames\":true}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + assertTrue(json.contains("\"tags\":{\"host\":\"web02\"}")); + } + + @Test + public void postMetricMetaTwoQueries() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web01\"}}," + + "{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web02\"}}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postMetricBackscanWithTags() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web01\"}}],\"backScan\":1}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postTSUIDMeta() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"tsuids\":[\"000001000001000001\"]}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"value\":\"24\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postTSUIDMetaList() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"tsuids\":[\"000001000001000001\"," + + "\"000001000001000002\"]}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postTSUIDMetaResolve() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"tsuids\":[\"000001000001000001\"," + + "\"000001000001000002\"]}],\"resolveNames\":true}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + assertTrue(json.contains("\"tags\":{\"host\":\"web02\"}")); + } + + @Test + public void postTSUIDMetaTwoQueries() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"tsuids\":[\"000001000001000001\"]}," + + "{\"tsuids\":[\"000001000001000002\"]}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postTSUIDBackscan() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1356998400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"tsuids\":[\"000001000001000001\"]}],\"backScan\":1}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1356998400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"value\":\"24\"")); + assertFalse(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postDualMeta() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web01\"}}," + + "{\"tsuids\":[\"000001000001000002\"]}]}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertFalse(json.contains("\"metric\"")); + assertFalse(json.contains("\"tags\"")); + } + + @Test + public void postDualMetaResolve() throws Exception { + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + tags.put("host", "web02"); + tsdb.addPoint("sys.cpu.user", 1388534400L, 24, tags); + + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[{\"metric\":\"sys.cpu.user\",\"tags\":" + + "{\"host\":\"web01\"}}," + + "{\"tsuids\":[\"000001000001000002\"]}]," + + "\"resolveNames\":true}"); + rpc.execute(tsdb, query); + final String json = getContent(query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertTrue(json.contains("\"timestamp\":1388534400000")); + assertTrue(json.contains("\"value\":\"42\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"value\":\"24\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(json.contains("\"metric\":\"sys.cpu.user\"")); + assertTrue(json.contains("\"tags\":{\"host\":\"web01\"}")); + assertTrue(json.contains("\"tags\":{\"host\":\"web02\"}")); + } + + @Test + public void postEmpty() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{\"queries\":[]}"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { } + } + + @Test + public void postEmptyList() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/last", + "{}"); + try { + rpc.execute(tsdb, query); + fail("Expected a BadRequestException"); + } catch (BadRequestException e) { } + } + + /** + * Returns the content of the response buffer + * @param query The query to parse + * @return Some string if we were lucky + */ + private String getContent(final HttpQuery query) { + return query.response().getContent().toString(Charset.forName("UTF-8")); + } +} \ No newline at end of file diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java index 23f5bf319f..ea7158f6dc 100644 --- a/test/tsd/TestRollupRpc.java +++ b/test/tsd/TestRollupRpc.java @@ -100,7 +100,8 @@ public void beforeLocal() throws Exception { storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); storage.addTable("tsdb-agg".getBytes(), families); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); - Whitebox.setInternalState(tsdb, "agg_tag_key", + Whitebox.setInternalState(tsdb, "rollups_split_queries", false); + Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); Whitebox.setInternalState(tsdb, "raw_agg_tag_value", config.getString("tsd.rollups.raw_agg_tag_value")); @@ -850,5 +851,4 @@ public void httpUnknownInterval() throws Exception { validateCounters(0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); } - -} \ No newline at end of file +} diff --git a/test/tsd/TestStatusRpc.java b/test/tsd/TestStatusRpc.java new file mode 100644 index 0000000000..0c4b03120e --- /dev/null +++ b/test/tsd/TestStatusRpc.java @@ -0,0 +1,97 @@ +// 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.tsd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.stumbleupon.async.Deferred; + +import java.nio.charset.Charset; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.utils.Config; + +import org.hbase.async.HBaseClient; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({HttpJsonSerializer.class, TSDB.class, Config.class, + HttpQuery.class, Thread.class, HBaseClient.class }) +public class TestStatusRpc { + private TSDB tsdb; + private HBaseClient client; + private RpcManager.Status rpc; + + @Before + public void before() throws Exception { + rpc = new RpcManager.Status(); + tsdb = NettyMocks.getMockedHTTPTSDB(); + client = mock(HBaseClient.class); + when(tsdb.getClient()).thenReturn(client); + } + + private String getStatus() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/status"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertNotNull(json); + return json; + } + + @Test + public void printStatus() throws Exception { + // Initial status is "startup" + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.NONE)); + assertEquals(getStatus(), "{\"status\":\"startup\"}"); + + // Partial availability: + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.PARTIAL)); + assertEquals(getStatus(), "{\"status\":\"partial\"}"); + + // Full availibility: + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.FULL)); + assertEquals(getStatus(), "{\"status\":\"ok\"}"); + + // No availability (after having seen some in the past): + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.NONE)); + assertEquals(getStatus(), "{\"status\":\"error\"}"); + + // After shutdown status is "shutting-down", regardless of availability: + rpc.shutdown(); + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.NONE)); + assertEquals(getStatus(), "{\"status\":\"shutting-down\"}"); + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.PARTIAL)); + assertEquals(getStatus(), "{\"status\":\"shutting-down\"}"); + when(tsdb.checkNecessaryTablesAvailability()). + thenReturn(Deferred.fromResult(TSDB.TableAvailability.FULL)); + assertEquals(getStatus(), "{\"status\":\"shutting-down\"}"); + } +} diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index 6969b8f565..b90e1e6ceb 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -64,7 +64,7 @@ public final class TestUniqueIdRpc { private UniqueId tag_names = mock(UniqueId.class); private UniqueId tag_values = mock(UniqueId.class); private MockBase storage; - private UniqueIdRpc rpc = new UniqueIdRpc(); + private UniqueIdRpc rpc = new UniqueIdRpc(TSDB.OperationMode.READWRITE); @Before public void before() throws Exception { @@ -89,6 +89,15 @@ public void notImplemented() throws Exception { } // Test /api/uid/assign ---------------------- + + @Test (expected = BadRequestException.class) + public void assignReadOnlyMode() throws Exception { + setupAssign(); + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/assign?metric=sys.cpu.0"); + this.rpc.execute(tsdb, query); + } @Test public void assignQsMetricSingle() throws Exception { @@ -540,6 +549,14 @@ public void renameBadMethod() throws Exception { rpc.execute(tsdb, query); } + @Test (expected = BadRequestException.class) + public void renameReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"metric\":\"sys.cpu.1\",\"name\":\"sys.cpu.2\"}"); + this.rpc.execute(tsdb, query); + } + @Test public void renamePostMetric() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", @@ -686,6 +703,15 @@ public void renameRenameException() throws Exception { } // Teset /api/uid/uidmeta -------------------- + + @Test (expected = BadRequestException.class) + public void uidGetWriteOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.WRITEONLY); + setupUID(); + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/uidmeta?type=metric&uid=000001"); + rpc.execute(tsdb, query); + } @Test public void uidGet() throws Exception { @@ -719,6 +745,15 @@ public void uidGetNSU() throws Exception { "/api/uid/uidmeta?type=metric&uid=000002"); rpc.execute(tsdb, query); } + + @Test (expected = BadRequestException.class) + public void uidPostReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupUID(); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/uidmeta", + "{\"uid\":\"000001\",\"type\":\"metric\",\"displayName\":\"Hello!\"}"); + rpc.execute(tsdb, query); + } @Test public void uidPost() throws Exception { @@ -770,6 +805,15 @@ public void uidPostQS() throws Exception { rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } + + @Test (expected = BadRequestException.class) + public void uidPutReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupUID(); + HttpQuery query = NettyMocks.putQuery(tsdb, "/api/uid/uidmeta", + "{\"uid\":\"000001\",\"type\":\"metric\",\"displayName\":\"Hello!\"}"); + rpc.execute(tsdb, query); + } @Test public void uidPut() throws Exception { @@ -821,6 +865,15 @@ public void uidPutQS() throws Exception { rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } + + @Test (expected = BadRequestException.class) + public void uidDeleteReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupUID(); + HttpQuery query = NettyMocks.deleteQuery(tsdb, "/api/uid/uidmeta", + "{\"uid\":\"000001\",\"type\":\"metric\",\"displayName\":\"Hello!\"}"); + rpc.execute(tsdb, query); + } @Test public void uidDelete() throws Exception { @@ -857,6 +910,15 @@ public void uidDeleteQS() throws Exception { } // Test /api/uid/tsmeta ---------------------- + + @Test (expected = BadRequestException.class) + public void tsuidGetWriteOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.WRITEONLY); + setupTSUID(); + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/tsmeta?tsuid=000001000001000001"); + rpc.execute(tsdb, query); + } @Test public void tsuidGet() throws Exception { @@ -943,6 +1005,15 @@ public void tsuidGetMissingTSUID() throws Exception { rpc.execute(tsdb, query); } + @Test (expected = BadRequestException.class) + public void tsuidPostReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupTSUID(); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/tsmeta", + "{\"tsuid\":\"000001000001000001\", \"displayName\":\"Hello World\"}"); + rpc.execute(tsdb, query); + } + @Test public void tsuidPost() throws Exception { setupTSUID(); @@ -989,6 +1060,15 @@ public void tsuidPostQSNoTSUID() throws Exception { "/api/uid/tsmeta?display_name=42&method_override=post"); rpc.execute(tsdb, query); } + + @Test (expected = BadRequestException.class) + public void tsuidPutReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupTSUID(); + HttpQuery query = NettyMocks.putQuery(tsdb, "/api/uid/tsmeta", + "{\"tsuid\":\"000001000001000001\", \"displayName\":\"Hello World\"}"); + rpc.execute(tsdb, query); + } @Test public void tsuidPut() throws Exception { @@ -1036,6 +1116,15 @@ public void tsuidPutQSNoTSUID() throws Exception { "/api/uid/tsmeta?display_name=42&method_override=put"); rpc.execute(tsdb, query); } + + @Test (expected = BadRequestException.class) + public void tsuidDeleteReadOnlyMode() throws Exception { + rpc = new UniqueIdRpc(TSDB.OperationMode.READONLY); + setupTSUID(); + HttpQuery query = NettyMocks.deleteQuery(tsdb, "/api/uid/tsmeta", + "{\"tsuid\":\"000001000001000001\", \"displayName\":\"Hello World\"}"); + rpc.execute(tsdb, query); + } @Test public void tsuidDelete() throws Exception { diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 1b54e602ca..cee9866487 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -22,6 +22,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.Locale; import java.util.TimeZone; import org.junit.Before; @@ -413,6 +414,11 @@ public void getDurationUnitsNull() { public void getDurationUnitsEmpty() { DateTime.getDurationUnits(""); } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIsNull() { + DateTime.getDurationUnits(null); + } @Test public void getDurationInterval() { @@ -782,6 +788,9 @@ public void previousIntervalDays() { @Test public void previousIntervalWeeks() { + // Test assumes Sunday is first day of week. + Locale.setDefault(Locale.US); + // interval 1 DST_TS starts on 13th of Dec, NON starts on the 10th of May assertEquals(1449964800000L, DateTime.previousInterval(DST_TS, 1, Calendar.DAY_OF_WEEK).getTimeInMillis()); diff --git a/third_party/apache/commons-math3-3.4.1.jar.md5 b/third_party/apache/commons-math3-3.4.1.jar.md5 index 9939ae9c15..a26a157f84 100644 --- a/third_party/apache/commons-math3-3.4.1.jar.md5 +++ b/third_party/apache/commons-math3-3.4.1.jar.md5 @@ -1 +1 @@ -14a218d0ee57907dd2c7ef944b6c0afd +14a218d0ee57907dd2c7ef944b6c0afd \ No newline at end of file diff --git a/third_party/apache/include.mk b/third_party/apache/include.mk index a97b81a366..5f4fb82d7b 100644 --- a/third_party/apache/include.mk +++ b/third_party/apache/include.mk @@ -24,7 +24,7 @@ APACHE_MATH_VERSION := 3.4.1 APACHE_MATH := third_party/apache/commons-math3-$(APACHE_MATH_VERSION).jar -APACHE_MATH_BASE_URL := http://repo1.maven.org/maven2/org/apache/commons/commons-math3/$(APACHE_MATH_VERSION) +APACHE_MATH_BASE_URL := https://repo1.maven.org/maven2/org/apache/commons/commons-math3/$(APACHE_MATH_VERSION) $(APACHE_MATH): $(APACHE_MATH).md5 set dummy "$(APACHE_MATH_BASE_URL)" "$(APACHE_MATH)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index d1b3c84d45..697e6cefe3 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -15,7 +15,7 @@ ASYNCBIGTABLE_VERSION := 0.3.1-20170903.031804-2 ASYNCBIGTABLE := third_party/asyncbigtable/asyncbigtable-$(ASYNCBIGTABLE_VERSION)-jar-with-dependencies.jar -ASYNCBIGTABLE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/com/pythian/opentsdb/asyncbigtable/0.3.1-SNAPSHOT/ +ASYNCBIGTABLE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/com/pythian/opentsdb/asyncbigtable/0.3.1-SNAPSHOT $(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/guava/include.mk b/third_party/guava/include.mk index b686739a88..53ba9e2638 100644 --- a/third_party/guava/include.mk +++ b/third_party/guava/include.mk @@ -25,7 +25,7 @@ GUAVA_VERSION := 18.0 GUAVA := third_party/guava/guava-$(GUAVA_VERSION).jar -GUAVA_BASE_URL := http://central.maven.org/maven2/com/google/guava/guava/$(GUAVA_VERSION) +GUAVA_BASE_URL := https://repo1.maven.org/maven2/com/google/guava/guava/$(GUAVA_VERSION) $(GUAVA): $(GUAVA).md5 set dummy "$(GUAVA_BASE_URL)" "$(GUAVA)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/gwt/include.mk b/third_party/gwt/include.mk index 83e876026c..27f8699f3c 100644 --- a/third_party/gwt/include.mk +++ b/third_party/gwt/include.mk @@ -17,7 +17,7 @@ GWT_VERSION := 2.6.0 GWT_DEV_VERSION := $(GWT_VERSION) GWT_DEV := third_party/gwt/gwt-dev-$(GWT_DEV_VERSION).jar -GWT_DEV_BASE_URL := http://central.maven.org/maven2/com/google/gwt/gwt-dev/$(GWT_DEV_VERSION) +GWT_DEV_BASE_URL := https://repo1.maven.org/maven2/com/google/gwt/gwt-dev/$(GWT_DEV_VERSION) $(GWT_DEV): $(GWT_DEV).md5 set dummy "$(GWT_DEV_BASE_URL)" "$(GWT_DEV)"; shift; $(FETCH_DEPENDENCY) @@ -25,14 +25,14 @@ $(GWT_DEV): $(GWT_DEV).md5 GWT_USER_VERSION := $(GWT_VERSION) GWT_USER := third_party/gwt/gwt-user-$(GWT_USER_VERSION).jar -GWT_USER_BASE_URL := http://central.maven.org/maven2/com/google/gwt/gwt-user/$(GWT_USER_VERSION) +GWT_USER_BASE_URL := https://repo1.maven.org/maven2/com/google/gwt/gwt-user/$(GWT_USER_VERSION) $(GWT_USER): $(GWT_USER).md5 set dummy "$(GWT_USER_BASE_URL)" "$(GWT_USER)"; shift; $(FETCH_DEPENDENCY) GWT_THEME_VERSION := 1.0.0 GWT_THEME := third_party/gwt/opentsdb-gwt-theme-$(GWT_THEME_VERSION).jar -GWT_THEME_BASE_URL := http://central.maven.org/maven2/net/opentsdb/opentsdb-gwt-theme/$(GWT_THEME_VERSION) +GWT_THEME_BASE_URL := https://repo1.maven.org/maven2/net/opentsdb/opentsdb-gwt-theme/$(GWT_THEME_VERSION) $(GWT_THEME): $(GWT_THEME).md5 set dummy "$(GWT_THEME_BASE_URL)" "$(GWT_THEME)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/hamcrest/include.mk b/third_party/hamcrest/include.mk index b643b87743..c4e8b2151b 100644 --- a/third_party/hamcrest/include.mk +++ b/third_party/hamcrest/include.mk @@ -15,7 +15,7 @@ HAMCREST_VERSION := 1.3 HAMCREST := third_party/hamcrest/hamcrest-core-$(HAMCREST_VERSION).jar -HAMCREST_BASE_URL := http://central.maven.org/maven2/org/hamcrest/hamcrest-core/$(HAMCREST_VERSION) +HAMCREST_BASE_URL := https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/$(HAMCREST_VERSION) $(HAMCREST): $(HAMCREST).md5 set dummy "$(HAMCREST_BASE_URL)" "$(HAMCREST)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index cbda6eec6d..01a5407ff7 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -15,7 +15,7 @@ ASYNCHBASE_VERSION := 1.8.2 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) +ASYNCHBASE_BASE_URL := https://repo1.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/jackson/include.mk b/third_party/jackson/include.mk index d6e77b5957..06a62b68e1 100644 --- a/third_party/jackson/include.mk +++ b/third_party/jackson/include.mk @@ -13,25 +13,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -JACKSON_VERSION := 2.9.5 +JACKSON_VERSION := 2.9.10 JACKSON_ANNOTATIONS_VERSION = $(JACKSON_VERSION) JACKSON_ANNOTATIONS := third_party/jackson/jackson-annotations-$(JACKSON_ANNOTATIONS_VERSION).jar -JACKSON_ANNOTATIONS_BASE_URL := http://central.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/$(JACKSON_VERSION) +JACKSON_ANNOTATIONS_BASE_URL := https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/$(JACKSON_VERSION) $(JACKSON_ANNOTATIONS): $(JACKSON_ANNOTATIONS).md5 set dummy "$(JACKSON_ANNOTATIONS_BASE_URL)" "$(JACKSON_ANNOTATIONS)"; shift; $(FETCH_DEPENDENCY) JACKSON_CORE_VERSION = $(JACKSON_VERSION) JACKSON_CORE := third_party/jackson/jackson-core-$(JACKSON_CORE_VERSION).jar -JACKSON_CORE_BASE_URL := http://central.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/$(JACKSON_VERSION) +JACKSON_CORE_BASE_URL := https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/$(JACKSON_VERSION) $(JACKSON_CORE): $(JACKSON_CORE).md5 set dummy "$(JACKSON_CORE_BASE_URL)" "$(JACKSON_CORE)"; shift; $(FETCH_DEPENDENCY) JACKSON_DATABIND_VERSION = $(JACKSON_VERSION) JACKSON_DATABIND := third_party/jackson/jackson-databind-$(JACKSON_DATABIND_VERSION).jar -JACKSON_DATABIND_BASE_URL := http://central.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/$(JACKSON_VERSION) +JACKSON_DATABIND_BASE_URL := https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/$(JACKSON_VERSION) $(JACKSON_DATABIND): $(JACKSON_DATABIND).md5 set dummy "$(JACKSON_DATABIND_BASE_URL)" "$(JACKSON_DATABIND)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/jackson/jackson-annotations-2.9.10.jar.md5 b/third_party/jackson/jackson-annotations-2.9.10.jar.md5 new file mode 100644 index 0000000000..78c26afb02 --- /dev/null +++ b/third_party/jackson/jackson-annotations-2.9.10.jar.md5 @@ -0,0 +1 @@ +26c2b6f7bc704ccadc64c83995e0ff7f \ No newline at end of file diff --git a/third_party/jackson/jackson-core-2.9.10.jar.md5 b/third_party/jackson/jackson-core-2.9.10.jar.md5 new file mode 100644 index 0000000000..89a33946ea --- /dev/null +++ b/third_party/jackson/jackson-core-2.9.10.jar.md5 @@ -0,0 +1 @@ +d62d9b1d1d83dd553e678bc8fce8f809 \ No newline at end of file diff --git a/third_party/jackson/jackson-databind-2.9.10.jar.md5 b/third_party/jackson/jackson-databind-2.9.10.jar.md5 new file mode 100644 index 0000000000..a8536777d9 --- /dev/null +++ b/third_party/jackson/jackson-databind-2.9.10.jar.md5 @@ -0,0 +1 @@ +ff43d79c624b0f7d465542fee6648474 \ No newline at end of file diff --git a/third_party/javacc/include.mk b/third_party/javacc/include.mk index 2c7f29785a..aa022bbe72 100644 --- a/third_party/javacc/include.mk +++ b/third_party/javacc/include.mk @@ -15,7 +15,7 @@ JAVACC_VERSION := 6.1.2 JAVACC := third_party/javacc/javacc-$(JAVACC_VERSION).jar -JAVACC_BASE_URL := http://central.maven.org/maven2/net/java/dev/javacc/javacc/$(JAVACC_VERSION) +JAVACC_BASE_URL := https://repo1.maven.org/maven2/net/java/dev/javacc/javacc/$(JAVACC_VERSION) $(JAVACC): $(JAVACC).md5 set dummy "$(JAVACC_BASE_URL)" "$(JAVACC)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/javassist/include.mk b/third_party/javassist/include.mk index e639914f79..c667a3e5c0 100644 --- a/third_party/javassist/include.mk +++ b/third_party/javassist/include.mk @@ -25,7 +25,7 @@ JAVASSIST_VERSION := 3.21.0-GA JAVASSIST := third_party/javassist/javassist-$(JAVASSIST_VERSION).jar -JAVASSIST_BASE_URL := http://central.maven.org/maven2/org/javassist/javassist/$(JAVASSIST_VERSION) +JAVASSIST_BASE_URL := https://repo1.maven.org/maven2/org/javassist/javassist/$(JAVASSIST_VERSION) $(JAVASSIST): $(JAVASSIST).md5 set dummy "$(JAVASSIST_BASE_URL)" "$(JAVASSIST)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/jexl/include.mk b/third_party/jexl/include.mk index b78ce1e8ee..b8ac41ff1a 100644 --- a/third_party/jexl/include.mk +++ b/third_party/jexl/include.mk @@ -15,7 +15,7 @@ JEXL_VERSION := 2.1.1 JEXL := third_party/jexl/commons-jexl-$(JEXL_VERSION).jar -JEXL_BASE_URL := http://central.maven.org/maven2/org/apache/commons/commons-jexl/$(JEXL_VERSION) +JEXL_BASE_URL := https://repo1.maven.org/maven2/org/apache/commons/commons-jexl/$(JEXL_VERSION) $(JEXL): $(JEXL).md5 set dummy "$(JEXL_BASE_URL)" "$(JEXL)"; shift; $(FETCH_DEPENDENCY) @@ -25,9 +25,9 @@ THIRD_PARTY += $(JEXL) # In here as Jexl depends on it and no one else (for now, I hope) COMMONS_LOGGING_VERSION := 1.1.1 COMMONS_LOGGING := third_party/jexl/commons-logging-$(COMMONS_LOGGING_VERSION).jar -COMMONS_LOGGING_BASE_URL := http://central.maven.org/maven2/commons-logging/commons-logging/$(COMMONS_LOGGING_VERSION) +COMMONS_LOGGING_BASE_URL := https://repo1.maven.org/maven2/commons-logging/commons-logging/$(COMMONS_LOGGING_VERSION) $(COMMONS_LOGGING): $(COMMONS_LOGGING).md5 set dummy "$(COMMONS_LOGGING_BASE_URL)" "$(COMMONS_LOGGING)"; shift; $(FETCH_DEPENDENCY) -THIRD_PARTY += $(COMMONS_LOGGING) \ No newline at end of file +THIRD_PARTY += $(COMMONS_LOGGING) diff --git a/third_party/jgrapht/include.mk b/third_party/jgrapht/include.mk index 11647e3bcc..cce2048438 100644 --- a/third_party/jgrapht/include.mk +++ b/third_party/jgrapht/include.mk @@ -15,7 +15,7 @@ JGRAPHT_VERSION := 0.9.1 JGRAPHT := third_party/jgrapht/jgrapht-core-$(JGRAPHT_VERSION).jar -JGRAPHT_BASE_URL := http://central.maven.org/maven2/org/jgrapht/jgrapht-core/$(JGRAPHT_VERSION) +JGRAPHT_BASE_URL := https://repo1.maven.org/maven2/org/jgrapht/jgrapht-core/$(JGRAPHT_VERSION) $(JGRAPHT): $(JGRAPHT).md5 set dummy "$(JGRAPHT_BASE_URL)" "$(JGRAPHT)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/junit/include.mk b/third_party/junit/include.mk index 846953d64f..7f97540ace 100644 --- a/third_party/junit/include.mk +++ b/third_party/junit/include.mk @@ -15,7 +15,7 @@ JUNIT_VERSION := 4.11 JUNIT := third_party/junit/junit-$(JUNIT_VERSION).jar -JUNIT_BASE_URL := http://central.maven.org/maven2/junit/junit/$(JUNIT_VERSION) +JUNIT_BASE_URL := https://repo1.maven.org/maven2/junit/junit/$(JUNIT_VERSION) $(JUNIT): $(JUNIT).md5 set dummy "$(JUNIT_BASE_URL)" "$(JUNIT)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/kryo/asm-4.0.jar.md5.1 b/third_party/kryo/asm-4.0.jar.md5.1 new file mode 100644 index 0000000000..7a92e07c69 --- /dev/null +++ b/third_party/kryo/asm-4.0.jar.md5.1 @@ -0,0 +1 @@ +322d8f88c5111af612df838c0191cd7e \ No newline at end of file diff --git a/third_party/kryo/include.mk b/third_party/kryo/include.mk index d9343cc4fc..72694248f1 100644 --- a/third_party/kryo/include.mk +++ b/third_party/kryo/include.mk @@ -13,32 +13,32 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -KRYO_VERSION := 2.21.1 +KRYO_VERSION := 3.0.0 KRYO := third_party/kryo/kryo-$(KRYO_VERSION).jar -KRYO_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/kryo/kryo/$(KRYO_VERSION) +KRYO_BASE_URL := https://repo1.maven.org/maven2/com/esotericsoftware/kryo/$(KRYO_VERSION) $(KRYO): $(KRYO).md5 set dummy "$(KRYO_BASE_URL)" "$(KRYO)"; shift; $(FETCH_DEPENDENCY) -REFLECTASM_VERSION := 1.07 +REFLECTASM_VERSION := 1.10.0 REFLECTASM := third_party/kryo/reflectasm-$(REFLECTASM_VERSION)-shaded.jar -REFLECTASM_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/reflectasm/reflectasm/$(REFLECTASM_VERSION) +REFLECTASM_BASE_URL :=https://repo1.maven.org/maven2/com/esotericsoftware/reflectasm/$(REFLECTASM_VERSION) $(REFLECTASM): $(REFLECTASM).md5 set dummy "$(REFLECTASM_BASE_URL)" "$(REFLECTASM)"; shift; $(FETCH_DEPENDENCY) ASM_VERSION := 4.0 ASM := third_party/kryo/asm-$(ASM_VERSION).jar -ASM_BASE_URL := http://central.maven.org/maven2/org/ow2/asm/asm/$(ASM_VERSION) +ASM_BASE_URL := https://repo1.maven.org/maven2/org/ow2/asm/asm/$(ASM_VERSION) $(ASM): $(ASM).md5 set dummy "$(ASM_BASE_URL)" "$(ASM)"; shift; $(FETCH_DEPENDENCY) -MINLOG_VERSION := 1.2 +MINLOG_VERSION := 1.3 MINLOG := third_party/kryo/minlog-$(MINLOG_VERSION).jar -MINLOG_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/minlog/minlog/$(MINLOG_VERSION) +MINLOG_BASE_URL := https://repo1.maven.org/maven2/com/esotericsoftware/minlog/$(MINLOG_VERSION) $(MINLOG): $(MINLOG).md5 set dummy "$(MINLOG_BASE_URL)" "$(MINLOG)"; shift; $(FETCH_DEPENDENCY) -THIRD_PARTY += $(KRYO) $(REFLECTASM) $(ASM) $(MINLOG) \ No newline at end of file +THIRD_PARTY += $(KRYO) $(REFLECTASM) $(ASM) $(MINLOG) diff --git a/third_party/kryo/kryo-3.0.0.jar.md5 b/third_party/kryo/kryo-3.0.0.jar.md5 new file mode 100644 index 0000000000..28ce336010 --- /dev/null +++ b/third_party/kryo/kryo-3.0.0.jar.md5 @@ -0,0 +1 @@ +720adc0fa9b1ebfa789c6ceda3ffa990 \ No newline at end of file diff --git a/third_party/kryo/kryo-4.0.0.jar.md5 b/third_party/kryo/kryo-4.0.0.jar.md5 new file mode 100644 index 0000000000..ba8eac67ec --- /dev/null +++ b/third_party/kryo/kryo-4.0.0.jar.md5 @@ -0,0 +1 @@ +e817940f2e49280c3e5ad063f38e7884 \ No newline at end of file diff --git a/third_party/kryo/minlog-1.3.jar.md5 b/third_party/kryo/minlog-1.3.jar.md5 new file mode 100644 index 0000000000..da08b47e84 --- /dev/null +++ b/third_party/kryo/minlog-1.3.jar.md5 @@ -0,0 +1 @@ +b4e9b84eaea9750fe58ac3e196c7ed9b \ No newline at end of file diff --git a/third_party/kryo/reflectasm-1.10.0-shaded.jar.md5 b/third_party/kryo/reflectasm-1.10.0-shaded.jar.md5 new file mode 100644 index 0000000000..9f1c813651 --- /dev/null +++ b/third_party/kryo/reflectasm-1.10.0-shaded.jar.md5 @@ -0,0 +1 @@ +779472dd799c5e9b1469e14b13c73061 \ No newline at end of file diff --git a/third_party/logback/include.mk b/third_party/logback/include.mk index de025c59ff..078f6eadfd 100644 --- a/third_party/logback/include.mk +++ b/third_party/logback/include.mk @@ -13,12 +13,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -http://central.maven.org/maven2/ch/qos/logback/logback-classic/1.0.13/logback-classic-1.0.13.jar +https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.0.13/logback-classic-1.0.13.jar LOGBACK_VERSION := 1.0.13 LOGBACK_CLASSIC_VERSION := $(LOGBACK_VERSION) LOGBACK_CLASSIC := third_party/logback/logback-classic-$(LOGBACK_CLASSIC_VERSION).jar -LOGBACK_CLASSIC_BASE_URL := http://central.maven.org/maven2/ch/qos/logback/logback-classic/$(LOGBACK_VERSION) +LOGBACK_CLASSIC_BASE_URL := https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/$(LOGBACK_VERSION) $(LOGBACK_CLASSIC): $(LOGBACK_CLASSIC).md5 set dummy "$(LOGBACK_CLASSIC_BASE_URL)" "$(LOGBACK_CLASSIC)"; shift; $(FETCH_DEPENDENCY) @@ -26,7 +26,7 @@ $(LOGBACK_CLASSIC): $(LOGBACK_CLASSIC).md5 LOGBACK_CORE_VERSION := $(LOGBACK_VERSION) LOGBACK_CORE := third_party/logback/logback-core-$(LOGBACK_CORE_VERSION).jar -LOGBACK_CORE_BASE_URL := http://central.maven.org/maven2/ch/qos/logback/logback-core/$(LOGBACK_VERSION) +LOGBACK_CORE_BASE_URL := https://repo1.maven.org/maven2/ch/qos/logback/logback-core/$(LOGBACK_VERSION) $(LOGBACK_CORE): $(LOGBACK_CORE).md5 set dummy "$(LOGBACK_CORE_BASE_URL)" "$(LOGBACK_CORE)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/mockito/include.mk b/third_party/mockito/include.mk index aa99f81071..8f2e8591be 100644 --- a/third_party/mockito/include.mk +++ b/third_party/mockito/include.mk @@ -15,7 +15,7 @@ MOCKITO_VERSION := 1.9.5 MOCKITO := third_party/mockito/mockito-core-$(MOCKITO_VERSION).jar -MOCKITO_BASE_URL := http://central.maven.org/maven2/org/mockito/mockito-core/$(MOCKITO_VERSION) +MOCKITO_BASE_URL := https://repo1.maven.org/maven2/org/mockito/mockito-core/$(MOCKITO_VERSION) $(MOCKITO): $(MOCKITO).md5 set dummy "$(MOCKITO_BASE_URL)" "$(MOCKITO)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/netty/include.mk b/third_party/netty/include.mk index a0ea78407a..875ff72251 100644 --- a/third_party/netty/include.mk +++ b/third_party/netty/include.mk @@ -26,7 +26,7 @@ NETTY_MAJOR_VERSION = 3.10 NETTY_VERSION := 3.10.6.Final NETTY := third_party/netty/netty-$(NETTY_VERSION).jar -NETTY_BASE_URL := http://central.maven.org/maven2/io/netty/netty/$(NETTY_VERSION) +NETTY_BASE_URL := https://repo1.maven.org/maven2/io/netty/netty/$(NETTY_VERSION) $(NETTY): $(NETTY).md5 set dummy "$(NETTY_BASE_URL)" "$(NETTY)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/objenesis/include.mk b/third_party/objenesis/include.mk index 51396bf59c..53b6b4585f 100644 --- a/third_party/objenesis/include.mk +++ b/third_party/objenesis/include.mk @@ -15,7 +15,7 @@ OBJENESIS_VERSION := 1.3 OBJENESIS := third_party/objenesis/objenesis-$(OBJENESIS_VERSION).jar -OBJENESIS_BASE_URL := http://central.maven.org/maven2/org/objenesis/objenesis/$(OBJENESIS_VERSION) +OBJENESIS_BASE_URL := https://repo1.maven.org/maven2/org/objenesis/objenesis/$(OBJENESIS_VERSION) $(OBJENESIS): $(OBJENESIS).md5 set dummy "$(OBJENESIS_BASE_URL)" "$(OBJENESIS)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/powermock/include.mk b/third_party/powermock/include.mk index 234235e355..9b3d4081c9 100644 --- a/third_party/powermock/include.mk +++ b/third_party/powermock/include.mk @@ -25,7 +25,7 @@ POWERMOCK_MOCKITO_VERSION := 1.5.4 POWERMOCK_MOCKITO := third_party/powermock/powermock-mockito-release-full-$(POWERMOCK_MOCKITO_VERSION)-full.jar -POWERMOCK_MOCKITO_BASE_URL := http://central.maven.org/maven2/org/powermock/powermock-mockito-release-full/$(POWERMOCK_MOCKITO_VERSION) +POWERMOCK_MOCKITO_BASE_URL := https://repo1.maven.org/maven2/org/powermock/powermock-mockito-release-full/$(POWERMOCK_MOCKITO_VERSION) $(POWERMOCK_MOCKITO): $(POWERMOCK_MOCKITO).md5 set dummy "$(POWERMOCK_MOCKITO_BASE_URL)" "$(POWERMOCK_MOCKITO)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/protobuf/include.mk b/third_party/protobuf/include.mk index d7a9a01311..c521e130da 100644 --- a/third_party/protobuf/include.mk +++ b/third_party/protobuf/include.mk @@ -15,7 +15,7 @@ PROTOBUF_VERSION := 2.5.0 PROTOBUF := third_party/protobuf/protobuf-java-$(PROTOBUF_VERSION).jar -PROTOBUF_BASE_URL := http://central.maven.org/maven2/com/google/protobuf/protobuf-java/$(PROTOBUF_VERSION) +PROTOBUF_BASE_URL := https://repo1.maven.org/maven2/com/google/protobuf/protobuf-java/$(PROTOBUF_VERSION) $(PROTOBUF): $(PROTOBUF).md5 set dummy "$(PROTOBUF_BASE_URL)" "$(PROTOBUF)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/slf4j/include.mk b/third_party/slf4j/include.mk index 48d686d80a..49743b486a 100644 --- a/third_party/slf4j/include.mk +++ b/third_party/slf4j/include.mk @@ -18,7 +18,7 @@ SLF4J_VERSION = 1.7.7 LOG4J_OVER_SLF4J_VERSION := $(SLF4J_VERSION) LOG4J_OVER_SLF4J := third_party/slf4j/log4j-over-slf4j-$(LOG4J_OVER_SLF4J_VERSION).jar -LOG4J_OVER_SLF4J_BASE_URL := http://central.maven.org/maven2/org/slf4j/log4j-over-slf4j/$(LOG4J_OVER_SLF4J_VERSION) +LOG4J_OVER_SLF4J_BASE_URL := https://repo1.maven.org/maven2/org/slf4j/log4j-over-slf4j/$(LOG4J_OVER_SLF4J_VERSION) $(LOG4J_OVER_SLF4J): $(LOG4J_OVER_SLF4J).md5 set dummy "$(LOG4J_OVER_SLF4J_BASE_URL)" "$(LOG4J_OVER_SLF4J)"; shift; $(FETCH_DEPENDENCY) @@ -26,7 +26,7 @@ $(LOG4J_OVER_SLF4J): $(LOG4J_OVER_SLF4J).md5 SLF4J_API_VERSION := $(SLF4J_VERSION) SLF4J_API := third_party/slf4j/slf4j-api-$(SLF4J_API_VERSION).jar -SLF4J_API_BASE_URL := http://central.maven.org/maven2/org/slf4j/slf4j-api/$(SLF4J_API_VERSION) +SLF4J_API_BASE_URL := https://repo1.maven.org/maven2/org/slf4j/slf4j-api/$(SLF4J_API_VERSION) $(SLF4J_API): $(SLF4J_API).md5 set dummy "$(SLF4J_API_BASE_URL)" "$(SLF4J_API)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/suasync/include.mk b/third_party/suasync/include.mk index 599fe6e89d..1751dfedc2 100644 --- a/third_party/suasync/include.mk +++ b/third_party/suasync/include.mk @@ -15,7 +15,7 @@ SUASYNC_VERSION := 1.4.0 SUASYNC := third_party/suasync/async-$(SUASYNC_VERSION).jar -SUASYNC_BASE_URL := http://central.maven.org/maven2/com/stumbleupon/async/$(SUASYNC_VERSION) +SUASYNC_BASE_URL := https://repo1.maven.org/maven2/com/stumbleupon/async/$(SUASYNC_VERSION) $(SUASYNC): $(SUASYNC).md5 set dummy "$(SUASYNC_BASE_URL)" "$(SUASYNC)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/validation-api/include.mk b/third_party/validation-api/include.mk index 3bd2f96f7d..e2bcd957ea 100644 --- a/third_party/validation-api/include.mk +++ b/third_party/validation-api/include.mk @@ -15,7 +15,7 @@ VALIDATION_API_VERSION := 1.0.0.GA VALIDATION_API := third_party/validation-api/validation-api-$(VALIDATION_API_VERSION).jar -VALIDATION_API_BASE_URL := http://central.maven.org/maven2/javax/validation/validation-api/$(VALIDATION_API_VERSION) +VALIDATION_API_BASE_URL := https://repo1.maven.org/maven2/javax/validation/validation-api/$(VALIDATION_API_VERSION) $(VALIDATION_API): $(VALIDATION_API).md5 set dummy "$(VALIDATION_API_BASE_URL)" "$(VALIDATION_API)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/zookeeper/include.mk b/third_party/zookeeper/include.mk index 7fc7695f4f..6ca1c99ce8 100644 --- a/third_party/zookeeper/include.mk +++ b/third_party/zookeeper/include.mk @@ -15,7 +15,7 @@ ZOOKEEPER_VERSION := 3.4.6 ZOOKEEPER := third_party/zookeeper/zookeeper-$(ZOOKEEPER_VERSION).jar -ZOOKEEPER_BASE_URL := http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/$(ZOOKEEPER_VERSION) +ZOOKEEPER_BASE_URL := https://repo1.maven.org/maven2/org/apache/zookeeper/zookeeper/$(ZOOKEEPER_VERSION) $(ZOOKEEPER): $(ZOOKEEPER).md5 set dummy "$(ZOOKEEPER_BASE_URL)" "$(ZOOKEEPER)"; shift; $(FETCH_DEPENDENCY) diff --git a/tools/check_tsd_v2 b/tools/check_tsd_v2 new file mode 100755 index 0000000000..8d92b48e90 --- /dev/null +++ b/tools/check_tsd_v2 @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 + +from urllib import request +import json +import operator +import logging +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + +AGGREGATORS = ("avg", "count", "dev", "diff", + "ep50r3", "ep50r7", "ep75r3", "ep75r7", "ep90r3", "ep90r7", "ep95r3", + "ep95r7", "ep99r3", "ep99r7", "ep999r3", "ep999r7", + "mimmin", "mimmax", "min", "max", "none", + "p50", "p75", "p90", "p95", "p99", "p999", + "sum", "zimsum") +FILL_POLICIES = ("none", "nan", "null", "zero") +METHODS = ("gt", "ge", "lt", "le", "eq", "ne") +ALARMS = ("warn", "crit") +log = logging.getLogger("check_tsd_v2") +log.setLevel(logging.INFO) +ch = logging.StreamHandler() +logformat = "%(message)s" +formatter = logging.Formatter(logformat) +ch.setFormatter(formatter) +log.addHandler(ch) + + +def _get_metrics(query, timeout): + """ + Actually get data from OpenTSDB + + :param str query: the query string + :param int timeout: how long to wait for OpenTSDB to respond + :returns: yields metrics from the resulting list one at a time + :rtype: dict (generator) + """ + try: + log.debug("Sending Query: {}".format(query)) + res = request.urlopen(query, timeout=timeout) + metrics = json.loads(res.read().decode("utf-8")) + except Exception as e: + log.error("Failed to collect metrics: {}".format(e)) + exit(1) + for m in metrics: + yield m + + +def build_query(args): + """ + Format the query we'll be sending to OpenTSDB + + :param dict args: All arguments needed to format query + :returns: formatted query + :rtype: str + """ + if args.ssl: + query = "https" + else: + query = "http" + query += "://{}:{}/api/query?".format(args.host, args.port) + query += "start={}s-ago&".format(args.duration) + if args.ignore_recent > 0: + query +="end={}s-ago&".format(args.ignore_recent) + query +="noAnnotations=true&m={}:".format(args.aggregator) + + if args.rate: + query += "rate" + if args.rate_counter or args.rate_reset_value: + if args.rate_counter: + query += "{counter,," + else: + query += "{,," + if args.rate_reset_value: + query += "{}}".format(args.rate_reset_value) + else: + query += "}" + query += ":" + if args.downsample: + query += "{}s-{}".format(args.downsample_window, args.downsample) + if args.downsample_fill_policy: + query += "-{}".format(args.downsample_fill_policy) + query += ":" + query += args.metric + if args.tag: + tags = ",".join(args.tag) + query += "{" + query += tags + query += "}" + return query + + +def build_comparisons(expressions): + """ + Turn a string object like 'gt,100,crit' into a tuple that + python can use to evaluate state. + Also ensure critical checks are put first in the list + so we don't evaluate a datapoint as WARNING that should + be CRITICAL + + :param list expressions: all expression strings + :returns: formatted expressions + :rtype: list + """ + comparisons = [] + for expression in expressions: + comparator, value, alarm = expression.split(",") + if comparator not in METHODS: + log.error("Invalid comparison method.") + exit(1) + if alarm not in ALARMS: + log.error("Invalid alarm type.") + exit(1) + try: + value = float(value) + except ValueError: + log.error("Alarm value must be a number.") + exit(1) + comparator = operator.__dict__[comparator] + comparisons.append((comparator, value, alarm)) + # Ensure we check criticals first, since all comparisons are ORed + sorted_comp = [] + for comp in comparisons: + if comp[2] == "crit": + sorted_comp.insert(0, comp) + else: + sorted_comp.append(comp) + return sorted_comp + + +def _process_metric(m, args, comparisons): + """ + Evaluate a single metric from OpenTSDB. + In this case, a metric is a object containing a list + of tuples of (ts, value) and a separate group of tags related + to the metric. + + :param dict m: the actual metric data + :param dict args: all arguments needed to perform evaluations + :param list comparisons: all comparison tuples + :returns: object describing the metric evaluated and its state + :rtype: dict + """ + value_count = len(m["dps"]) + mresult = {"crit": 0, "crit_alarm": False, "warn_alarm": False, "warn": 0, + "crit_percent": 0, "warn_percent": 0, "empty": False, "metric_avg": 0} + if args.tag: + keys = [t.split("=")[0] for t in args.tag] + mresult["tags"] = [v for k, v in m["tags"].items() if k in keys] + if value_count < 1: + mresult["empty"] = True + return mresult + + avglist = [] + for ts, d in m["dps"].items(): + log.debug("Processing timestamp {} value {}".format(ts,d)) + try: + ts = int(ts) + except ValueError: + log.error("Bad timestamp for {}: {}".format(",".join(mresult["tags"]), ts)) + mresult["crit_alarm"] = True + break + avglist.append(d) + for comparison in comparisons: + comparator, value, alarm = comparison + if comparator(d, value): + mresult[alarm] += 1 + break + mresult["metric_avg"] = sum(avglist)/len(avglist) + log.debug("Number of datapoints outside of critical threshold: {}".format(mresult["crit"])) + log.debug("Number of datapoints outside of warning threshold: {}".format(mresult["warn"])) + mresult["crit_percent"] = mresult["crit"] / value_count * 100 + mresult["warn_percent"] = mresult["warn"] / value_count * 100 + if mresult["crit"] > 0: + if args.percent_over > 0 and mresult["crit_percent"] < args.percent_over: + log.debug("Calculated Critical Percent: {:.1f}, less than value of percent_over argument: {}".format(mresult["crit_percent"], args.percent_over)) + mresult["crit_alarm"] = False + else: + log.debug("Calculated Critical Percent: {:.1f}, more than value of percent_over argument: {}".format(mresult["crit_percent"], args.percent_over)) + mresult["crit_alarm"] = True + if mresult["warn"] > 0: + if args.percent_over > 0 and mresult["warn_percent"] < args.percent_over: + log.debug("Calculated Warning Percent: {:.1f}, less than value of percent_over argument: {}".format(mresult["warn_percent"], args.percent_over)) + mresult["warn_alarm"] = False + else: + log.debug("Calculated Warning Percent: {:.1f}, more than value of percent_over argument: {}".format(mresult["warn_percent"], args.percent_over)) + mresult["warn_alarm"] = True + return mresult + + +def process_metrics(query, args, comparisons): + """ + Because we may get multiple metric "groups" back (if a query like + system.load5{host=*} was sent in) we need to evaluate each individual + metric "group" that returns from _get_metrics(). This wrapper helps + us do just that. + + :param str query: The query to send to OpenTSDB + :param dict args: All potential evaluation arguments + :param list comparisons: all comparison tuples to use for evaluating state + :returns: yields each evaluated metric object as it compeletes + :rtype: dict (generator) + """ + for m in _get_metrics(query, args.timeout): + yield _process_metric(m, args, comparisons) + + +def cli_opts(): + parser = ArgumentParser(description="check tsd query", + formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument("-H", "--host", default="localhost", type=str, + help="host to check for stats") + parser.add_argument("-p", "--port", default=4242, type=int, + help="port to check for stats") + parser.add_argument("-m", "--metric", required=True, type=str, + help="Metric to query.") + parser.add_argument("-t", "--tag", action="append", default=[], + help="Tags to filter the metric on.") + parser.add_argument("-d", "--duration", type=int, default=3600, + help="How far back to look for data.") + parser.add_argument("-D", "--downsample", default=None, + help="Downsample function", choices=AGGREGATORS) + parser.add_argument("-W", "--downsample-window", type=int, default=60, + help="Window size over which to downsample.") + parser.add_argument("-F", "--downsample-fill-policy", default=None, + help="Downsample Fill Policies", choices=FILL_POLICIES) + parser.add_argument("-a", "--aggregator", default="sum", + help="Aggregation method", choices=AGGREGATORS) + parser.add_argument("-r", "--rate", default=False, + action="store_true", help="Use rate value as comparison operand.") + parser.add_argument("--rate-counter", default=False, + action="store_true", help="Use rate counter") + parser.add_argument("--rate-reset-value", default=0, + type=int, help="rate reset value") + parser.add_argument("-e", "--expression", action="append", required=True, + help="Comparison expression. e.g. gt,100,warn (multiple allowed)\n" + "Allowed methods: {}\nAllowed alarms: {}".format(",".join(METHODS), ",".join(ALARMS))) + parser.add_argument("-I", "--ignore-recent", default=0, type=int, + help="Ignore data points from this many seconds ago or newer.") + parser.add_argument("-P", "--percent-over", dest="percent_over", default=0, + type=float, help="Only alarm if PERCENT of the data" + " points violate the threshold.") + parser.add_argument("-S", "--ssl", default=False, action="store_true", + help="Make queries to OpenTSDB via SSL (https)") + parser.add_argument("-T", "--timeout", type=int, default=30, + help="How long to wait for the response from TSD.") + parser.add_argument("-A", "--alarm-empty", default=False, + action="store_true", help="Alert when an emtpy series returns") + parser.add_argument("--debug", default=False, + action="store_true", help="Verbose logging") + return parser.parse_args() + + +def main(): + args = cli_opts() + if args.debug: + log.setLevel(logging.DEBUG) + if args.percent_over > 100 or args.percent_over < 0: + log.error("Percentage over must be a value from 0-100: {}".format(args.percent_over)) + exit(1) + if args.downsample_window < 0: + log.error("Downsample window must be positive: {}".format(args.percent_over)) + exit(1) + if args.downsample_window < 0: + log.error("Downsample window must be positive: {}".format(args.percent_over)) + exit(1) + if args.ignore_recent >= args.duration: + log.error("Ignore Recent parameter must be smaller than Duration: {}".format(args.ignore_recent)) + exit(1) + comparisons = build_comparisons(args.expression) + query = build_query(args) + + crit = False + warn = False + crits = [] + warns = [] + total = [] + for r in process_metrics(query, args, comparisons): + total.append(r["tags"]) + if args.alarm_empty and r["empty"]: + log.info("{} => no data returned in range.".format(",".join(r["tags"]))) + crits.append(r["tags"]) + crit = True + continue + if r["crit_alarm"]: + crits.append(r["tags"]) + crit = True + elif r["warn_alarm"]: + warns.append(r["tags"]) + warn = True + alerts = r["crit"] + r["warn"] + perc = r["crit_percent"] + r["warn_percent"] + log.info("{} => outside threshold {} times in range. ({:.1f}% alarms). Avg. Value: {}".format(",".join(r["tags"]), + alerts, perc, r["metric_avg"])) + log.info("{} total metrics processed".format(len(total))) + crit_count = len(crits) + warn_count = len(warns) + if crit_count > 0: + log.info("{} Critical Alarms".format(crit_count)) + if warn_count > 0: + log.info("{} Warning Alarms".format(warn_count)) + if crit: + exit(2) + elif warn: + exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/docker/docker.sh b/tools/docker/docker.sh deleted file mode 100755 index 17686a0e61..0000000000 --- a/tools/docker/docker.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -x -BUILDROOT=./build; -TOOLS=./tools -DOCKER=$BUILDROOT/docker; -rm -r $DOCKER; -mkdir -p $DOCKER; -SOURCE_PATH=$BUILDROOT; -DEST_PATH=$DOCKER/libs; -mkdir -p $DEST_PATH; -cp ${TOOLS}/docker/Dockerfile ${DOCKER}; -cp ${BUILDROOT}/../src/opentsdb.conf ${DOCKER}; -cp ${BUILDROOT}/../src/logback.xml ${DOCKER}; -#cp ${BUILDROOT}/../src/mygnuplot.sh ${DOCKER}; -cp ${SOURCE_PATH}/tsdb-2.3.0-RC1.jar ${DOCKER}; -cp ${SOURCE_PATH}/third_party/*/*.jar ${DEST_PATH}; -docker build -t opentsdb/opentsdb $DOCKER diff --git a/tools/repair-tsd b/tools/repair-tsd index 43573dcdbf..79868cb3ca 100755 --- a/tools/repair-tsd +++ b/tools/repair-tsd @@ -181,8 +181,6 @@ def cli_opts(): help="Path to the OpenTSDB CLI binary") parser.add_argument("--cfg-path", default="/etc/opentsdb/opentsdb.conf", help="Path to OpenTSDB config") - parser.add_argument("--store-path", default="/tmp/opentsdb-fsck.list", - help="Path to OpenTSDB config") parser.add_argument("--use-sudo", action="store_true", default=False, help="switch user when running repairs?") @@ -228,7 +226,6 @@ def main(): "time_chunk": time_chunk, "tsd_path": args.tsd_path, "cfg_path": args.cfg_path, - "store_path": args.store_path, "shuffle": args.shuffle, "compact": args.compact, "retries": retries}) diff --git a/tools/tsdb_list_running_queries.py b/tools/tsdb_list_running_queries.py new file mode 100755 index 0000000000..466e6caf17 --- /dev/null +++ b/tools/tsdb_list_running_queries.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# pylint: disable=line-too-long,missing-docstring +# +# List the running queries on the local TSD, sorted ascending by age, with normalized start time, end time, and time range in secs + +from __future__ import print_function +from __future__ import division + +import httplib +import json +import os +import socket +import sys +import time + + +class ConnectionException(Exception): + pass + + +class OpenTSDBListRunningQueries(object): + + format_string = '{:<19}\t{:<10}\t{:<19}\t{:<19}\t{:<16}\t{:<10}\t{:<10}\t{}' + + # used by ms_to_secs() method further down to compare the epoch and figure out if it is in secs or ms and normalize it + # pulling this out of the iteratively called method is a little more efficient to not recalculate this for every query in the list + now_length = len(str(int(time.time()))) + + timestamp_multiplier = { + 'ms': 0.001, + 's': 1, + 'm': 60, + 'h': 3600, + 'd': 86400, + 'w': 7 * 86400, + 'n': 30 * 86400, + 'y': 365 * 86400 + } + + def __init__(self): + self.host = 'localhost' + self.port = 4242 + self.uri = '/api/stats/query' + self.server = httplib.HTTPConnection(self.host, self.port) + self.server.auto_open = True + + def request(self): + try: + self.server.request('GET', self.uri) + resp = self.server.getresponse().read() + self.server.close() + except socket.error as _: + raise ConnectionException('Socket Error querying TSDB port: {}'.format(_)) + except httplib.HTTPException as _: + raise ConnectionException('HTTP Error querying TSDB port: {}'.format(_)) + return json.loads(resp) + + # reference_timestamp is for comparing N-ago timestamps + def convert_timestamp_to_epoch(self, timestamp, reference_timestamp): + timestamp = str(timestamp).split('.')[0] + reference_timestamp_struct = time.strptime(reference_timestamp, '%Y-%m-%d %H:%M:%S') # %z not supported in the C runtime, and no workaround until Python 3.2 + reference_timestamp_secs = time.mktime(reference_timestamp_struct) + if '/' in timestamp: + timestamp = time.strptime(timestamp, '%Y/%m/%d-%H:%M:%S') + timestamp = time.mktime(timestamp) + elif timestamp == 'now': + timestamp = reference_timestamp_secs + elif timestamp[-4:] == '-ago': + timestamp = timestamp[:-4] + (ago, multiplier) = (timestamp[:-1], timestamp[-1]) + secs_ago = int(ago) * self.timestamp_multiplier[multiplier] + timestamp = int(reference_timestamp_secs) - secs_ago + timestamp = int(float(timestamp)) + timestamp = self.ms_to_secs(timestamp) + return timestamp + + def convert_human_time(self, epoch): + epoch = self.ms_to_secs(epoch) + human_time = time.strftime('%F %T', time.gmtime(float(epoch))) + #print('converted epoch {} to {}'.format(epoch, human_time)) + return human_time + + def ms_to_secs(self, epoch): + # convert from ms to secs if epoch is in ms + if len(str(epoch)) > self.now_length: + epoch = int(int(epoch) / 1000) + return epoch + + def process_query(self, query): + if os.getenv('DEBUG'): + print(json.dumps(query, indent=4, sort_keys=True)) + user = query.get('headers', {}).get('X-WEBAUTH-USER', '') + querystart = query.get('queryStart', '') + querystart_secs = self.ms_to_secs(querystart) + querystart = self.convert_human_time(querystart_secs) + query = query['query'] + start = query['start'] + start_epoch = self.ms_to_secs(self.convert_timestamp_to_epoch(start, querystart)) + start = self.convert_human_time(start_epoch) + end = query.get('end', '') + if end: + end_epoch = self.ms_to_secs(self.convert_timestamp_to_epoch(end, querystart)) + else: + end_epoch = int(time.mktime(time.strptime(querystart, '%Y-%m-%d %H:%M:%S'))) + end = self.convert_human_time(end_epoch) + timerange_secs = end_epoch - start_epoch + running_subquery_count = 0 + for subquery in query.get('queries', []): + metric = subquery.get('metric', '') + aggregator = subquery.get('aggregator') + downsample = subquery.get('downsample') + print(self.format_string.format(querystart, user, start, end, timerange_secs, aggregator, downsample, metric)) + running_subquery_count += 1 + return running_subquery_count + + def main(self): + stats = self.request() + running_queries = stats.get('running', []) + print('='*160) + print(self.format_string.format('Date', 'User', 'Start', 'End', 'TimeRange (Secs)', 'Aggregator', 'Downsample', 'Metric')) + print('='*160 + '\n') + running_queries.sort(key=lambda x: int(x['queryStart']), reverse=False) + running_subquery_count = 0 + for query in running_queries: + running_subquery_count += self.process_query(query) + running_query_count = len(running_queries) + print('\nListed {} running queries, {} individual subqueries'.format(running_query_count, running_subquery_count)) + sys.stdout.flush() + return 0 + + +if __name__ == "__main__": + try: + OpenTSDBListRunningQueries().main() + except ConnectionException as _: + print(_, file=sys.stderr) + sys.exit(2) + except KeyboardInterrupt: + print("Control-C, aborting...") + sys.exit(3)